Files
ts-pykeepass-wrapper/tests/unit/keepass.test.ts
T

227 lines
7.7 KiB
TypeScript

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);
});
});