test: expand bridge unit coverage for payloads and errors

This commit is contained in:
2026-05-10 00:29:48 +02:00
parent da0b396bf8
commit 2d444e9a8b
3 changed files with 153 additions and 28 deletions
+4 -1
View File
@@ -9,6 +9,7 @@ Provide a TypeScript wrapper around KeePass `.kdbx` databases using a Python bri
- TypeScript spawns a Python process per request; there is no persistent worker yet. - TypeScript spawns a Python process per request; there is no persistent worker yet.
- JSON is exchanged over stdin/stdout. - JSON is exchanged over stdin/stdout.
- Bridge errors, empty output, invalid JSON, missing files, and backend exceptions are surfaced as TypeScript errors. - Bridge errors, empty output, invalid JSON, missing files, and backend exceptions are surfaced as TypeScript errors.
- Coverage now includes `keyFile` payload propagation and core API smoke checks.
## Public API ## Public API
- `openKeePassDatabase(path, options)` - `openKeePassDatabase(path, options)`
@@ -19,6 +20,7 @@ Provide a TypeScript wrapper around KeePass `.kdbx` databases using a Python bri
- `KeePassDatabase.createGroup(group)` - `KeePassDatabase.createGroup(group)`
- `KeePassDatabase.save()` - `KeePassDatabase.save()`
- `KeePassDatabase.close()` is a no-op. - `KeePassDatabase.close()` is a no-op.
## Types ## Types
- Entries expose: `title`, `username`, `password`, `url`, `notes`, optional `groupPath`, optional `otp`. - Entries expose: `title`, `username`, `password`, `url`, `notes`, optional `groupPath`, optional `otp`.
- Groups expose: `name`, `path`. - Groups expose: `name`, `path`.
@@ -34,9 +36,10 @@ Provide a TypeScript wrapper around KeePass `.kdbx` databases using a Python bri
## Fixtures and tests ## Fixtures and tests
- Bundled fixtures: `tests/fixtures/data.kdbx` and `tests/fixtures/empty.kdbx`. - Bundled fixtures: `tests/fixtures/data.kdbx` and `tests/fixtures/empty.kdbx`.
- Companion JSON fixture: `tests/fixtures/data.kdbx.json` stores the password and expected content. - Companion JSON fixture: `tests/fixtures/data.kdbx.json` stores the password and expected content.
- Unit tests in `tests/unit/` mock the child process and validate bridge parsing/error handling. - Unit tests in `tests/unit/` mock the child process and validate bridge parsing, error handling, command forwarding, and payload shaping.
- Integration tests in `tests/integration/` use `data.kdbx` to verify entries, groups, partial search, OTP/TOTP output, and basic write persistence on a temporary copy when `pykeepass` is installed. - Integration tests in `tests/integration/` use `data.kdbx` to verify entries, groups, partial search, OTP/TOTP output, and basic write persistence on a temporary copy when `pykeepass` is installed.
- The integration test runner checks for `pykeepass` and skips cleanly when it is unavailable. - The integration test runner checks for `pykeepass` and skips cleanly when it is unavailable.
- Memory tracking files: `.memory/state.md` and `.memory/todo.md`.
## Main scripts ## Main scripts
- `bun run test` / `bun test` - `bun run test` / `bun test`
+3 -26
View File
@@ -1,28 +1,5 @@
# State # State
## Current focus - Added failure-path, command-forwarding, and keyFile payload unit tests for the bridge.
Read-only TypeScript wrapper around KeePass `.kdbx` databases through a Python JSON bridge. - Latest unit and full test runs passed.
- Current focus remains the TypeScript wrapper + Python bridge for KeePass.
## Current API
- `openKeePassDatabase(path, options)`
- `listEntries()`
- `findEntries(query)`
- `listGroups()`
- `close()` is a no-op
## Runtime model
- TypeScript spawns Python per request
- Python uses `pykeepass`
- JSON is exchanged over stdin/stdout
- Errors from the bridge, missing files, invalid JSON, and backend exceptions are surfaced to TypeScript
- Python defaults to `.venv/bin/python3`, overridable with `PYTHON_PATH`
## Current fixture/test status
- Bundled fixtures: `tests/fixtures/data.kdbx` and `tests/fixtures/empty.kdbx`
- `tests/fixtures/data.kdbx.json` stores the password and expected tree/content
- Unit tests mock `node:child_process.spawn`
- Integration tests validate entries, groups, partial search, and OTP/TOTP output for `data.kdbx`
- Integration tests skip when `pykeepass` is unavailable
## Next step
Keep tightening failure-path coverage and keep the API minimal unless a concrete need appears.
+146 -1
View File
@@ -1,5 +1,5 @@
import { describe, expect, mock, test } from "bun:test"; import { describe, expect, mock, test } from "bun:test";
import { KeePassDatabase } from "../../src/keepass"; import { KeePassDatabase, openKeePassDatabase } from "../../src/keepass";
const spawnMock = mock(() => { const spawnMock = mock(() => {
throw new Error("spawn should be mocked per test"); throw new Error("spawn should be mocked per test");
@@ -51,6 +51,57 @@ describe("KeePassDatabase", () => {
await expect(db.listEntries()).rejects.toThrow("boom"); 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 () => { test("createEntry forwards the create-entry command", async () => {
mockSuccessfulBridgeResponse({ title: "New" }); mockSuccessfulBridgeResponse({ title: "New" });
@@ -78,4 +129,98 @@ describe("KeePassDatabase", () => {
await expect(db.save()).resolves.toBeUndefined(); await expect(db.save()).resolves.toBeUndefined();
expect(spawnMock).toHaveBeenCalled(); 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);
});
}); });