import { describe, expect, it } from "@suaveplan/testing/runner"; import { encryptConcealedFields } from "../encryption/field-encryption.js"; import { applyPatch, generateItemId, rowToItem } from "./helpers.js"; import type { VaultItemRow } from "./migrations/migrations.js"; function vaultKey(): Uint8Array { const key = new Uint8Array(32); crypto.getRandomValues(key); return key; } function makeRow(over: Partial = {}): VaultItemRow { const key = vaultKey(); const id = "i1"; return { id, createdAt: 0, updatedAt: 0, version: 1, deleted: false, title: "GitHub", category: "login", fieldsJson: JSON.stringify({ username: "u" }), concealedFieldsJson: JSON.stringify( encryptConcealedFields(key, id, { password: "p" }) ), tagsJson: JSON.stringify(["work"]), favorite: false, ...over, }; } describe("items helpers", () => { it("rowToItem decrypts concealed fields", { timeout: 5000 }, () => { const key = vaultKey(); const row = makeRow({ concealedFieldsJson: JSON.stringify( encryptConcealedFields(key, "i1", { password: "p" }) ), }); const item = rowToItem(row, key); expect(item.title).toBe("GitHub"); expect(item.concealedFields.password).toBe("p"); expect(item.fields.username).toBe("u"); expect(item.tags).toEqual(["work"]); }); it("applyPatch updates each field independently", { timeout: 5000 }, () => { const key = vaultKey(); const row = makeRow({ concealedFieldsJson: JSON.stringify( encryptConcealedFields(key, "i1", { password: "p" }) ), }); const patched = applyPatch( row, { title: "Renamed", favorite: true, fields: { url: "x" }, tags: ["new"], }, key, undefined ); expect(patched.title).toBe("Renamed"); expect(patched.favorite).toBe(true); expect(JSON.parse(patched.fieldsJson)).toEqual({ url: "x" }); expect(JSON.parse(patched.tagsJson)).toEqual(["new"]); }); it("applyPatch re-encrypts concealed fields on update", { timeout: 5000, }, () => { const key = vaultKey(); const row = makeRow({ concealedFieldsJson: JSON.stringify( encryptConcealedFields(key, "i1", { password: "old" }) ), }); const patched = applyPatch( row, { concealedFields: { password: "new" } }, key, undefined ); const recovered = rowToItem(patched, key); expect(recovered.concealedFields.password).toBe("new"); }); it("applyPatch updates category when provided", { timeout: 5000 }, () => { const key = vaultKey(); const row = makeRow(); const patched = applyPatch( row, { category: "secure-note" }, key, undefined ); expect(patched.category).toBe("secure-note"); }); it("generateItemId produces 32-character hex strings", { timeout: 5000, }, () => { const id = generateItemId(undefined); expect(id).toMatch(/^[0-9a-f]{32}$/); }); it("generateItemId uses the injected RNG when provided", { timeout: 5000, }, () => { const rng = { nextUint32: () => 1, nextFloat: () => 0, nextBytes: (len: number) => new Uint8Array(len).fill(0xab), }; const id = generateItemId(rng); expect(id).toBe("ab".repeat(16)); }); });