Files
ts-pykeepass-wrapper/tests/integration/pykeepass.test.ts
T

190 lines
6.0 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("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",
]);
});