Files
siftlode/frontend/src/lib/removedFeatures.contract.test.ts
T
peter 94418a087e fix(auth): a tab acts only as the account it was built for, and the review's other findings
A tab names the account it was claimed for on every request, beacon and upload
(X-Siftlode-Expect-Account); current_user and /auth/logout refuse a mismatch with
401 account_changed and keep the session, so a tab left showing the account
signed in before reloads instead of writing to the new one.

/auth/login clears a connect marker left by an abandoned connect, and a link
completes only on a session that still holds. The prefs merge reads the row
under a lock. A tab's sessionStorage records its own owner. A page the account
loses is given up, address bar and remembered page included. The panel layouts
are preferences; the storage contract also refuses a key handed to the Storage
API or to usePref outside its kind. A refused sign-in as someone else and a
failed sign-out say so. The chromium-only e2e specs are in the gate register,
the member walk waits for its requests, and devices.spec checks the save itself.
2026-10-21 01:05:12 +02:00

77 lines
3.6 KiB
TypeScript

import { readdirSync, readFileSync } from "node:fs";
import { describe, expect, it } from "vitest";
// The shared demo account was removed (the owner's decision, 2026-10-04): its user, its whitelist, its
// login entry, and every `is_demo` branch that kept it from quota, YouTube, downloads and the debug
// recorder. A guard left behind is not harmless — it reads as a live rule, and the next reader keeps
// it alive by copying it — so this fails on the word anywhere in the code a feature could live in:
// the backend (app + tests), the frontend (src + e2e) and the repo scripts.
//
// Not scanned: the Alembic migrations (0014 added the account and 0098 removed it, and both stay true
// as history) and the release notes, which record what shipped when. `audit_metrics.py` counts the
// footprint by this same word, and test_migration_0098.py tests the removal itself, so both name it
// on purpose.
const REPO = new URL("../../../", import.meta.url);
const ROOTS = ["backend/app", "backend/tests", "frontend/src", "frontend/e2e", "scripts"];
const SKIP_DIRS = new Set(["node_modules", "__pycache__", "dist", ".speed", "release-notes"]);
const EXTENSIONS = /\.(py|ts|tsx|js|mjs|json|css|md|sh)$/;
const ALLOWED = new Set([
"frontend/src/lib/removedFeatures.contract.test.ts",
"scripts/audit_metrics.py",
"backend/tests/test_migration_0098.py",
]);
// "demo" as a word or an identifier part (`is_demo`, `DemoBanner`, `tryDemo`) — not "demote" (a role
// change) and not "Demokratikus" (a country's Hungarian name in the vocab).
const DEMO = /demo(?!t(e|ion)|kr)/i;
function files(dir: string): string[] {
return readdirSync(new URL(dir, REPO), { withFileTypes: true }).flatMap((e) =>
e.isDirectory()
? SKIP_DIRS.has(e.name)
? []
: files(`${dir}/${e.name}`)
: EXTENSIONS.test(e.name)
? [`${dir}/${e.name}`]
: [],
);
}
describe("removed features stay removed", () => {
// Reads every source file of the repo: ~0.4 s idle, past 5 s on a busy machine's disk and CPU.
it("leaves no trace of the demo account", { timeout: 30_000 }, () => {
const hits = ROOTS.flatMap(files)
.filter((f) => !ALLOWED.has(f))
.flatMap((f) =>
readFileSync(new URL(f, REPO), "utf-8")
.split("\n")
.flatMap((line, i) => (DEMO.test(line) ? [`${f}:${i + 1}: ${line.trim()}`] : [])),
);
expect(hits).toEqual([]);
});
// One account per browser session (2026-10-20): the per-tab account pin, the header and `?account=`
// that carried it, and the account list and switch went. A request naming an account would now
// be ignored by the server, so a pin left behind reads as working while it does nothing. Only the
// claim that clears the old pin keys (storage.ts) and its test may name them. (The tab's
// X-Siftlode-Expect-Account is not a pin: it never picks an account, the server refuses a request
// from a tab showing another one than the cookie's.)
it("leaves no per-tab account pin, header or switch", { timeout: 30_000 }, () => {
const PIN =
/X-Siftlode-Account|activeAccount|nextAccount|[?&]account=|switchAccount|\/api\/me\/(accounts|switch)/i;
const allowed = new Set([
"frontend/src/lib/storage.ts",
"frontend/src/lib/localData.dom.test.ts",
]);
const hits = ["frontend/src", "frontend/e2e"]
.flatMap(files)
.filter((f) => !allowed.has(f) && f !== "frontend/src/lib/removedFeatures.contract.test.ts")
.flatMap((f) =>
readFileSync(new URL(f, REPO), "utf-8")
.split("\n")
.flatMap((line, i) => (PIN.test(line) ? [`${f}:${i + 1}: ${line.trim()}`] : [])),
);
expect(hits).toEqual([]);
});
});