1import { describe, expect, it } from "vitest";2import { buildAdvancedHealthReport } from "./health";3import type { VaultEntry } from "../shared/types";45const NINETY_ONE_DAYS = 91 * 86_400_000;6const ONE_EIGHTY_ONE_DAYS = 181 * 86_400_000;78function entry(overrides: Partial<VaultEntry>): VaultEntry {9 return {10 id: overrides.id ?? crypto.randomUUID(),11 type: "login",12 title: "Test",13 username: "[email protected]",14 password: "Strong-Passw0rd!",15 url: "https://example.com",16 createdAt: Date.now() - 10_000,17 modifiedAt: Date.now() - 10_000,18 ...overrides,19 };20}2122describe("buildAdvancedHealthReport", () => {23 it("flags reused passwords across entries", async () => {24 const password = "Repeated-Passw0rd!";25 const a = entry({ id: "a", url: "https://github.com", password });26 const b = entry({ id: "b", url: "https://gitlab.com", password });27 const c = entry({ id: "c", url: "https://other.com", password: "Different-Passw0rd!" });2829 const report = await buildAdvancedHealthReport([a, b, c]);3031 expect(report.reusedGroups).toBe(1);32 const ra = report.remediation.find((r) => r.id === "a");33 const rc = report.remediation.find((r) => r.id === "c");34 expect(ra?.factors.reused).toBe(true);35 expect(rc?.factors.reused).toBeFalsy();36 });3738 it("ages passwords using 90-day threshold for sensitive domains", async () => {39 const sensitive = entry({40 id: "s",41 url: "https://www.gmail.com",42 passwordChangedAt: Date.now() - NINETY_ONE_DAYS,43 });44 const regular = entry({45 id: "r",46 url: "https://news.example.com",47 passwordChangedAt: Date.now() - NINETY_ONE_DAYS,48 });4950 const report = await buildAdvancedHealthReport([sensitive, regular]);5152 const rs = report.remediation.find((r) => r.id === "s");53 const rr = report.remediation.find((r) => r.id === "r");54 expect(rs?.factors.aged).toBe(true);55 expect(rr?.factors.aged).toBeFalsy();56 });5758 it("ages non-sensitive entries past 180 days", async () => {59 const old = entry({60 id: "o",61 url: "https://news.example.com",62 passwordChangedAt: Date.now() - ONE_EIGHTY_ONE_DAYS,63 });64 const report = await buildAdvancedHealthReport([old]);65 expect(report.remediation[0]?.factors.aged).toBe(true);66 });6768 it("scores breach + reuse + age higher than weak alone", async () => {69 const breached = entry({70 id: "b",71 url: "https://bank.example.com",72 breach: 25,73 passwordChangedAt: Date.now() - NINETY_ONE_DAYS,74 });75 const breached2 = entry({76 id: "b2",77 url: "https://other.com",78 password: breached.password,79 });80 const weakOnly = entry({ id: "w", password: "abc12345", url: "https://other2.com" });8182 const report = await buildAdvancedHealthReport([breached, breached2, weakOnly]);8384 expect(report.remediation[0]?.id).toBe("b");85 const top = report.remediation[0]!;86 expect(top.score).toBeGreaterThan(60);87 expect(top.factors.breached).toBe(true);88 expect(top.factors.reused).toBe(true);89 });9091 it("ignores deleted and non-login entries", async () => {92 const deleted = entry({ id: "d", deletedAt: Date.now() });93 const note = entry({ id: "n", type: "note", password: undefined });94 const report = await buildAdvancedHealthReport([deleted, note]);95 expect(report.remediation).toHaveLength(0);96 expect(report.entryCount).toBe(0);97 });9899 it("returns 0 vault score for an empty vault", async () => {100 const report = await buildAdvancedHealthReport([]);101 expect(report.vaultScore).toBe(0);102 expect(report.remediation).toHaveLength(0);103 });104105 it("never stores or returns the hash itself", async () => {106 const a = entry({ id: "a", password: "secret-Passw0rd!" });107 const report = await buildAdvancedHealthReport([a]);108 const json = JSON.stringify(report);109 expect(json.includes("secret-Passw0rd!")).toBe(false);110 111 const expectedHash = await crypto.subtle.digest(112 "SHA-256",113 new TextEncoder().encode("secret-Passw0rd!")114 );115 const hex = Array.from(new Uint8Array(expectedHash))116 .map((b) => b.toString(16).padStart(2, "0"))117 .join("");118 expect(json.includes(hex)).toBe(false);119 });120});