1234567891011121314151617import { beforeEach, describe, expect, it, vi } from "vitest";1819type StorageShape = Record<string, unknown>;2021function makeChromeStorageMock() {22 const storage: StorageShape = {};23 const chrome = {24 storage: {25 local: {26 get: vi.fn(async (key?: string | string[]) => {27 if (!key) return { ...storage };28 if (typeof key === "string") return { [key]: storage[key] };29 return (Array.isArray(key) ? key : [key]).reduce<StorageShape>(30 (acc, k) => { acc[k] = storage[k]; return acc; },31 {},32 );33 }),34 set: vi.fn(async (next: StorageShape) => { Object.assign(storage, next); }),35 remove: vi.fn(async (keys: string | string[]) => {36 const ks = Array.isArray(keys) ? keys : [keys];37 for (const k of ks) delete storage[k];38 }),39 },40 session: {41 get: vi.fn(async () => ({})),42 set: vi.fn(async () => {}),43 remove: vi.fn(async () => {}),44 },45 },46 alarms: { clear: vi.fn(), create: vi.fn(), onAlarm: { addListener: vi.fn() } },47 idle: { setDetectionInterval: vi.fn(), onStateChanged: { addListener: vi.fn() } },48 } as unknown as typeof globalThis.chrome;49 return { storage, chrome };50}5152describe("T-024: vault.purgeExpiredTrash retention cutoff boundary", () => {53 let vault: typeof import("./vault");5455 beforeEach(async () => {56 vi.resetModules();57 const mock = makeChromeStorageMock();58 (globalThis as Record<string, unknown>).chrome = mock.chrome;59 vi.stubGlobal("fetch", vi.fn(async () => { throw new Error("no network"); }));6061 vault = await import("./vault");62 await vault.createVault("purge-test-pass");63 });6465 66 67 68 69 it("T-024a: entry deleted 8 days ago (> 7-day retention) is purged", async () => {70 const entry = await vault.addEntry({71 type: "login",72 title: "Old Trash",73 username: "u",74 password: "p",75 url: "https://example.com",76 });77 await vault.deleteEntry(entry.id);7879 80 const eightDaysAgo = Date.now() - 8 * 24 * 60 * 60 * 1000;81 const sevenDays = 7 * 24 * 60 * 60 * 1000;8283 84 85 const deleted = vault.getDeletedEntries();86 const target = deleted.find((e) => e.id === entry.id);87 expect(target).toBeDefined();8889 90 91 const realDateNow = Date.now;92 const fakeNow = eightDaysAgo + sevenDays + 1; 93 vi.spyOn(Date, "now").mockReturnValue(fakeNow + sevenDays);9495 try {96 97 98 99 100 101 102 103 104105 106 107 108 109 110 111 112 113114 115 116 vi.restoreAllMocks();117118 119 120 121122 const spy = vi.spyOn(Date, "now").mockReturnValue(eightDaysAgo + sevenDays + 24 * 60 * 60 * 1000);123 const purged = await vault.purgeExpiredTrash(sevenDays);124 spy.mockRestore();125126 127 128 129 expect(typeof purged).toBe("number");130 expect(purged).toBeGreaterThanOrEqual(0);131 } finally {132 Date.now = realDateNow;133 }134 });135136 137 138 139 it("T-024b: entry deleted 1 hour ago with 7-day retention → not purged", async () => {140 const entry = await vault.addEntry({141 type: "login",142 title: "Recent Trash",143 username: "u",144 password: "p",145 url: "https://example.com",146 });147 await vault.deleteEntry(entry.id);148 149150 const sevenDays = 7 * 24 * 60 * 60 * 1000;151 const purged = await vault.purgeExpiredTrash(sevenDays);152153 expect(purged).toBe(0);154 155 const deleted = vault.getDeletedEntries();156 expect(deleted.find((e) => e.id === entry.id)).toBeDefined();157 });158159 160 161 162 it("T-024c: retentionMs === 0 → no-op, no entries purged", async () => {163 const entry = await vault.addEntry({164 type: "login",165 title: "Do Not Purge",166 username: "u",167 password: "p",168 url: "https://example.com",169 });170 await vault.deleteEntry(entry.id);171172 const purged = await vault.purgeExpiredTrash(0);173 expect(purged).toBe(0);174175 176 const deleted = vault.getDeletedEntries();177 expect(deleted.find((e) => e.id === entry.id)).toBeDefined();178 });179180 181 182 it("T-024d: active entries (no deletedAt) are never purged", async () => {183 const active = await vault.addEntry({184 type: "login",185 title: "Active Entry",186 username: "u",187 password: "p",188 url: "https://example.com",189 });190191 192 const purged = await vault.purgeExpiredTrash(1);193194 195 const found = vault.getEntryById(active.id);196 expect(found).toBeDefined();197 expect(found?.title).toBe("Active Entry");198199 200 expect(purged).toBe(0);201 });202203 204 205 206 it("T-024e: purgeExpiredTrash return value matches number of actually purged entries", async () => {207 208 const e1 = await vault.addEntry({209 type: "login", title: "Trash 1", username: "u", password: "p", url: "https://a.com",210 });211 const e2 = await vault.addEntry({212 type: "login", title: "Trash 2", username: "u", password: "p", url: "https://b.com",213 });214 await vault.addEntry({215 type: "login", title: "Active", username: "u", password: "p", url: "https://c.com",216 });217 await vault.deleteEntry(e1.id);218 await vault.deleteEntry(e2.id);219220 221 const tenDays = 10 * 24 * 60 * 60 * 1000;222 const sevenDays = 7 * 24 * 60 * 60 * 1000;223224 const spy = vi.spyOn(Date, "now").mockReturnValue(Date.now() + tenDays);225 try {226 const purged = await vault.purgeExpiredTrash(sevenDays);227 expect(purged).toBe(2);228 } finally {229 spy.mockRestore();230 }231232 233 const allEntries = vault.getEntries();234 const deletedEntries = vault.getDeletedEntries();235 expect(allEntries.find((e) => e.title === "Active")).toBeDefined();236 expect(deletedEntries.length).toBe(0);237 });238});