The transaction lists and ENS name lookups on the address and token screens, the address scan after a wallet is created, the endpoint checks in Settings, the wait screen's receipt check, the Send screen's Max fee estimate and the token lookup on both add-token screens now check the signal the popup aborts on pagehide before reporting a failed request. scanForAddresses(), resolveEnsNames() and lookupTokenInfo() take the signal. End-to-end tests reload the popup on the address screen and during the address scan with their requests held. Jest tests show each of these reports a real failure and stays silent once the popup has closed. The transaction detail and confirmation screens are left out: they discard the popup context that carries the signal. 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);
|
|
});
|