Files
ts-pykeepass-wrapper/tests/integration/pykeepass.test.ts
T
matmoul 5fa30414d7 fix: normalize bridge errors and support nested group paths
Distinguish invalid KeePass requests from backend failures in the Python bridge, improve nested group path resolution, and add coverage for nested group creation plus payload forwarding.
2026-05-10 00:56:58 +02:00

212 lines
6.7 KiB
TypeScript

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<T>(filePath: string, fn: (tempPath: string) => Promise<T>): Promise<T> {
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<boolean> {
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<FixtureEntry & { groupPath: string }> {
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",
]);
});