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
355 lines
12 KiB
JavaScript
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",
|
|
});
|
|
});
|
|
});
|