Five defects, one of which destroyed every wallet, came from src/background reading and writing the module-level state singleton the MV3 worker never populates, which silently served DEFAULT_STATE. Each point fix created the next defect. The background now has its own per-call getState() and a queued read-modify-write updateState(); the singleton is unreachable from it, and an unpopulated read throws instead of serving defaults. The prohibition is enforced by the build, not by review: build.js asserts over esbuild's own metafile that no forbidden module is an input of a background bundle, so every specifier syntax esbuild resolves is covered, and both halves of the table are checked for rot -- a stale key, a stale module, an empty list, or an unlisted entry point under src/background/ all fail the build. The ESLint rule remains as fast local feedback and reads the same shared table. Known bounds are documented where the table lives. Also closes #320: getProvider() now requires a validated network id, so a cold worker no longer prepares a non-mainnet dApp transaction for mainnet and gets refused by the wallet's own verifier. backgroundRefresh() no longer mutates address objects across a network round trip, the broadcast path takes its endpoint and chain id from one snapshot, and eight test storage stubs now structured-clone on get as the real chrome.storage.local does. closes #320
74 lines
2.8 KiB
JavaScript
74 lines
2.8 KiB
JavaScript
// A chrome.storage.local stub that behaves like the real one.
|
|
//
|
|
// The real extension storage API is a serialization boundary: `set` writes a
|
|
// structured clone of what it is given, and `get` hands back a structured
|
|
// clone of what is stored. Nothing an extension page holds is ever the object
|
|
// storage holds.
|
|
//
|
|
// A stub that skips the clone aliases them together, and that hides an entire
|
|
// class of defect rather than merely being imprecise. loadState() assigns
|
|
// nested references straight out of the get result, so over an aliasing stub a
|
|
// test can assert "the endpoint was persisted" and pass on a build that never
|
|
// called saveState() at all: the in-memory mutation IS the stored record.
|
|
// Measured, not theorised — with an aliasing `get` restored over the handler
|
|
// fixed in https://git.eeqj.de/sneak/AutistMask/pulls/319, the whole suite
|
|
// passed 794/794 (https://git.eeqj.de/sneak/AutistMask/issues/324).
|
|
//
|
|
// So every test that drives real persistence uses this, and nothing rebuilds
|
|
// a storage stub by hand.
|
|
|
|
// `initial` is the starting contents, keyed as storage is: { autistmask: {...} }.
|
|
// `onOp` runs before each operation, for a test that needs to advance a clock
|
|
// or count round trips.
|
|
function makeStorageStub(initial, onOp) {
|
|
const store = initial ? structuredClone(initial) : {};
|
|
const tick = onOp || (() => {});
|
|
|
|
const get = jest.fn(async (key) => {
|
|
tick();
|
|
if (key === undefined || key === null) return structuredClone(store);
|
|
const keys = Array.isArray(key) ? key : [key];
|
|
const out = {};
|
|
for (const k of keys) {
|
|
if (Object.prototype.hasOwnProperty.call(store, k)) {
|
|
out[k] = structuredClone(store[k]);
|
|
}
|
|
}
|
|
return out;
|
|
});
|
|
|
|
const set = jest.fn(async (items) => {
|
|
tick();
|
|
for (const k of Object.keys(items)) {
|
|
store[k] = structuredClone(items[k]);
|
|
}
|
|
});
|
|
|
|
const remove = jest.fn(async (key) => {
|
|
tick();
|
|
for (const k of Array.isArray(key) ? key : [key]) delete store[k];
|
|
});
|
|
|
|
return {
|
|
// Drop this straight in as chrome.storage.
|
|
local: { get, set, remove },
|
|
get,
|
|
set,
|
|
remove,
|
|
// What is stored, cloned on the way out: a test can neither observe a
|
|
// later write through an object it read nor reach into the store by
|
|
// mutating one.
|
|
read: (key) =>
|
|
key === undefined
|
|
? structuredClone(store)
|
|
: structuredClone(store[key]),
|
|
// Seed or replace a record without going through the module under
|
|
// test — for standing in as "another page wrote this".
|
|
write: (key, value) => {
|
|
store[key] = structuredClone(value);
|
|
},
|
|
};
|
|
}
|
|
|
|
module.exports = { makeStorageStub };
|