feat: vendor and censor the phishing blocklist at build time (closes #219)
All checks were successful
check / check (push) Successful in 27s
e2e / e2e-chrome (push) Successful in 48s
e2e / e2e-firefox (push) Successful in 21s

This commit was merged in pull request #301.
This commit is contained in:
2026-08-17 10:05:56 +02:00
parent 8fcdd8a053
commit ff3387d8cf
25 changed files with 1354 additions and 232497 deletions

View File

@@ -80,17 +80,12 @@ describe("alarms module", () => {
delete global.chrome;
});
test("ensureRecurringAlarms schedules both recurring jobs", async () => {
test("ensureRecurringAlarms schedules the recurring job", async () => {
const created = await alarmsMod.ensureRecurringAlarms();
expect(created).toEqual({ balance: true, phishing: true });
expect(created).toEqual({ balance: true, cleared: [] });
const names = alarmsStub.created.map((c) => c.name).sort();
expect(names).toEqual(
[
alarmsMod.BALANCE_REFRESH_ALARM,
alarmsMod.PHISHING_REFRESH_ALARM,
].sort(),
);
const names = alarmsStub.created.map((c) => c.name);
expect(names).toEqual([alarmsMod.BALANCE_REFRESH_ALARM]);
});
test("the balance refresh keeps its 60-second cadence", async () => {
@@ -99,12 +94,35 @@ describe("alarms module", () => {
expect(balance.periodInMinutes).toBe(1);
});
test("the phishing refresh keeps its 24-hour cadence", async () => {
test("a retired job's alarm is cleared, not left running", async () => {
// The browser holds an alarm until something clears it. Deleting the
// job from the code is not enough: on every install that ever ran the
// version which created it, the alarm goes on waking the service
// worker on its old schedule with nothing to deliver it to.
for (const name of alarmsMod.OBSOLETE_ALARMS) {
alarmsStub.create(name, { periodInMinutes: 24 * 60 });
}
expect(alarmsMod.OBSOLETE_ALARMS.length).toBeGreaterThan(0);
const result = await alarmsMod.ensureRecurringAlarms();
expect(result.cleared).toEqual(alarmsMod.OBSOLETE_ALARMS);
for (const name of alarmsMod.OBSOLETE_ALARMS) {
expect(alarmsStub.alarms.get(name)).toBeUndefined();
}
});
test("clearing a retired alarm is not re-reported once it is gone", async () => {
await alarmsMod.ensureRecurringAlarms();
const phishing = alarmsStub.alarms.get(
alarmsMod.PHISHING_REFRESH_ALARM,
const again = await alarmsMod.ensureRecurringAlarms();
expect(again.cleared).toEqual([]);
});
test("no retired name is also a live one", async () => {
// A name in both lists would be created and then cleared on every
// start, so the job it schedules would never fire.
expect(alarmsMod.OBSOLETE_ALARMS).not.toContain(
alarmsMod.BALANCE_REFRESH_ALARM,
);
expect(phishing.periodInMinutes).toBe(24 * 60);
});
test("no period is below the browser-enforced minimum", async () => {
@@ -122,14 +140,14 @@ describe("alarms module", () => {
test("a revived worker does not reset an existing alarm's schedule", async () => {
await alarmsMod.ensureRecurringAlarms();
expect(alarmsStub.create).toHaveBeenCalledTimes(2);
expect(alarmsStub.create).toHaveBeenCalledTimes(1);
// Every wake re-runs the startup path. Re-creating an alarm restarts
// its period, so a busy extension would push the next fire out
// forever and the job would never run.
const again = await alarmsMod.ensureRecurringAlarms();
expect(again).toEqual({ balance: false, phishing: false });
expect(alarmsStub.create).toHaveBeenCalledTimes(2);
expect(again).toEqual({ balance: false, cleared: [] });
expect(alarmsStub.create).toHaveBeenCalledTimes(1);
});
test("a missing alarm is re-created on the next start", async () => {
@@ -137,7 +155,7 @@ describe("alarms module", () => {
await alarmsStub.clear(alarmsMod.BALANCE_REFRESH_ALARM);
const again = await alarmsMod.ensureRecurringAlarms();
expect(again).toEqual({ balance: true, phishing: false });
expect(again).toEqual({ balance: true, cleared: [] });
expect(
alarmsStub.alarms.get(alarmsMod.BALANCE_REFRESH_ALARM),
).toBeDefined();
@@ -147,17 +165,17 @@ describe("alarms module", () => {
// An install carries its alarms across an extension update, so a
// period changed in a new release only ever reaches users if the
// stale one is reconciled.
alarmsStub.create(alarmsMod.PHISHING_REFRESH_ALARM, {
alarmsStub.create(alarmsMod.BALANCE_REFRESH_ALARM, {
periodInMinutes: 7 * 24 * 60,
});
alarmsStub.create.mockClear();
const created = await alarmsMod.ensureRecurringAlarms();
expect(created.phishing).toBe(true);
expect(created.balance).toBe(true);
expect(
alarmsStub.alarms.get(alarmsMod.PHISHING_REFRESH_ALARM)
alarmsStub.alarms.get(alarmsMod.BALANCE_REFRESH_ALARM)
.periodInMinutes,
).toBe(alarmsMod.PHISHING_REFRESH_PERIOD_MINUTES);
).toBe(alarmsMod.BALANCE_REFRESH_PERIOD_MINUTES);
});
test("reconciling a period settles instead of re-creating forever", async () => {
@@ -168,31 +186,31 @@ describe("alarms module", () => {
alarmsStub.create.mockClear();
const again = await alarmsMod.ensureRecurringAlarms();
expect(again).toEqual({ balance: false, phishing: false });
expect(again).toEqual({ balance: false, cleared: [] });
expect(alarmsStub.create).not.toHaveBeenCalled();
});
test("handlers are dispatched by alarm name from one listener", () => {
const balance = jest.fn();
const phishing = jest.fn();
const other = jest.fn();
expect(
alarmsMod.registerAlarmHandlers({
[alarmsMod.BALANCE_REFRESH_ALARM]: balance,
[alarmsMod.PHISHING_REFRESH_ALARM]: phishing,
"autistmask-some-other-job": other,
}),
).toBe(true);
expect(alarmsStub.listenerCount()).toBe(1);
alarmsStub.fire(alarmsMod.BALANCE_REFRESH_ALARM);
expect(balance).toHaveBeenCalledTimes(1);
expect(phishing).not.toHaveBeenCalled();
expect(other).not.toHaveBeenCalled();
alarmsStub.fire(alarmsMod.PHISHING_REFRESH_ALARM);
expect(phishing).toHaveBeenCalledTimes(1);
alarmsStub.fire("autistmask-some-other-job");
expect(other).toHaveBeenCalledTimes(1);
alarmsStub.fire("some-other-extension-alarm");
alarmsStub.fire("an-alarm-with-no-handler");
expect(balance).toHaveBeenCalledTimes(1);
expect(phishing).toHaveBeenCalledTimes(1);
expect(other).toHaveBeenCalledTimes(1);
});
test("Firefox MV2 gets the same treatment via browser.alarms", async () => {
@@ -205,8 +223,8 @@ describe("alarms module", () => {
try {
const mod = require("../src/shared/alarms");
const created = await mod.ensureRecurringAlarms();
expect(created).toEqual({ balance: true, phishing: true });
expect(firefoxAlarms.created).toHaveLength(2);
expect(created).toEqual({ balance: true, cleared: [] });
expect(firefoxAlarms.created).toHaveLength(1);
// The Chrome stub must not have been touched.
expect(alarmsStub.create).not.toHaveBeenCalled();
} finally {
@@ -220,7 +238,7 @@ describe("alarms module", () => {
const mod = require("../src/shared/alarms");
await expect(mod.ensureRecurringAlarms()).resolves.toEqual({
balance: false,
phishing: false,
cleared: [],
});
expect(mod.registerAlarmHandlers({})).toBe(false);
});
@@ -274,9 +292,12 @@ function loadBackground(initialStore = {}) {
tabs: { query: jest.fn(), sendMessage: jest.fn() },
action: { setPopup: jest.fn() },
};
// Present so that a startup path which went to the network would be
// recorded rather than throwing, which is what makes "no request was made"
// an observation instead of an assumption.
global.fetch = jest.fn(async () => ({
ok: true,
json: async () => ({ blacklist: [] }),
json: async () => ({}),
}));
jest.resetModules();
require("../src/background/index");
@@ -318,17 +339,21 @@ describe("background worker scheduling", () => {
// Let the startup path's promises settle.
await settle();
const names = alarmsStub.created.map((c) => c.name).sort();
const {
BALANCE_REFRESH_ALARM,
PHISHING_REFRESH_ALARM,
} = require("../src/shared/alarms");
expect(names).toEqual(
[BALANCE_REFRESH_ALARM, PHISHING_REFRESH_ALARM].sort(),
);
const names = alarmsStub.created.map((c) => c.name);
const { BALANCE_REFRESH_ALARM } = require("../src/shared/alarms");
expect(names).toEqual([BALANCE_REFRESH_ALARM]);
expect(mockSetIntervalCalls).toBe(0);
});
test("startup contacts nothing", async () => {
// The phishing blocklist is vendored at build time and there is no
// other startup fetch, so a worker coming up asks nobody anything.
// Every wake used to be a candidate for a blocklist download.
loadBackground();
await settle();
expect(global.fetch).not.toHaveBeenCalled();
});
test("an onAlarm listener is installed on startup", async () => {
alarmsStub = loadBackground().alarmsStub;
await settle();
@@ -348,7 +373,7 @@ describe("background worker scheduling", () => {
alarmsStub.created.length = 0;
loaded.listeners.onStartup[0]();
await settle();
expect(alarmsStub.created).toHaveLength(2);
expect(alarmsStub.created).toHaveLength(1);
});
test("the install-time listener and the top-level call share one run", async () => {
@@ -360,13 +385,10 @@ describe("background worker scheduling", () => {
loaded.listeners.onInstalled[0]();
await settle();
expect(alarmsStub.created).toHaveLength(2);
expect(alarmsStub.created.map((c) => c.name).sort()).toEqual(
[
"autistmask-balance-refresh",
"autistmask-phishing-refresh",
].sort(),
);
expect(alarmsStub.created).toHaveLength(1);
expect(alarmsStub.created.map((c) => c.name)).toEqual([
"autistmask-balance-refresh",
]);
});
});