import { describe, expect, mock, test } from "bun:test"; import { KeePassDatabase, openKeePassDatabase } from "../../src/keepass"; const spawnMock = mock(() => { throw new Error("spawn should be mocked per test"); }); mock.module("node:child_process", () => ({ spawn: spawnMock, })); function mockSuccessfulBridgeResponse(data: unknown) { spawnMock.mockImplementation(() => { const child = { stdin: { write: () => undefined, end: () => undefined }, stdout: { on: (_event: string, cb: (chunk: Buffer | string) => void) => cb(JSON.stringify({ ok: true, data })) }, stderr: { on: () => undefined }, on: (event: string, cb: (code?: number | null) => void) => { if (event === "close") queueMicrotask(() => cb(0)); }, }; return child as never; }); } describe("KeePassDatabase", () => { test("listEntries parses successful bridge response", async () => { mockSuccessfulBridgeResponse([{ title: "Entry" }]); const db = new KeePassDatabase("db.kdbx", { password: "secret" }, "python3", new URL("file:///tmp/bridge.py")); const entries = await db.listEntries(); expect(entries).toEqual([{ title: "Entry" }]); expect(spawnMock).toHaveBeenCalled(); }); test("throws on bridge error payload", async () => { spawnMock.mockImplementation(() => { const child = { stdin: { write: () => undefined, end: () => undefined }, stdout: { on: (_event: string, cb: (chunk: Buffer | string) => void) => cb('{"ok":false,"error":"boom"}') }, stderr: { on: () => undefined }, on: (event: string, cb: (code?: number | null) => void) => { if (event === "close") queueMicrotask(() => cb(1)); }, }; return child as never; }); const db = new KeePassDatabase("db.kdbx", { password: "secret" }, "python3", new URL("file:///tmp/bridge.py")); await expect(db.listEntries()).rejects.toThrow("boom"); }); test("throws on empty bridge output", async () => { spawnMock.mockImplementation(() => { const child = { stdin: { write: () => undefined, end: () => undefined }, stdout: { on: (_event: string, cb: (chunk: Buffer | string) => void) => cb(" ") }, stderr: { on: (_event: string, cb: (chunk: Buffer | string) => void) => cb("bridge failed") }, on: (event: string, cb: (code?: number | null) => void) => { if (event === "close") queueMicrotask(() => cb(1)); }, }; return child as never; }); const db = new KeePassDatabase("db.kdbx", { password: "secret" }, "python3", new URL("file:///tmp/bridge.py")); await expect(db.listEntries()).rejects.toThrow("bridge failed"); }); test("throws on invalid JSON output", async () => { spawnMock.mockImplementation(() => { const child = { stdin: { write: () => undefined, end: () => undefined }, stdout: { on: (_event: string, cb: (chunk: Buffer | string) => void) => cb("not json") }, stderr: { on: () => undefined }, on: (event: string, cb: (code?: number | null) => void) => { if (event === "close") queueMicrotask(() => cb(0)); }, }; return child as never; }); const db = new KeePassDatabase("db.kdbx", { password: "secret" }, "python3", new URL("file:///tmp/bridge.py")); await expect(db.listEntries()).rejects.toThrow("Invalid JSON from Python bridge"); }); test("throws on spawn error", async () => { spawnMock.mockImplementation(() => { const child = { stdin: { write: () => undefined, end: () => undefined }, stdout: { on: () => undefined }, stderr: { on: () => undefined }, on: (event: string, cb: (error: Error) => void) => { if (event === "error") queueMicrotask(() => cb(new Error("spawn failed"))); }, }; return child as never; }); const db = new KeePassDatabase("db.kdbx", { password: "secret" }, "python3", new URL("file:///tmp/bridge.py")); await expect(db.listEntries()).rejects.toThrow("spawn failed"); }); test("createEntry forwards the create-entry command", async () => { mockSuccessfulBridgeResponse({ title: "New" }); const db = new KeePassDatabase("db.kdbx", { password: "secret" }, "python3", new URL("file:///tmp/bridge.py")); const created = await db.createEntry({ title: "New" }); expect(created).toEqual({ title: "New" }); expect(spawnMock).toHaveBeenCalled(); }); test("createGroup forwards the create-group command", async () => { mockSuccessfulBridgeResponse({ name: "Folder", path: "" }); const db = new KeePassDatabase("db.kdbx", { password: "secret" }, "python3", new URL("file:///tmp/bridge.py")); const created = await db.createGroup({ name: "Folder" }); expect(created).toEqual({ name: "Folder", path: "" }); expect(spawnMock).toHaveBeenCalled(); }); test("save forwards the save command", async () => { mockSuccessfulBridgeResponse(null); const db = new KeePassDatabase("db.kdbx", { password: "secret" }, "python3", new URL("file:///tmp/bridge.py")); await expect(db.save()).resolves.toBeUndefined(); expect(spawnMock).toHaveBeenCalled(); }); test("findEntries forwards the query payload", async () => { let payload = ""; spawnMock.mockImplementation(() => { const child = { stdin: { write: (chunk: string) => { payload += chunk; }, end: () => undefined, }, stdout: { on: (_event: string, cb: (chunk: Buffer | string) => void) => cb(JSON.stringify({ ok: true, data: [] })) }, stderr: { on: () => undefined }, on: (event: string, cb: (code?: number | null) => void) => { if (event === "close") queueMicrotask(() => cb(0)); }, }; return child as never; }); const db = new KeePassDatabase("db.kdbx", { password: "secret" }, "python3", new URL("file:///tmp/bridge.py")); await db.findEntries({ title: "abc", username: "u", url: "https://x", groupPath: "Folder" }); expect(JSON.parse(payload)).toMatchObject({ command: "find-entries", path: "db.kdbx", password: "secret", query: { title: "abc", username: "u", url: "https://x", groupPath: "Folder" }, }); }); test("listGroups forwards the list-groups command", async () => { let payload = ""; spawnMock.mockImplementation(() => { const child = { stdin: { write: (chunk: string) => { payload += chunk; }, end: () => undefined, }, stdout: { on: (_event: string, cb: (chunk: Buffer | string) => void) => cb(JSON.stringify({ ok: true, data: [] })) }, stderr: { on: () => undefined }, on: (event: string, cb: (code?: number | null) => void) => { if (event === "close") queueMicrotask(() => cb(0)); }, }; return child as never; }); const db = new KeePassDatabase("db.kdbx", { password: "secret" }, "python3", new URL("file:///tmp/bridge.py")); await db.listGroups(); expect(JSON.parse(payload)).toMatchObject({ command: "list-groups", path: "db.kdbx", password: "secret", }); }); test("passes keyFile in the bridge payload", async () => { let payload = ""; spawnMock.mockImplementation(() => { const child = { stdin: { write: (chunk: string) => { payload += chunk; }, end: () => undefined, }, stdout: { on: (_event: string, cb: (chunk: Buffer | string) => void) => cb(JSON.stringify({ ok: true, data: [] })) }, stderr: { on: () => undefined }, on: (event: string, cb: (code?: number | null) => void) => { if (event === "close") queueMicrotask(() => cb(0)); }, }; return child as never; }); const db = new KeePassDatabase("db.kdbx", { password: "secret", keyFile: "keyfile.key" }, "python3", new URL("file:///tmp/bridge.py")); await db.listEntries(); expect(JSON.parse(payload)).toMatchObject({ path: "db.kdbx", password: "secret", keyFile: "keyfile.key", command: "list-entries", }); }); test("openKeePassDatabase returns a KeePassDatabase instance", () => { const db = openKeePassDatabase("db.kdbx", { password: "secret" }); expect(db).toBeInstanceOf(KeePassDatabase); }); });