Initial Commit
This commit is contained in:
121
packages/security-stack/security/vault/src/items/helpers.test.ts
Normal file
121
packages/security-stack/security/vault/src/items/helpers.test.ts
Normal file
@@ -0,0 +1,121 @@
|
||||
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));
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user