Files
AutistMask/tests/walletDelete.test.js
T
sneak e557f21bb0
check / check (push) Failing after 2s
e2e / e2e-chrome (push) Failing after 3s
e2e / e2e-firefox (push) Failing after 2s
harden: key remembered site permissions by full origin (closes #402)
allowedSites and deniedSites held the bare hostname, so a grant to
https://dapp.example also authorised http://dapp.example and every port
on that host, and the connection, transaction and signature prompts
named only the hostname. Both lists now store and match the full origin
(scheme://host[:port]), the key the connections approved without
Remember already used. The prompts, the Settings site lists and
AUTISTMASK_REMOVE_SITE use the origin too. Entries saved by hostname
are not migrated (pre-1.0): they match no site.

Model: opus-5-5
2026-10-04 15:43:58 +00:00

355 lines
12 KiB
JavaScript

const {
canRemoveAddress,
removeAddressFromState,
removeWalletFromState,
broadcastActiveChanged,
} = require("../src/shared/walletDelete");
// Fixed addresses — never used for anything but these tests.
const A0 = "0x66133E8ea0f5D1d612D2502a968757D1048c214a";
const A1 = "0xdAC17F958D2ee523a2206206994597C13D831ec7";
const A2 = "0x514910771AF9Ca656af840dff83E8264EcF986CA";
const B0 = "0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599";
const C0 = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48";
function wallet(name, addresses) {
return {
name,
addresses: addresses.map((address) => ({ address })),
};
}
// A three-wallet state; wallet A is an HD wallet with two addresses.
function makeState(overrides = {}) {
return {
hasWallet: true,
wallets: [wallet("A", [A0, A1]), wallet("B", [B0]), wallet("C", [C0])],
selectedWallet: 0,
selectedAddress: 0,
activeAddress: A0,
allowedSites: {
[A0]: ["https://a.example"],
[A1]: ["https://b.example"],
[B0]: ["https://c.example"],
},
deniedSites: {
[A1]: ["https://d.example"],
[C0]: ["https://e.example"],
},
...overrides,
};
}
describe("removeWalletFromState", () => {
test("deleting the last wallet clears hasWallet", () => {
const state = makeState({
wallets: [wallet("A", [A0])],
allowedSites: { [A0]: ["https://a.example"] },
deniedSites: {},
});
const { activeAddressChanged } = removeWalletFromState(state, 0);
expect(state.hasWallet).toBe(false);
expect(state.wallets).toEqual([]);
expect(state.selectedWallet).toBeNull();
expect(state.selectedAddress).toBeNull();
expect(state.activeAddress).toBeNull();
expect(activeAddressChanged).toBe(true);
});
test("deleting a non-selected wallet leaves the selection intact", () => {
const state = makeState({
selectedWallet: 2,
selectedAddress: 0,
activeAddress: C0,
});
const { activeAddressChanged } = removeWalletFromState(state, 1);
// Wallet C moved from index 2 to index 1 by the splice.
expect(state.wallets.map((w) => w.name)).toEqual(["A", "C"]);
expect(state.selectedWallet).toBe(1);
expect(state.selectedAddress).toBe(0);
expect(state.activeAddress).toBe(C0);
expect(activeAddressChanged).toBe(false);
expect(state.hasWallet).toBe(true);
});
test("deleting a wallet after the selection does not shift it", () => {
const state = makeState({
selectedWallet: 1,
selectedAddress: 0,
activeAddress: B0,
});
const { activeAddressChanged } = removeWalletFromState(state, 2);
expect(state.selectedWallet).toBe(1);
expect(state.activeAddress).toBe(B0);
expect(activeAddressChanged).toBe(false);
});
test("deleting the active wallet falls back to the first remaining address", () => {
const state = makeState({
selectedWallet: 0,
selectedAddress: 1,
activeAddress: A1,
});
const { activeAddressChanged } = removeWalletFromState(state, 0);
expect(state.wallets.map((w) => w.name)).toEqual(["B", "C"]);
expect(state.selectedWallet).toBe(0);
expect(state.selectedAddress).toBe(0);
expect(state.activeAddress).toBe(B0);
expect(activeAddressChanged).toBe(true);
expect(state.hasWallet).toBe(true);
});
test("site permissions are dropped for every address of the wallet", () => {
const state = makeState();
removeWalletFromState(state, 0);
expect(state.allowedSites).toEqual({ [B0]: ["https://c.example"] });
expect(state.deniedSites).toEqual({ [C0]: ["https://e.example"] });
});
});
// An HD wallet with three addresses next to a single-address key wallet.
// `nextIndex` is the wallet's derivation high-water mark, three addresses in.
function makeAddressState(overrides = {}) {
return {
hasWallet: true,
wallets: [
{ ...wallet("A", [A0, A1, A2]), type: "hd", nextIndex: 3 },
{ ...wallet("B", [B0]), type: "key" },
],
selectedWallet: 0,
selectedAddress: 0,
activeAddress: A0,
allowedSites: {
[A0]: ["https://a.example"],
[A1]: ["https://b.example"],
},
deniedSites: {
[A1]: ["https://d.example"],
[B0]: ["https://e.example"],
},
...overrides,
};
}
describe("canRemoveAddress", () => {
test("an HD wallet with more than one address may remove one", () => {
expect(canRemoveAddress({ type: "hd", addresses: [{}, {}] })).toBe(
true,
);
});
test("an xprv wallet with more than one address may too", () => {
expect(canRemoveAddress({ type: "xprv", addresses: [{}, {}] })).toBe(
true,
);
});
// The last address is what delete-wallet is for.
test("a wallet holding a single address may not", () => {
expect(canRemoveAddress({ type: "hd", addresses: [{}] })).toBe(false);
});
// A key wallet holds one bare private key and cannot derive more, so it
// has no "+" button and gets no remove control either.
test("a key wallet may not, whatever its address count", () => {
expect(canRemoveAddress({ type: "key", addresses: [{}] })).toBe(false);
expect(canRemoveAddress({ type: "key", addresses: [{}, {}] })).toBe(
false,
);
});
test("a missing or typeless wallet may not", () => {
expect(canRemoveAddress(undefined)).toBe(false);
expect(canRemoveAddress({})).toBe(false);
});
});
describe("removeAddressFromState", () => {
test("removing a non-selected address leaves the selection where it is", () => {
const state = makeAddressState({
selectedAddress: 2,
activeAddress: A2,
});
const { removed, activeAddressChanged } = removeAddressFromState(
state,
0,
0,
);
expect(removed).toBe(true);
// A2 moved from index 2 to index 1 by the splice.
expect(state.wallets[0].addresses.map((a) => a.address)).toEqual([
A1,
A2,
]);
expect(state.selectedWallet).toBe(0);
expect(state.selectedAddress).toBe(1);
expect(state.activeAddress).toBe(A2);
expect(activeAddressChanged).toBe(false);
// The wallet list itself is untouched.
expect(state.wallets).toHaveLength(2);
expect(state.hasWallet).toBe(true);
});
test("removing an address after the selection does not shift it", () => {
const state = makeAddressState({
selectedAddress: 0,
activeAddress: A0,
});
const { removed, activeAddressChanged } = removeAddressFromState(
state,
0,
2,
);
expect(removed).toBe(true);
expect(state.selectedAddress).toBe(0);
expect(state.activeAddress).toBe(A0);
expect(activeAddressChanged).toBe(false);
});
test("a selection in another wallet is untouched", () => {
const state = makeAddressState({
selectedWallet: 1,
selectedAddress: 0,
activeAddress: B0,
});
const { removed, activeAddressChanged } = removeAddressFromState(
state,
0,
1,
);
expect(removed).toBe(true);
expect(state.selectedWallet).toBe(1);
expect(state.selectedAddress).toBe(0);
expect(state.activeAddress).toBe(B0);
expect(activeAddressChanged).toBe(false);
});
test("removing the selected address falls back to the wallet's first address", () => {
const state = makeAddressState({
selectedAddress: 1,
activeAddress: A1,
});
const { removed, activeAddressChanged } = removeAddressFromState(
state,
0,
1,
);
expect(removed).toBe(true);
expect(state.wallets[0].addresses.map((a) => a.address)).toEqual([
A0,
A2,
]);
expect(state.selectedWallet).toBe(0);
expect(state.selectedAddress).toBe(0);
expect(state.activeAddress).toBe(A0);
expect(activeAddressChanged).toBe(true);
});
// The active address can be persisted in a different case than the
// wallet's copy of it, so the comparison must not be literal.
test("the active address is matched case-insensitively", () => {
const state = makeAddressState({
selectedAddress: 1,
activeAddress: A1.toLowerCase(),
});
const { activeAddressChanged } = removeAddressFromState(state, 0, 1);
expect(state.activeAddress).toBe(A0);
expect(activeAddressChanged).toBe(true);
});
test("site permissions are dropped for the removed address only", () => {
const state = makeAddressState();
removeAddressFromState(state, 0, 1);
expect(state.allowedSites).toEqual({ [A0]: ["https://a.example"] });
expect(state.deniedSites).toEqual({ [B0]: ["https://e.example"] });
});
// The derivation counter is a high-water mark, never rewound: "+" derives
// a fresh index rather than re-deriving the address just removed.
test("the wallet's derivation counter is not rewound", () => {
const state = makeAddressState();
removeAddressFromState(state, 0, 1);
expect(state.wallets[0].nextIndex).toBe(3);
});
test("the last address of a wallet is refused, and nothing changes", () => {
const state = makeAddressState({
selectedWallet: 1,
selectedAddress: 0,
activeAddress: B0,
});
const { removed, activeAddressChanged } = removeAddressFromState(
state,
1,
0,
);
expect(removed).toBe(false);
expect(activeAddressChanged).toBe(false);
expect(state.wallets[1].addresses.map((a) => a.address)).toEqual([B0]);
expect(state.activeAddress).toBe(B0);
expect(state.hasWallet).toBe(true);
});
// The same refusal reached the other way: an HD wallet worn down to one
// address is no more removable than a key wallet.
test("an HD wallet down to its last address is refused too", () => {
const state = makeAddressState();
expect(removeAddressFromState(state, 0, 2).removed).toBe(true);
expect(removeAddressFromState(state, 0, 1).removed).toBe(true);
expect(removeAddressFromState(state, 0, 0).removed).toBe(false);
expect(state.wallets[0].addresses.map((a) => a.address)).toEqual([A0]);
});
test("an out-of-range address index is refused", () => {
const state = makeAddressState();
expect(removeAddressFromState(state, 0, 7).removed).toBe(false);
expect(removeAddressFromState(state, 7, 0).removed).toBe(false);
expect(state.wallets[0].addresses).toHaveLength(3);
});
});
describe("broadcastActiveChanged", () => {
afterEach(() => {
delete global.chrome;
});
test("sends AUTISTMASK_ACTIVE_CHANGED to the background", () => {
const sendMessage = jest.fn();
global.chrome = { runtime: { sendMessage } };
broadcastActiveChanged();
expect(sendMessage).toHaveBeenCalledWith({
type: "AUTISTMASK_ACTIVE_CHANGED",
});
});
});