health.test.ts

4.2 KB · raw

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    // sha256 of "secret-Passw0rd!" must not appear either111    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});