import { describe, expect, it } from "vitest"; import { buildAdvancedHealthReport } from "./health"; import type { VaultEntry } from "../shared/types"; const NINETY_ONE_DAYS = 91 * 86_400_000; const ONE_EIGHTY_ONE_DAYS = 181 * 86_400_000; function entry(overrides: Partial): VaultEntry { return { id: overrides.id ?? crypto.randomUUID(), type: "login", title: "Test", username: "user@example.com", password: "Strong-Passw0rd!", url: "https://example.com", createdAt: Date.now() - 10_000, modifiedAt: Date.now() - 10_000, ...overrides, }; } describe("buildAdvancedHealthReport", () => { it("flags reused passwords across entries", async () => { const password = "Repeated-Passw0rd!"; const a = entry({ id: "a", url: "https://github.com", password }); const b = entry({ id: "b", url: "https://gitlab.com", password }); const c = entry({ id: "c", url: "https://other.com", password: "Different-Passw0rd!" }); const report = await buildAdvancedHealthReport([a, b, c]); expect(report.reusedGroups).toBe(1); const ra = report.remediation.find((r) => r.id === "a"); const rc = report.remediation.find((r) => r.id === "c"); expect(ra?.factors.reused).toBe(true); expect(rc?.factors.reused).toBeFalsy(); }); it("ages passwords using 90-day threshold for sensitive domains", async () => { const sensitive = entry({ id: "s", url: "https://www.gmail.com", passwordChangedAt: Date.now() - NINETY_ONE_DAYS, }); const regular = entry({ id: "r", url: "https://news.example.com", passwordChangedAt: Date.now() - NINETY_ONE_DAYS, }); const report = await buildAdvancedHealthReport([sensitive, regular]); const rs = report.remediation.find((r) => r.id === "s"); const rr = report.remediation.find((r) => r.id === "r"); expect(rs?.factors.aged).toBe(true); expect(rr?.factors.aged).toBeFalsy(); }); it("ages non-sensitive entries past 180 days", async () => { const old = entry({ id: "o", url: "https://news.example.com", passwordChangedAt: Date.now() - ONE_EIGHTY_ONE_DAYS, }); const report = await buildAdvancedHealthReport([old]); expect(report.remediation[0]?.factors.aged).toBe(true); }); it("scores breach + reuse + age higher than weak alone", async () => { const breached = entry({ id: "b", url: "https://bank.example.com", breach: 25, passwordChangedAt: Date.now() - NINETY_ONE_DAYS, }); const breached2 = entry({ id: "b2", url: "https://other.com", password: breached.password, }); const weakOnly = entry({ id: "w", password: "abc12345", url: "https://other2.com" }); const report = await buildAdvancedHealthReport([breached, breached2, weakOnly]); expect(report.remediation[0]?.id).toBe("b"); const top = report.remediation[0]!; expect(top.score).toBeGreaterThan(60); expect(top.factors.breached).toBe(true); expect(top.factors.reused).toBe(true); }); it("ignores deleted and non-login entries", async () => { const deleted = entry({ id: "d", deletedAt: Date.now() }); const note = entry({ id: "n", type: "note", password: undefined }); const report = await buildAdvancedHealthReport([deleted, note]); expect(report.remediation).toHaveLength(0); expect(report.entryCount).toBe(0); }); it("returns 0 vault score for an empty vault", async () => { const report = await buildAdvancedHealthReport([]); expect(report.vaultScore).toBe(0); expect(report.remediation).toHaveLength(0); }); it("never stores or returns the hash itself", async () => { const a = entry({ id: "a", password: "secret-Passw0rd!" }); const report = await buildAdvancedHealthReport([a]); const json = JSON.stringify(report); expect(json.includes("secret-Passw0rd!")).toBe(false); // sha256 of "secret-Passw0rd!" must not appear either const expectedHash = await crypto.subtle.digest( "SHA-256", new TextEncoder().encode("secret-Passw0rd!") ); const hex = Array.from(new Uint8Array(expectedHash)) .map((b) => b.toString(16).padStart(2, "0")) .join(""); expect(json.includes(hex)).toBe(false); }); });