harden: stop the background reading the shared state singleton, and enforce it at build time (closes #324)
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
This commit was merged in pull request #344.
This commit is contained in:
73
tests/support/storageStub.js
Normal file
73
tests/support/storageStub.js
Normal file
@@ -0,0 +1,73 @@
|
||||
// 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 };
|
||||
Reference in New Issue
Block a user