Files
AutistMask/tests/settingsEndpointCheck.test.js
T
clawbot 8ac2c87c2c
check / check (push) Failing after 2s
e2e / e2e-chrome (push) Failing after 1s
e2e / e2e-firefox (push) Failing after 2s
harden: debug mode logs only a request's origin and JSON-RPC method (closes #410)
With debug mode on, debugFetch logged every request's full URL and body,
so an RPC endpoint with an API key in its path or query string printed
that key to the console on every request. It now logs the HTTP method,
the URL's origin and, for a JSON-RPC body, the method name. The balance
refresh and token lookup log the RPC endpoint by its origin too. Failed
RPC calls print ethers' short message, since its full message for an
HTTP error carries the request URL. A failed endpoint check in settings
prints the endpoint's origin, since fetch's error for a URL with a user
name and password carries the whole URL. The README's DEBUG Mode Policy
says what debug mode logs.

Model: opus-5-5
2026-10-04 21:09:04 +02:00

149 lines
4.7 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());
}
function loadSettingsView() {
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({});
}
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");
});