test: expand bridge unit coverage for payloads and errors
This commit is contained in:
+4
-1
@@ -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
@@ -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
@@ -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);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user