All checks were successful
check / check (push) Successful in 32s
A user who created a wallet in AutistMask and did not write the phrase
down had no way to retrieve it. Adds a "Show recovery phrase" action on
the wallet row in Settings, next to the per-wallet actions that already
live there, mirroring the per-address private key export in structure,
password gate and warning treatment.
The screen displays the secret that owns every address in the wallet, so:
- Only HD wallets are offered it. walletHasRecoveryPhrase() is an
allowlist on type "hd", so the key and xprv types — which have no
phrase at all — are excluded, as is any type added later.
- Nothing is decrypted and nothing enters the page until
decryptWithPassword accepts the password. A wrong password produces a
full-sentence error and leaves the value node empty.
- Leaving the screen wipes it by any route, not just "Back": views that
hold a secret register a cleanup with showView() via onViewLeave(),
which also covers the settings gear.
- The phrase is never assigned to state, so it cannot be persisted, and
the view is not in RESTORABLE_VIEWS — reopening the popup lands on
Home. That set moves to src/popup/restorableViews.js so the exclusion
can be asserted directly; the popup entry point cannot be required
outside a browser.
- The phrase cannot reach the logger: the view does not import
src/shared/log.js, and the failed-decrypt path reports a fixed
sentence rather than the caught error.
Tests: unit coverage for the type gate, the RESTORABLE_VIEWS exclusion
and the absence of any logger path; the DOM behaviour is driven against
the real popup in the e2e suite, which is where this repo tests views.
95 lines
3.7 KiB
JavaScript
95 lines
3.7 KiB
JavaScript
// Tests for the recovery phrase display (issue #161).
|
|
//
|
|
// These cover the parts that do not need a DOM: which wallet types may be
|
|
// offered the action at all, the exclusion of the screen from the set of
|
|
// views the popup may reopen onto, and the absence of any path from this
|
|
// module to the logger. The DOM behaviour it guards — nothing rendered
|
|
// before the password is accepted, a wrong password revealing nothing, and
|
|
// the wipe on leaving — is driven against the real popup in a real browser
|
|
// by tests/e2e/run.js, which is where every other view behaviour is tested.
|
|
|
|
const fs = require("fs");
|
|
const path = require("path");
|
|
|
|
const { walletHasRecoveryPhrase } = require("../src/shared/wallet");
|
|
const { RESTORABLE_VIEWS } = require("../src/popup/restorableViews");
|
|
|
|
const SHOW_PHRASE_VIEW = "show-phrase";
|
|
|
|
// helpers.js pulls in state.js, which reads chrome.storage.local at load.
|
|
function loadHelpers() {
|
|
globalThis.chrome = {
|
|
storage: { local: { get: async () => ({}), set: async () => {} } },
|
|
};
|
|
return require("../src/popup/views/helpers");
|
|
}
|
|
|
|
describe("which wallets have a recovery phrase", () => {
|
|
test("an HD wallet does", () => {
|
|
expect(walletHasRecoveryPhrase({ type: "hd" })).toBe(true);
|
|
});
|
|
|
|
// A key wallet holds a bare private key and an xprv wallet an extended
|
|
// private key. Neither can be turned back into words, so neither may be
|
|
// offered the action.
|
|
test("a key wallet does not", () => {
|
|
expect(walletHasRecoveryPhrase({ type: "key" })).toBe(false);
|
|
});
|
|
|
|
test("an xprv wallet does not", () => {
|
|
expect(walletHasRecoveryPhrase({ type: "xprv" })).toBe(false);
|
|
});
|
|
|
|
test("an unknown or missing wallet type does not", () => {
|
|
expect(walletHasRecoveryPhrase({ type: "something-new" })).toBe(false);
|
|
expect(walletHasRecoveryPhrase({})).toBe(false);
|
|
expect(walletHasRecoveryPhrase(undefined)).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe("views the popup may reopen onto", () => {
|
|
// Restoring onto a secret screen would put the phrase on screen with no
|
|
// password prompt in front of it, on a popup the user may have reopened
|
|
// by accident.
|
|
test("the recovery phrase screen is not restorable", () => {
|
|
expect(RESTORABLE_VIEWS.has(SHOW_PHRASE_VIEW)).toBe(false);
|
|
});
|
|
|
|
test("the private key export screen is not restorable either", () => {
|
|
expect(RESTORABLE_VIEWS.has("export-privkey")).toBe(false);
|
|
});
|
|
|
|
test("the recovery phrase screen is still a registered view", () => {
|
|
const { VIEWS } = loadHelpers();
|
|
expect(VIEWS).toContain(SHOW_PHRASE_VIEW);
|
|
});
|
|
|
|
// Guards the other direction: a restorable name that is not a real view
|
|
// would leave restoreView() showing nothing at all.
|
|
test("every restorable view is a registered view", () => {
|
|
const { VIEWS } = loadHelpers();
|
|
for (const view of RESTORABLE_VIEWS) {
|
|
expect(VIEWS).toContain(view);
|
|
}
|
|
});
|
|
});
|
|
|
|
describe("the phrase cannot reach the logger", () => {
|
|
const source = fs.readFileSync(
|
|
path.join(__dirname, "..", "src", "popup", "views", "showPhrase.js"),
|
|
"utf8",
|
|
);
|
|
|
|
// The decrypted phrase only ever lives in a local and in the DOM node
|
|
// that displays it. The module has no logger to hand it to, and this
|
|
// pins that: src/shared/log.js writes to the console, and a console
|
|
// record of a recovery phrase outlives the popup.
|
|
test("the view does not import src/shared/log.js", () => {
|
|
expect(source).not.toMatch(/require\(["'][^"']*shared\/log["']\)/);
|
|
});
|
|
|
|
test("the view calls no logger method", () => {
|
|
expect(source).not.toMatch(/\blog\.(debugf|infof|warnf|errorf)\b/);
|
|
});
|
|
});
|