/** * Key register / list / deregister round-trip against a mocked Neo4j * session + mocked crypto + mocked fetch (for key validation against * PropertyData's /national-hpi endpoint). */ import { afterEach, describe, expect, it, vi } from "vitest"; vi.mock("../lib/neo4j.js", () => { let stored: { encryptedKey: string; createdAt: string; updatedAt: string } | null = null; return { getSession: () => ({ run: vi.fn(async (cypher: string, params: Record) => { if (/CREATE INDEX/.test(cypher)) return { records: [] }; if (/MERGE \(k:PropertyDataKey/.test(cypher)) { const now = String(params.now ?? new Date().toISOString()); stored = { encryptedKey: String(params.encryptedKey), createdAt: stored?.createdAt ?? now, updatedAt: now, }; return { records: [] }; } if (/MATCH \(k:PropertyDataKey/.test(cypher) && /DELETE/.test(cypher)) { const count = stored ? 1 : 0; stored = null; return { records: [{ get: (k: string) => (k === "deleted" ? { toNumber: () => count } : null) }], }; } if (/MATCH \(k:PropertyDataKey/.test(cypher) && /RETURN/.test(cypher)) { return { records: stored ? [ { get: (k: string) => { if (k === "encryptedKey") return stored!.encryptedKey; if (k === "createdAt") return stored!.createdAt; if (k === "updatedAt") return stored!.updatedAt; return null; }, }, ] : [], }; } return { records: [] }; }), close: vi.fn(async () => {}), }), closeDriver: vi.fn(async () => {}), }; }); vi.mock("../lib/crypto.js", () => ({ encrypt: (s: string) => `enc:${s}`, decrypt: (s: string) => s.replace(/^enc:/, ""), })); import { keyRegister } from "../tools/key-register.js"; import { keyDeregister } from "../tools/key-deregister.js"; import { keyList } from "../tools/key-list.js"; const originalFetch = globalThis.fetch; afterEach(() => { globalThis.fetch = originalFetch; vi.restoreAllMocks(); }); describe("key round-trip", () => { it("rejects an unregistered account on list", async () => { const out = await keyList({ accountId: "acc-empty" }); expect(out).toMatch(/No PropertyData API key registered/); }); it("rejects deregister when no key is stored", async () => { await expect(keyDeregister({ accountId: "acc-empty-2" })).rejects.toThrow( /No PropertyData API key/, ); }); it("rejects registration when PropertyData returns 401", async () => { globalThis.fetch = vi.fn( async () => new Response("unauthorized", { status: 401 }), ) as unknown as typeof fetch; await expect(keyRegister({ apiKey: "bad-key", accountId: "acc-x" })).rejects.toThrow( /rejected the API key/, ); }); it("registers, lists, then deregisters successfully", async () => { globalThis.fetch = vi.fn( async () => new Response(JSON.stringify({ status: "success", uk_hpi: 100 }), { status: 200 }), ) as unknown as typeof fetch; await keyRegister({ apiKey: "good-key-12345", accountId: "acc-r" }); const listed = await keyList({ accountId: "acc-r" }); expect(listed).toMatch(/PropertyData API key registered/); expect(listed).not.toContain("good-key-12345"); await keyDeregister({ accountId: "acc-r" }); const afterDelete = await keyList({ accountId: "acc-r" }); expect(afterDelete).toMatch(/No PropertyData API key/); }); });