refactor(frontend): one account per browser, its local state under plain keys
The per-tab account pin, the X-Siftlode-Account header and ?account= go: the session cookie alone says who is asking. localStorage keys lose their account suffix; the browser records whose state it holds (siftlode.owner), BootGate hands it to the account the server answered as, and a different account starts from a clean slate (the public /watch keys stay). The first boot moves the account's old per-account copies onto the plain keys and drops other accounts' copies. Until the claim, and after another tab claimed the browser for someone else, the account helpers read and write nothing. The account panel lists no other accounts: "Add another account" becomes "Sign in as someone else" (Google's account chooser; the pick replaces this account, a cancel keeps it), shown where Google sign-in is configured.
This commit is contained in:
@@ -0,0 +1,87 @@
|
||||
// @vitest-environment jsdom
|
||||
//
|
||||
// The notification and hints stores read the account's localStorage when they are imported, which is
|
||||
// before BootGate knows the account, so they read as nobody's; they must read again on the claim. The
|
||||
// notification store used to keep its empty, nobody's history, and the tab's first notice replaced the
|
||||
// account's stored history with it.
|
||||
import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const entry = (id: number, message: string) => ({
|
||||
id,
|
||||
level: "info",
|
||||
message,
|
||||
ts: 1,
|
||||
read: true,
|
||||
dismissed: true,
|
||||
transient: false,
|
||||
repeat: 1,
|
||||
requiresInteraction: false,
|
||||
});
|
||||
const storedMessages = () =>
|
||||
(
|
||||
JSON.parse(localStorage.getItem("siftlode.notifications") ?? "[]") as {
|
||||
message: string;
|
||||
}[]
|
||||
).map((n) => n.message);
|
||||
|
||||
// Warm the transform cache once (see verifyEntry.dom.test.tsx): the first cold import costs ~0.8 s
|
||||
// alone and outran the 5 s case timeout under a full coverage run. Transformed code outlives
|
||||
// vi.resetModules, so each case still gets fresh module instances, only faster.
|
||||
beforeAll(async () => {
|
||||
await import("./notifications");
|
||||
await import("./hints");
|
||||
await import("./storage");
|
||||
}, 60_000);
|
||||
|
||||
// Warm or not, each case still loads and evaluates the modules it imports afresh after its module
|
||||
// reset: tens of ms idle, but the same reload put sibling suites past 5 s when the full suite's
|
||||
// workers share a busy machine.
|
||||
const CASE_BUDGET = 30_000;
|
||||
|
||||
beforeEach(() => {
|
||||
localStorage.clear();
|
||||
sessionStorage.clear();
|
||||
vi.resetModules(); // both stores load at import: each test needs a fresh one
|
||||
});
|
||||
|
||||
describe("stores read at import, before the account is known", { timeout: CASE_BUDGET }, () => {
|
||||
it("keep the account's notification history and settings once it is claimed", async () => {
|
||||
localStorage.setItem("siftlode.owner", "5");
|
||||
localStorage.setItem("siftlode.notifications", JSON.stringify([entry(7, "older")]));
|
||||
localStorage.setItem("siftlode.notifSettings", JSON.stringify({ durationMs: 0 }));
|
||||
const notifications = await import("./notifications");
|
||||
const { claimLocalData } = await import("./storage");
|
||||
|
||||
notifications.notify({ message: "before the claim" }); // e.g. a failed boot probe
|
||||
claimLocalData(5);
|
||||
notifications.notify({ message: "after the claim" });
|
||||
|
||||
expect(storedMessages()).toEqual(["older", "before the claim", "after the claim"]);
|
||||
expect(notifications.getNotifSettings().durationMs).toBe(0);
|
||||
});
|
||||
|
||||
it("read the account's hints choice once it is claimed", async () => {
|
||||
localStorage.setItem("siftlode.owner", "5");
|
||||
localStorage.setItem("siftlode.hints", "0");
|
||||
const hints = await import("./hints");
|
||||
const { claimLocalData } = await import("./storage");
|
||||
|
||||
expect(hints.hintsEnabled()).toBe(true); // nobody's: the default
|
||||
claimLocalData(5);
|
||||
expect(hints.hintsEnabled()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("a browser that held another account's state", { timeout: CASE_BUDGET }, () => {
|
||||
it("starts the new account's notices from an empty history", async () => {
|
||||
localStorage.setItem("siftlode.owner", "4");
|
||||
localStorage.setItem("siftlode.notifications", JSON.stringify([entry(7, "four's")]));
|
||||
const notifications = await import("./notifications");
|
||||
const { claimLocalData } = await import("./storage");
|
||||
|
||||
claimLocalData(5);
|
||||
notifications.notify({ message: "as five" });
|
||||
|
||||
expect(storedMessages()).toEqual(["as five"]);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user