122 lines
3.7 KiB
TypeScript
122 lines
3.7 KiB
TypeScript
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> = {}): 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));
|
|
});
|
|
});
|