import { describe, expect, it } from "vitest"; import { utf8ToBytes } from "../../src/crypto/encoding"; import { compareCollectionSnapshots, createCollectionSnapshot, parseCollectionSnapshot, } from "../../src/otp/collection"; import type { OtpProfile } from "../../src/otp/profile"; function profile(account: string, secret: string): OtpProfile { return { kind: "totp", issuer: "Example", account, secret: utf8ToBytes(secret), algorithm: "SHA-1", digits: 6, period: 30, counter: 0n, extensions: new Map(), }; } describe("OTP collection snapshots", () => { it("exports fingerprints without secrets", async () => { const snapshot = await createCollectionSnapshot([ profile("alice", "top-secret-value"), ]); const serialized = JSON.stringify(snapshot); expect(serialized).not.toContain("top-secret-value"); expect(parseCollectionSnapshot(serialized)).toEqual(snapshot); }); it("detects renames, parameter changes and secret reuse", async () => { const before = await createCollectionSnapshot([ profile("alice", "secret-a"), profile("bob", "secret-b"), ]); const bob = profile("bob", "secret-b"); bob.digits = 8; const after = await createCollectionSnapshot([ profile("alice-renamed", "secret-a"), bob, profile("carol", "secret-b"), ]); const result = compareCollectionSnapshots(before, after); expect(result.changes.map(({ kind }) => kind)).toEqual( expect.arrayContaining([ "renamed", "parameters-changed", "added", "secret-reused", ]), ); expect(result.rotationPlan[0]?.kind).toBe("secret-reused"); }); });