import { expect, test } from "bun:test"; 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 = { title: string; user: string; password: string; otp?: string; }; type FixtureFolder = { name: string; folders: FixtureFolder[]; entries: FixtureEntry[]; }; type FixtureData = { password: string; content: { root: FixtureFolder; }; }; const FIXTURE_PATH = "tests/fixtures/data.kdbx"; const FIXTURE_DATA_PATH = "tests/fixtures/data.kdbx.json"; async function withTempCopy(filePath: string, fn: (tempPath: string) => Promise): Promise { const tempPath = join(tmpdir(), `ts-pykeepass-wrapper-${randomUUID()}.kdbx`); await copyFile(filePath, tempPath); try { return await fn(tempPath); } finally { await rm(tempPath, { force: true }); } } async function ensurePyKeePass(): Promise { const python = process.env.PYTHON_PATH ?? ".venv/bin/python3"; const child = Bun.spawn([python, "-c", "import pykeepass; print('ok')"], { stdout: "pipe", stderr: "pipe", }); return child.exited.then((code) => code === 0); } function flattenEntries(folder: FixtureFolder, groupPath = ""): Array { const ownEntries = folder.entries.map((entry) => ({ ...entry, groupPath })); const nestedEntries = folder.folders.flatMap((child) => flattenEntries(child, groupPath ? `${groupPath}/${child.name}` : child.name)); return [...ownEntries, ...nestedEntries]; } function groupTreeToString(folder: FixtureFolder, indent = ""): string[] { const lines = [`${indent}${folder.name || "Racine"}/`]; for (const entry of folder.entries) { lines.push(`${indent} - ${entry.title}`); } for (const child of folder.folders) { lines.push(...groupTreeToString(child, `${indent} `)); } return lines; } test("opens the bundled data fixture and exposes all entries and values", async () => { const [{ password, content }, 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; } const expectedEntries = flattenEntries(content.root); const db = openKeePassDatabase(FIXTURE_PATH, { password }); const entries = await db.listEntries(); expect(entries).toHaveLength(expectedEntries.length); expect(entries.map((entry) => entry.title).sort()).toEqual(expectedEntries.map((entry) => entry.title).sort()); for (const expected of expectedEntries) { const actual = entries.find((entry) => entry.title === expected.title); expect(actual).toBeTruthy(); expect(actual?.username).toBe(expected.user); expect(actual?.password).toBe(expected.password); expect(actual?.notes).toBe(""); expect(actual?.url).toBe(""); expect(actual?.groupPath).toBe(expected.groupPath); if (expected.otp) { expect(actual?.otp).toBe(expected.otp); } } }); test("lists the groups from the bundled data fixture", 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; } const db = openKeePassDatabase(FIXTURE_PATH, { password }); const groups = await db.listGroups(); expect(groups).toEqual([ { name: "Racine", path: "" }, { name: "Folder1", path: "Folder1" }, { name: "Folder2", path: "Folder2" }, ]); }); test("finds entries in the bundled data fixture", 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; } const db = openKeePassDatabase(FIXTURE_PATH, { password }); const entries = await db.findEntries({ title: "f1-item1" }); expect(entries).toHaveLength(1); expect(entries[0]?.title).toBe("f1-item1"); expect(entries[0]?.username).toBe("f1-item1"); expect(entries[0]?.password).toBe("123"); 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("creates nested groups on a temporary copy", 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 createdGroup = await db.createGroup({ name: "Nested", path: "Folder1" }); expect(createdGroup.path).toBe("Folder1/Nested"); const groups = await db.listGroups(); expect(groups.some((group) => group.path === "Folder1/Nested")).toBe(true); }); }); 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; expect(content.root.folders).toHaveLength(2); expect(content.root.entries).toHaveLength(2); expect(flattenEntries(content.root)).toEqual([ { title: "root", user: "root", password: "123", groupPath: "" }, { title: "otp1", user: "otp1", password: "123", otp: "otpauth://totp/otp1:otp1?secret=234324AB34%3D%3D%3D%3D%3D%3D&period=30&digits=6&issuer=otp1", groupPath: "" }, { title: "f1-item1", user: "f1-item1", password: "123", groupPath: "Folder1" }, { title: "f2-item1", user: "f2-item1", password: "123", groupPath: "Folder2" }, ]); expect(groupTreeToString(content.root)).toEqual([ "Racine/", "\t- root", "\t- otp1", "\tFolder1/", "\t\t- f1-item1", "\tFolder2/", "\t\t- f2-item1", ]); });