// WebCryptoKeyStore unit tests under JSDOM. Uses Node 22's WebCrypto // (Ed25519 has been stable since Node 20) and `fake-indexeddb/auto` // for storage. The "simulated reload" case closes the database and // reopens it under the same name to prove the persisted keypair // still signs after the connection round-trips. import "fake-indexeddb/auto"; import { afterEach, beforeEach, describe, expect, test } from "vitest"; import type { IDBPDatabase } from "idb"; import { WebCryptoKeyStore } from "../src/platform/store/webcrypto-keystore"; import { openGalaxyDB, type GalaxyDB } from "../src/platform/store/idb"; let db: IDBPDatabase; let dbName: string; beforeEach(async () => { dbName = `galaxy-ui-test-${crypto.randomUUID()}`; db = await openGalaxyDB(dbName); }); afterEach(async () => { db.close(); indexedDB.deleteDatabase(dbName); }); describe("WebCryptoKeyStore", () => { test("generate produces a 32-byte raw Ed25519 public key", async () => { const ks = new WebCryptoKeyStore(db); const keypair = await ks.generate(); expect(keypair.publicKey).toBeInstanceOf(Uint8Array); expect(keypair.publicKey.length).toBe(32); }); test("generate-then-load returns the same public key and a working signer", async () => { const ks = new WebCryptoKeyStore(db); const fresh = await ks.generate(); const loaded = await ks.load(); expect(loaded).not.toBeNull(); expect(Array.from(loaded!.publicKey)).toEqual(Array.from(fresh.publicKey)); const canonical = new TextEncoder().encode("canonical-bytes"); const sigA = await fresh.sign(canonical); const sigB = await loaded!.sign(canonical); // Ed25519 is deterministic: identical (key, message) ⇒ identical // signature bytes. This proves the loaded handle is the same // signing key as the freshly generated one without ever // touching the private bytes. expect(Array.from(sigA)).toEqual(Array.from(sigB)); }); test("produced signature verifies under a third-party public key import", async () => { const ks = new WebCryptoKeyStore(db); const keypair = await ks.generate(); const canonical = new TextEncoder().encode("verify-me"); const signature = await keypair.sign(canonical); expect(signature.length).toBe(64); const verifyKey = await crypto.subtle.importKey( "raw", keypair.publicKey as BufferSource, { name: "Ed25519" }, false, ["verify"], ); const ok = await crypto.subtle.verify( { name: "Ed25519" }, verifyKey, signature as BufferSource, canonical as BufferSource, ); expect(ok).toBe(true); }); test("survives a simulated page reload", async () => { const ks1 = new WebCryptoKeyStore(db); const generated = await ks1.generate(); const canonical = new TextEncoder().encode("reload-canonical"); const sigBefore = await generated.sign(canonical); db.close(); db = await openGalaxyDB(dbName); const ks2 = new WebCryptoKeyStore(db); const reloaded = await ks2.load(); expect(reloaded).not.toBeNull(); expect(Array.from(reloaded!.publicKey)).toEqual( Array.from(generated.publicKey), ); const sigAfter = await reloaded!.sign(canonical); expect(Array.from(sigAfter)).toEqual(Array.from(sigBefore)); }); test("clear empties the slot", async () => { const ks = new WebCryptoKeyStore(db); await ks.generate(); await ks.clear(); expect(await ks.load()).toBeNull(); }); test("load on a fresh database returns null", async () => { const ks = new WebCryptoKeyStore(db); expect(await ks.load()).toBeNull(); }); test("generate after clear yields a different keypair", async () => { const ks = new WebCryptoKeyStore(db); const first = await ks.generate(); await ks.clear(); const second = await ks.generate(); expect(Array.from(second.publicKey)).not.toEqual( Array.from(first.publicKey), ); }); });