The transaction lists on the address and token screens, the address scan after a wallet is created, and the endpoint checks in Settings now check the signal the popup aborts on pagehide before reporting a failed request, as the home screen and the balance refresh already do. scanForAddresses() takes the signal ahead of its gap limit. End-to-end tests reload the popup on the address screen with Blockscout held, and during the address scan with its requests held. Jest tests show the transaction lists, the scan and the Settings checks report a real failure and stay silent once the popup has closed. The transaction detail and confirmation screens are left out: they do not keep the popup context. Model: opus-5-5
170 lines
5.5 KiB
JavaScript
170 lines
5.5 KiB
JavaScript
// What reaches the console when an endpoint check in Settings fails.
|
|
//
|
|
// fetch refuses a URL with a user name and password in it, or one it cannot
|
|
// parse, with an error whose message carries the whole URL: the password, and
|
|
// any API key in the path or query string. The checks behind the RPC and
|
|
// Blockscout Save buttons printed that message
|
|
// (https://git.eeqj.de/sneak/AutistMask/issues/410); they now name the
|
|
// endpoint by its origin.
|
|
//
|
|
// The real fetch runs; it throws before making any request. Debug mode is on,
|
|
// so every log level is printed.
|
|
|
|
const SECRETS = ["SECRETPASS789", "PATHKEY123", "QUERYTOKEN456"];
|
|
const RPC_WITH_PASSWORD =
|
|
"https://user:SECRETPASS789@rpc.example.invalid/v3/PATHKEY123?token=QUERYTOKEN456";
|
|
// Port 99999 is out of range, so the URL does not parse.
|
|
const RPC_UNPARSEABLE =
|
|
"https://rpc.example.invalid:99999/v3/PATHKEY123?token=QUERYTOKEN456";
|
|
const BLOCKSCOUT_WITH_PASSWORD =
|
|
"https://user:SECRETPASS789@explorer.example.invalid/PATHKEY123/api/v2";
|
|
|
|
const SAVED_RPC = "https://saved-rpc.example.invalid";
|
|
const SAVED_BLOCKSCOUT = "https://saved-explorer.example.invalid/api/v2";
|
|
|
|
let elements;
|
|
let flashes;
|
|
let printed;
|
|
let state;
|
|
|
|
// A stand-in for one DOM node: enough of an element for init() to set
|
|
// properties on it and hang listeners off it.
|
|
function fakeElement() {
|
|
return {
|
|
value: "",
|
|
checked: false,
|
|
textContent: "",
|
|
href: "",
|
|
style: {},
|
|
dataset: {},
|
|
classList: { add() {}, remove() {} },
|
|
listeners: {},
|
|
addEventListener(event, handler) {
|
|
this.listeners[event] = handler;
|
|
},
|
|
querySelectorAll: () => [],
|
|
};
|
|
}
|
|
|
|
function element(id) {
|
|
return (elements[id] ||= fakeElement());
|
|
}
|
|
|
|
// `pageClosed` stands in for the popup's: aborting it is the popup closing.
|
|
function loadSettingsView(pageClosed = new AbortController()) {
|
|
elements = {};
|
|
flashes = [];
|
|
|
|
jest.resetModules();
|
|
|
|
jest.doMock("../src/popup/views/helpers", () => ({
|
|
$: element,
|
|
showView: () => {},
|
|
updateDebugBanner: () => {},
|
|
showFlash: (msg) => flashes.push(msg),
|
|
escapeHtml: (s) => s,
|
|
flashCopyFeedback: () => {},
|
|
goBack: () => {},
|
|
pushCurrentView: () => {},
|
|
onViewLeave: () => {},
|
|
VIEWS: [],
|
|
}));
|
|
|
|
state = require("../src/shared/state").state;
|
|
state.rpcUrl = SAVED_RPC;
|
|
state.blockscoutUrl = SAVED_BLOCKSCOUT;
|
|
require("../src/shared/log").setRuntimeDebug(true);
|
|
|
|
require("../src/popup/views/settings").init({
|
|
pageClosed: pageClosed.signal,
|
|
});
|
|
}
|
|
|
|
async function save(fieldId, buttonId, typed) {
|
|
element(fieldId).value = typed;
|
|
await element(buttonId).listeners.click();
|
|
}
|
|
|
|
// The check failed, nothing was saved, and nothing printed carries the
|
|
// password or the key.
|
|
function expectFailedWithoutSecrets(label) {
|
|
expect(flashes).toContain("Could not reach endpoint.");
|
|
expect(state.rpcUrl).toBe(SAVED_RPC);
|
|
expect(state.blockscoutUrl).toBe(SAVED_BLOCKSCOUT);
|
|
const line = printed.find((text) => text.includes(label));
|
|
expect(line).toBeDefined();
|
|
const all = printed.join("\n");
|
|
for (const secret of SECRETS) {
|
|
expect(all).not.toContain(secret);
|
|
}
|
|
return line;
|
|
}
|
|
|
|
beforeEach(() => {
|
|
printed = [];
|
|
for (const method of ["log", "warn", "error"]) {
|
|
jest.spyOn(console, method).mockImplementation((...args) => {
|
|
printed.push(args.map(String).join(" "));
|
|
});
|
|
}
|
|
globalThis.chrome = {
|
|
runtime: { sendMessage: () => {} },
|
|
storage: { local: { get: async () => ({}), set: async () => {} } },
|
|
};
|
|
});
|
|
|
|
afterEach(() => {
|
|
jest.dontMock("../src/popup/views/helpers");
|
|
delete globalThis.chrome;
|
|
jest.restoreAllMocks();
|
|
});
|
|
|
|
test("fetch puts the whole URL in the error it throws for such a URL", async () => {
|
|
for (const url of [RPC_WITH_PASSWORD, RPC_UNPARSEABLE]) {
|
|
const error = await fetch(url).catch((e) => e);
|
|
expect(error.message).toContain("PATHKEY123");
|
|
}
|
|
});
|
|
|
|
test("the RPC check of a URL with a user name and password", async () => {
|
|
loadSettingsView();
|
|
await save("settings-rpc", "btn-save-rpc", RPC_WITH_PASSWORD);
|
|
const line = expectFailedWithoutSecrets("RPC validation fetch failed");
|
|
expect(line).toContain("https://rpc.example.invalid");
|
|
});
|
|
|
|
test("the RPC check of a URL that does not parse", async () => {
|
|
loadSettingsView();
|
|
await save("settings-rpc", "btn-save-rpc", RPC_UNPARSEABLE);
|
|
expectFailedWithoutSecrets("RPC validation fetch failed");
|
|
});
|
|
|
|
test("the Blockscout check of a URL with a user name and password", async () => {
|
|
loadSettingsView();
|
|
await save(
|
|
"settings-blockscout",
|
|
"btn-save-blockscout",
|
|
BLOCKSCOUT_WITH_PASSWORD,
|
|
);
|
|
const line = expectFailedWithoutSecrets("Blockscout validation failed");
|
|
expect(line).toContain("https://explorer.example.invalid");
|
|
});
|
|
|
|
// In the popup a check its own closing cancelled fails as these do, and is
|
|
// not reported (https://git.eeqj.de/sneak/AutistMask/issues/475). Nothing is
|
|
// saved either.
|
|
test("a check that fails once the popup has closed is not reported", async () => {
|
|
const pageClosed = new AbortController();
|
|
pageClosed.abort();
|
|
loadSettingsView(pageClosed);
|
|
await save("settings-rpc", "btn-save-rpc", RPC_UNPARSEABLE);
|
|
await save(
|
|
"settings-blockscout",
|
|
"btn-save-blockscout",
|
|
BLOCKSCOUT_WITH_PASSWORD,
|
|
);
|
|
expect(console.error).not.toHaveBeenCalled();
|
|
expect(state.rpcUrl).toBe(SAVED_RPC);
|
|
expect(state.blockscoutUrl).toBe(SAVED_BLOCKSCOUT);
|
|
});
|