From 2d9fe10252e5ab5220f8bd679008ecee6301cf7f Mon Sep 17 00:00:00 2001 From: Alain Brenzikofer Date: Thu, 10 Sep 2026 13:12:27 +0000 Subject: [PATCH] core: test the restore cases the key has to survive A database backed up after the key carries the key and the binding, so the restore is all of it and an import is refused. A database backed up before the key comes back with the profiles and no binding, so the phrase is imported and each profile says which account was its. A key imported before the database is restored is replaced by what the backup held. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01Rvc3HbiWBTqbAvRT45G5oX --- tests/WalletTests.hs | 66 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 66 insertions(+) diff --git a/tests/WalletTests.hs b/tests/WalletTests.hs index 6aed5410a6..ac31d56738 100644 --- a/tests/WalletTests.hs +++ b/tests/WalletTests.hs @@ -53,6 +53,9 @@ walletTests = do it "exports the secret of any name key" testWalletExportDerivedSecret it "deletes the key, and a key can be imported again" testWalletDelete it "binds a profile to the account it had" testWalletBind + it "needs no import when the database was backed up after the key" testWalletBackupAfterKey + it "rebinds by index when the database was backed up before the key" testWalletBackupBeforeKey + it "discards a key imported before the database is restored" testWalletImportThenRestore nameRows :: HasCallStack => TestCC -> IO [(String, String)] nameRows cc = mapM (\_ -> nameRow <$> getTermLine cc) [0 .. 1 :: Int] @@ -183,3 +186,66 @@ testWalletBind ps = withNewTestChat ps "alice" aliceProfile $ \alice -> do rows' <- nameRows alice alice <## "also on same seed: alice" map fst rows' `shouldBe` ["m/44'/60'/4'/0/0", "m/44'/60'/4'/0/1"] + +-- | The state a chat database backed up before the key restores to: the +-- profiles are the same rows, and nothing knows about a key. +forgetKey :: HasCallStack => TestCC -> IO () +forgetKey cc = do + cc ##> "/sql chat UPDATE users SET wallet_seed_id = NULL, wallet_account_index = NULL" + cc ##> "/sql chat DELETE FROM wallet_seeds" + +testWalletBackupAfterKey :: HasCallStack => TestParams -> IO () +testWalletBackupAfterKey ps = do + rows <- withNewTestChat ps "alice" aliceProfile $ \alice -> do + alice ##> "/_wallet create" + alice <## "this profile has no wallet key" + alice ##> "/_wallet bind" + nameRows alice + -- the key and the binding are both in the database, so the restore is all of it + withTestChat ps "alice" $ \alice -> do + alice ##> "/_wallet" + rows' <- nameRows alice + rows' `shouldBe` rows + alice ##> ("/_wallet import " <> B.unpack testPhrase) + alice <## "bad chat command: this device already has a wallet key" + +testWalletBackupBeforeKey :: HasCallStack => TestParams -> IO () +testWalletBackupBeforeKey ps = withNewTestChat ps "alice" aliceProfile $ \alice -> do + alice ##> ("/_wallet import " <> B.unpack testPhrase) + alice <## "this profile has no wallet key" + alice ##> "/_wallet bind 1" + aliceRows <- nameRows alice + alice ##> "/create user alisa" + showActiveUser alice "alisa" + alice ##> "/_wallet bind 0" + alisaRows <- nameRows alice + alice <## "also on same seed: alice" + forgetKey alice + alice ##> "/_wallet" + alice <## "no wallet key" + -- the phrase alone puts every account back, and each profile says which was its + alice ##> ("/_wallet import " <> B.unpack testPhrase) + alice <## "this profile has no wallet key" + alice ##> "/_wallet bind 0" + alisaRows' <- nameRows alice + alisaRows' `shouldBe` alisaRows + alice ##> "/user alice" + showActiveUser alice "alice (Alice)" + alice ##> "/_wallet bind 1" + aliceRows' <- nameRows alice + alice <## "also on same seed: alisa" + aliceRows' `shouldBe` aliceRows + +testWalletImportThenRestore :: HasCallStack => TestParams -> IO () +testWalletImportThenRestore ps = withNewTestChat ps "alice" aliceProfile $ \alice -> do + alice ##> ("/_wallet import " <> B.unpack testPhrase) + alice <## "this profile has no wallet key" + -- restoring the database replaces the key with what the backup held, which is nothing + forgetKey alice + alice ##> "/_wallet" + alice <## "no wallet key" + alice ##> ("/_wallet import " <> B.unpack testPhrase) + alice <## "this profile has no wallet key" + alice ##> "/_wallet bind" + rows <- nameRows alice + map fst rows `shouldBe` ["m/44'/60'/0'/0/0", "m/44'/60'/0'/0/1"]