// SessionStore unit tests under JSDOM with `fake-indexeddb` and Node // 22's WebCrypto. Each case wires a fresh `SessionStore` against a // per-test IndexedDB name, so persistence behaviour is observable // across cases without bleed and without touching the production // `dbConnection()` cache. import "fake-indexeddb/auto"; import { afterEach, beforeEach, describe, expect, test } from "vitest"; import type { IDBPDatabase } from "idb"; import { type GalaxyDB, openGalaxyDB } from "../src/platform/store/idb"; import { IDBCache } from "../src/platform/store/idb-cache"; import { WebCryptoKeyStore } from "../src/platform/store/webcrypto-keystore"; import type { Store } from "../src/platform/store/index"; import { SessionStore } from "../src/lib/session-store.svelte"; let db: IDBPDatabase; let dbName: string; let store: Store; beforeEach(async () => { dbName = `galaxy-ui-test-${crypto.randomUUID()}`; db = await openGalaxyDB(dbName); store = { keyStore: new WebCryptoKeyStore(db), cache: new IDBCache(db), }; }); afterEach(async () => { db.close(); await new Promise((resolve) => { const req = indexedDB.deleteDatabase(dbName); req.onsuccess = () => resolve(); req.onerror = () => resolve(); req.onblocked = () => resolve(); }); }); function newSessionStore(): SessionStore { const s = new SessionStore(); s.setStoreLoaderForTests(async () => store); return s; } describe("SessionStore.init", () => { test("settles to anonymous when no device-session id is persisted", async () => { const session = newSessionStore(); await session.init(); expect(session.status).toBe("anonymous"); expect(session.deviceSessionId).toBeNull(); expect(session.keypair).not.toBeNull(); expect(session.keypair!.publicKey.length).toBe(32); }); test("settles to authenticated when a device-session id is persisted", async () => { const first = newSessionStore(); await first.init(); await first.signIn("dev-1"); expect(first.status).toBe("authenticated"); // Simulate a fresh page load: a new SessionStore against the // same IndexedDB picks up the previously persisted session. const second = newSessionStore(); await second.init(); expect(second.status).toBe("authenticated"); expect(second.deviceSessionId).toBe("dev-1"); expect(second.keypair).not.toBeNull(); }); test("flips status to unsupported when WebCrypto Ed25519 is missing", async () => { const session = newSessionStore(); session.setSupportProbeForTests(async () => false); await session.init(); expect(session.status).toBe("unsupported"); expect(session.keypair).toBeNull(); }); test("init is idempotent", async () => { const session = newSessionStore(); await Promise.all([session.init(), session.init(), session.init()]); expect(session.status).toBe("anonymous"); }); }); describe("SessionStore.signIn / signOut", () => { test("signIn persists the device-session id and updates status", async () => { const session = newSessionStore(); await session.init(); await session.signIn("dev-2"); expect(session.deviceSessionId).toBe("dev-2"); expect(session.status).toBe("authenticated"); const reload = newSessionStore(); await reload.init(); expect(reload.deviceSessionId).toBe("dev-2"); }); test("signOut('user') wipes id, regenerates keypair, returns to anonymous", async () => { const session = newSessionStore(); await session.init(); const firstPublicKey = Array.from(session.keypair!.publicKey); await session.signIn("dev-3"); await session.signOut("user"); expect(session.deviceSessionId).toBeNull(); expect(session.status).toBe("anonymous"); expect(session.keypair).not.toBeNull(); const secondPublicKey = Array.from(session.keypair!.publicKey); expect(secondPublicKey).not.toEqual(firstPublicKey); }); test("signOut('revoked') has the same observable post-state as 'user'", async () => { const session = newSessionStore(); await session.init(); await session.signIn("dev-4"); await session.signOut("revoked"); expect(session.status).toBe("anonymous"); expect(session.deviceSessionId).toBeNull(); }); test("signIn before init throws", async () => { const local = new SessionStore(); await expect(local.signIn("x")).rejects.toThrow(/before init/); }); test("signOut before init throws", async () => { const local = new SessionStore(); await expect(local.signOut("user")).rejects.toThrow(/before init/); }); });