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 chrome;49 return { storage, chrome };50}515253function makePasskey(opts: {54 rpId: string;55 credentialId: string;56 lastUsedAt?: number;57 createdAt?: number;58}) {59 return {60 rpId: opts.rpId,61 rpName: opts.rpId,62 credentialId: opts.credentialId,63 publicKeyCose: "a401020326",64 privateKeyJwk: { kty: "EC", crv: "P-256", d: "test", x: "test", y: "test" },65 userHandle: "uh",66 userName: opts.rpId + "-user",67 userDisplayName: opts.rpId + " User",68 algorithm: -7 as const,69 signCount: 0,70 aaguid: "AAAAAAAAAAAAAAAAAAAAAA",71 backupEligible: false,72 backupState: false,73 createdAt: opts.createdAt ?? 1000,74 lastUsedAt: opts.lastUsedAt,75 };76}7778describe("T-023: vault.consolidatePasskeyEntries", () => {79 let vault: typeof import("../../background/vault");8081 beforeEach(async () => {82 vi.resetModules();83 const mock = makeChromeStorageMock();84 (globalThis as Record<string, unknown>).chrome = mock.chrome;85 vi.stubGlobal("fetch", vi.fn(async () => { throw new Error("no network"); }));8687 vault = await import("../../background/vault");88 await vault.createVault("consolidate-test-pass");89 });9091 92 93 94 95 96 97 98 it("T-023a: passkey-only entry merges into existing login with matching domain", async () => {99 100 const login = await vault.addEntry({101 type: "login",102 title: "Example Login",103 username: "[email protected]",104 password: "pw123",105 url: "https://example.com/login",106 });107108 109 const passkeyEntry = await vault.addEntry({110 type: "login",111 title: "Example Passkey",112 username: "[email protected]",113 password: "",114 url: "https://example.com",115 passkey: makePasskey({ rpId: "example.com", credentialId: "cred-001" }),116 });117118 await vault.consolidatePasskeyEntries();119120 const entries = vault.getEntries();121 const loginAfter = entries.find((e) => e.id === login.id);122 const passkeyAfter = vault.getEntryById(passkeyEntry.id);123124 125 expect(loginAfter?.passkey).toBeDefined();126 expect(loginAfter?.passkey?.credentialId).toBe("cred-001");127128 129 expect(passkeyAfter).toBeUndefined();130 });131132 133 134 135 136 137 it("T-023b: two passkeys for same rpId → most recently used is kept, other soft-deleted", async () => {138 const olderPasskey = await vault.addEntry({139 type: "login",140 title: "Old Passkey",141 username: "user",142 password: "",143 url: "https://github.com",144 passkey: makePasskey({145 rpId: "github.com",146 credentialId: "cred-old",147 lastUsedAt: 1000,148 createdAt: 1000,149 }),150 });151152 const newerPasskey = await vault.addEntry({153 type: "login",154 title: "New Passkey",155 username: "user",156 password: "",157 url: "https://github.com",158 passkey: makePasskey({159 rpId: "github.com",160 credentialId: "cred-new",161 lastUsedAt: 9999,162 createdAt: 9999,163 }),164 });165166 await vault.consolidatePasskeyEntries();167168 const entries = vault.getEntries();169170 171 const keptEntry = entries.find((e) =>172 e.passkey?.credentialId === "cred-new" && !e.deletedAt,173 );174 expect(keptEntry).toBeDefined();175176 177 const deletedEntry = vault.getEntryById(olderPasskey.id);178 expect(deletedEntry).toBeUndefined();179180 181 void newerPasskey;182 });183184 185 186 187 it("T-023c: single passkey with no matching login → entry is not deleted", async () => {188 const passkey = await vault.addEntry({189 type: "login",190 title: "Solo Passkey",191 username: "user",192 password: "",193 url: "https://uniquesite.example.com",194 passkey: makePasskey({ rpId: "uniquesite.example.com", credentialId: "cred-solo" }),195 });196197 await vault.consolidatePasskeyEntries();198199 const after = vault.getEntryById(passkey.id);200 expect(after).toBeDefined();201 expect(after?.passkey?.credentialId).toBe("cred-solo");202 });203204 205 206 207 it("T-023d: passkey domain mismatch → no merge (exact domain match, not substring)", async () => {208 await vault.addEntry({209 type: "login",210 title: "Google Login",211 username: "user",212 password: "pw",213 url: "https://google.com",214 });215216 const signinPasskey = await vault.addEntry({217 type: "login",218 title: "Google Signin Passkey",219 username: "user",220 password: "",221 url: "https://signin.google.com",222 passkey: makePasskey({ rpId: "signin.google.com", credentialId: "cred-signin" }),223 });224225 await vault.consolidatePasskeyEntries();226227 228 const after = vault.getEntryById(signinPasskey.id);229 expect(after).toBeDefined();230 });231});