Initial commit
This commit is contained in:
@@ -0,0 +1,15 @@
|
||||
# Tests
|
||||
|
||||
This directory is organized as follows:
|
||||
|
||||
- `tests/unit/`: fast unit tests with mocks
|
||||
- `tests/integration/`: optional integration tests that may require `pykeepass` and a Python environment
|
||||
- `tests/fixtures/`: bundled KeePass databases and matching JSON credentials/content files
|
||||
|
||||
## Conventions
|
||||
- Use `*.test.ts` filenames.
|
||||
- Keep unit tests isolated and fast.
|
||||
- Prefer mocking the Python bridge in unit tests.
|
||||
- Put environment-dependent checks in `tests/integration/`.
|
||||
- Keep fixture-driven expectations aligned with the matching `*.kdbx.json` file.
|
||||
- Integration tests should skip or self-report when prerequisites are missing.
|
||||
Vendored
BIN
Binary file not shown.
Vendored
+44
@@ -0,0 +1,44 @@
|
||||
{
|
||||
"password": "123",
|
||||
"content": {
|
||||
"root": {
|
||||
"folders": [
|
||||
{
|
||||
"name": "Folder1",
|
||||
"folders": [],
|
||||
"entries": [
|
||||
{
|
||||
"title": "f1-item1",
|
||||
"user": "f1-item1",
|
||||
"password": "123"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Folder2",
|
||||
"folders": [],
|
||||
"entries": [
|
||||
{
|
||||
"title": "f2-item1",
|
||||
"user": "f2-item1",
|
||||
"password": "123"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"entries": [
|
||||
{
|
||||
"title": "root",
|
||||
"user": "root",
|
||||
"password": "123"
|
||||
},
|
||||
{
|
||||
"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"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
Vendored
BIN
Binary file not shown.
Vendored
+3
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"password": "123"
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
# Integration Tests
|
||||
|
||||
This directory is reserved for tests that require external dependencies or a Python environment with `pykeepass` installed.
|
||||
|
||||
## Conventions
|
||||
- Use `*.test.ts` filenames.
|
||||
- Keep tests here isolated from unit tests.
|
||||
- Prefer explicit setup/skip logic when runtime dependencies are missing.
|
||||
- Integration tests should verify the real KeePass bridge when `pykeepass` and fixture credentials are available.
|
||||
- Prefer fixtures in `tests/fixtures/` and keep expectations aligned with the companion JSON file.
|
||||
@@ -0,0 +1,148 @@
|
||||
import { expect, test } from "bun:test";
|
||||
import { readFile } from "node:fs/promises";
|
||||
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 ensurePyKeePass(): Promise<boolean> {
|
||||
const child = Bun.spawn(["python3", "-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("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",
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
import { describe, expect, mock, test } from "bun:test";
|
||||
import { KeePassDatabase } from "../../src/keepass";
|
||||
|
||||
const spawnMock = mock(() => {
|
||||
throw new Error("spawn should be mocked per test");
|
||||
});
|
||||
|
||||
mock.module("node:child_process", () => ({
|
||||
spawn: spawnMock,
|
||||
}));
|
||||
|
||||
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;
|
||||
});
|
||||
|
||||
const db = new KeePassDatabase("db.kdbx", { password: "secret" }, "python3", new URL("file:///tmp/bridge.py"));
|
||||
const entries = await db.listEntries();
|
||||
|
||||
expect(entries).toEqual([{ title: "Entry" }]);
|
||||
expect(spawnMock).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("throws on bridge error payload", async () => {
|
||||
spawnMock.mockImplementation(() => {
|
||||
const child = {
|
||||
stdin: { write: () => undefined, end: () => undefined },
|
||||
stdout: { on: (_event: string, cb: (chunk: Buffer | string) => void) => cb('{"ok":false,"error":"boom"}') },
|
||||
stderr: { on: () => undefined },
|
||||
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("boom");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user