feat: add write support for KeePass entries and groups
This commit is contained in:
@@ -1,5 +1,8 @@
|
||||
import { expect, test } from "bun:test";
|
||||
import { readFile } from "node:fs/promises";
|
||||
import { copyFile, readFile, rm } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { randomUUID } from "node:crypto";
|
||||
import { openKeePassDatabase } from "../../src/keepass";
|
||||
|
||||
type FixtureEntry = {
|
||||
@@ -25,6 +28,16 @@ type FixtureData = {
|
||||
const FIXTURE_PATH = "tests/fixtures/data.kdbx";
|
||||
const FIXTURE_DATA_PATH = "tests/fixtures/data.kdbx.json";
|
||||
|
||||
async function withTempCopy<T>(filePath: string, fn: (tempPath: string) => Promise<T>): Promise<T> {
|
||||
const tempPath = join(tmpdir(), `kdbx-lib-${randomUUID()}.kdbx`);
|
||||
await copyFile(filePath, tempPath);
|
||||
try {
|
||||
return await fn(tempPath);
|
||||
} finally {
|
||||
await rm(tempPath, { force: true });
|
||||
}
|
||||
}
|
||||
|
||||
async function ensurePyKeePass(): Promise<boolean> {
|
||||
const python = process.env.PYTHON_PATH ?? ".venv/bin/python3";
|
||||
const child = Bun.spawn([python, "-c", "import pykeepass; print('ok')"], {
|
||||
@@ -125,6 +138,34 @@ test("finds entries in the bundled data fixture", async () => {
|
||||
expect(entries[0]?.groupPath).toBe("Folder1");
|
||||
});
|
||||
|
||||
test("creates entries in a temporary copy of the bundled fixture and persists them", async () => {
|
||||
const [{ password }, pykeepassReady] = await Promise.all([
|
||||
readFile(FIXTURE_DATA_PATH, "utf8").then((raw) => JSON.parse(raw) as FixtureData),
|
||||
ensurePyKeePass(),
|
||||
]);
|
||||
|
||||
if (!pykeepassReady) {
|
||||
console.log("Skipping integration test: pykeepass is not installed");
|
||||
return;
|
||||
}
|
||||
|
||||
await withTempCopy(FIXTURE_PATH, async (tempPath) => {
|
||||
const db = openKeePassDatabase(tempPath, { password });
|
||||
const createdEntry = await db.createEntry({
|
||||
title: "TempEntry",
|
||||
username: "temp-user",
|
||||
password: "temp-pass",
|
||||
});
|
||||
expect(createdEntry.title).toBe("TempEntry");
|
||||
expect(createdEntry.username).toBe("temp-user");
|
||||
|
||||
const persisted = await db.findEntries({ title: "TempEntry" });
|
||||
expect(persisted).toHaveLength(1);
|
||||
expect(persisted[0]?.username).toBe("temp-user");
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
test("uses the JSON fixture content as the source of truth for expectations", async () => {
|
||||
const { content } = JSON.parse(await readFile(FIXTURE_DATA_PATH, "utf8")) as FixtureData;
|
||||
|
||||
|
||||
+43
-25
@@ -9,33 +9,23 @@ 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 () => {
|
||||
spawnMock.mockImplementation(() => {
|
||||
const listeners: Record<string, ((chunk: Buffer | string) => void)[]> = {};
|
||||
const child = {
|
||||
stdin: {
|
||||
write: () => undefined,
|
||||
end: () => {
|
||||
listeners.close?.forEach((cb) => cb(Buffer.from("")));
|
||||
},
|
||||
},
|
||||
stdout: {
|
||||
on: (event: string, cb: (chunk: Buffer | string) => void) => {
|
||||
listeners[event] ??= [];
|
||||
listeners[event].push(cb);
|
||||
if (event === "data") {
|
||||
cb(JSON.stringify({ ok: true, data: [{ title: "Entry" }] }));
|
||||
}
|
||||
},
|
||||
},
|
||||
stderr: { on: () => undefined },
|
||||
on: (event: string, cb: (code?: number | null) => void) => {
|
||||
if (event === "close") queueMicrotask(() => cb(0));
|
||||
},
|
||||
};
|
||||
return child as never;
|
||||
});
|
||||
mockSuccessfulBridgeResponse([{ title: "Entry" }]);
|
||||
|
||||
const db = new KeePassDatabase("db.kdbx", { password: "secret" }, "python3", new URL("file:///tmp/bridge.py"));
|
||||
const entries = await db.listEntries();
|
||||
@@ -60,4 +50,32 @@ describe("KeePassDatabase", () => {
|
||||
const db = new KeePassDatabase("db.kdbx", { password: "secret" }, "python3", new URL("file:///tmp/bridge.py"));
|
||||
await expect(db.listEntries()).rejects.toThrow("boom");
|
||||
});
|
||||
|
||||
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();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user