import { beforeEach, describe, expect, it, vi } from "vitest"; import { decrypt } from "./aes"; import { deriveKey } from "../crypto/argon2"; import type { StoredVault } from "../shared/types"; type StorageShape = Record; describe("background vault", () => { let storage: StorageShape; let alarms = { created: [] as Array<{ name: string; delayInMinutes: number }>, cleared: [] as string[] }; beforeEach(() => { vi.resetModules(); storage = {}; alarms = { created: [], cleared: [] }; const chromeMock = { storage: { local: { get: vi.fn(async (key?: string | string[]) => { if (!key) return storage; if (typeof key === "string") { return { [key]: storage[key] }; } return key.reduce>((result, entry) => { result[entry] = storage[entry]; return result; }, {}); }), set: vi.fn(async (next: Record) => { Object.assign(storage, next); }), remove: vi.fn(async (keys: string | string[]) => { const list = Array.isArray(keys) ? keys : [keys]; for (const key of list) { delete storage[key]; } }), }, }, alarms: { clear: vi.fn((name: string) => { alarms.cleared.push(name); }), create: vi.fn((name: string, info: { delayInMinutes: number }) => { alarms.created.push({ name, delayInMinutes: info.delayInMinutes }); }), onAlarm: { addListener: vi.fn() }, }, idle: { setDetectionInterval: vi.fn(), onStateChanged: { addListener: vi.fn() }, }, } as unknown as typeof chrome; (globalThis as { chrome?: typeof chrome }).chrome = chromeMock; // Prevent fire-and-forget breach-check fetches from leaking into other tests. vi.stubGlobal("fetch", vi.fn(async () => { throw new Error("network disabled in tests"); })); }); it("creates a vault and stores encrypted payload", async () => { const vault = await import("./vault"); await vault.createVault("correct horse battery staple"); expect(storage.peach_vault_a).toBeDefined(); expect((storage.peach_vault_a as StoredVault).ciphertext).toBeTruthy(); expect(vault.getEntries()).toEqual([]); expect(alarms.created).toContainEqual({ name: "autolock", delayInMinutes: 5, }); }); it("writes both slots A and B on vault creation", async () => { const vault = await import("./vault"); await vault.createVault("correct horse battery staple"); expect(storage.peach_vault_a).toBeDefined(); expect(storage.peach_vault_b).toBeDefined(); expect(storage.peach_duress).toBeUndefined(); expect(storage.peach_vault).toBeUndefined(); expect(storage.peach_decoy_vault).toBeUndefined(); }); it("slot A and slot B have matching ciphertext size at fresh install", async () => { const vault = await import("./vault"); await vault.createVault("correct horse battery staple"); const a = storage.peach_vault_a as StoredVault; const b = storage.peach_vault_b as StoredVault; expect(a.ciphertext.length).toBe(b.ciphertext.length); }); it("does not expose a plaintext duress flag after setupDuress", async () => { const vault = await import("./vault"); await vault.createVault("real-password-12345"); await vault.setupDuress("decoy-password-67890"); expect(storage.peach_duress).toBeUndefined(); expect(storage.peach_decoy_vault).toBeUndefined(); const keys = Object.keys(storage).filter((k) => k.startsWith("peach_")); // Slot-neutral keys + optional settings + vault history (snapshot of the // pre-setupDuress real-mode blob). No exposed duress flag. for (const key of keys) { expect( [ "peach_vault_a", "peach_vault_b", "peach_settings", "peach_vault_history", "peach_last_modified_at", ] ).toContain(key); } expect(keys).toContain("peach_vault_a"); expect(keys).toContain("peach_vault_b"); }); it("unlocks with real password and loads real entries", async () => { const vault = await import("./vault"); await vault.createVault("real-password-12345"); await vault.addEntry({ title: "GitHub", username: "june", password: "realsecret", url: "github.com", }); vault.lock(); const ok = await vault.unlock("real-password-12345"); expect(ok).toBe(true); expect(vault.getEntries()).toHaveLength(1); expect(vault.getEntries()[0].title).toBe("GitHub"); }); it("unlocks with duress password and loads empty entries", async () => { const vault = await import("./vault"); await vault.createVault("real-password-12345"); await vault.addEntry({ title: "GitHub", username: "june", password: "realsecret", url: "github.com", }); await vault.setupDuress("decoy-password-67890"); vault.lock(); const ok = await vault.unlock("decoy-password-67890"); expect(ok).toBe(true); expect(vault.getEntries()).toEqual([]); }); it("runs both slot derivations in parallel for constant-time unlock (DUR-002)", async () => { // The unlock path must derive keys for both slots unconditionally so a // real-mode unlock and a decoy-mode unlock both spend roughly the same // wall-clock time. Verified here by spying on deriveKey and asserting it // is called twice (once per slot) on every unlock attempt. const argon2 = await import("../crypto/argon2"); const deriveSpy = vi.spyOn(argon2, "deriveKey"); const vault = await import("./vault"); await vault.createVault("real-password-12345"); await vault.setupDuress("decoy-password-67890"); vault.lock(); deriveSpy.mockClear(); await vault.unlock("real-password-12345"); expect(deriveSpy).toHaveBeenCalledTimes(2); vault.lock(); deriveSpy.mockClear(); await vault.unlock("decoy-password-67890"); expect(deriveSpy).toHaveBeenCalledTimes(2); vault.lock(); deriveSpy.mockClear(); await vault.unlock("wrong-password-xxxxx"); expect(deriveSpy).toHaveBeenCalledTimes(2); deriveSpy.mockRestore(); }); it("rejects wrong passwords", async () => { const vault = await import("./vault"); await vault.createVault("real-password-12345"); vault.lock(); const ok = await vault.unlock("nope-nope-nope"); expect(ok).toBe(false); }); it("persists duressConfigured across lock/unlock in encrypted payload", async () => { const vault = await import("./vault"); await vault.createVault("real-password-12345"); await vault.setupDuress("decoy-password-67890"); vault.lock(); await vault.unlock("real-password-12345"); expect(await vault.isDuressEnabled()).toBe(true); await vault.disableDuress(); expect(await vault.isDuressEnabled()).toBe(false); vault.lock(); await vault.unlock("real-password-12345"); expect(await vault.isDuressEnabled()).toBe(false); }); it("migrates legacy peach_vault / peach_decoy_vault / peach_duress schema", async () => { // Arrange: set up storage as if from pre-A-2 (v1.4.24 and earlier). const firstVault = await import("./vault"); await firstVault.createVault("legacy-password-abcde"); const legacySlotA = storage.peach_vault_a as StoredVault; storage.peach_vault = legacySlotA; delete storage.peach_vault_a; delete storage.peach_vault_b; storage.peach_duress = { enabled: true }; storage.peach_decoy_vault = legacySlotA; // reuse shape for test vi.resetModules(); // Act: fresh module load triggers migration on next vault access. const vault = await import("./vault"); await vault.hasVault(); // Assert: legacy keys removed, new keys present. expect(storage.peach_vault).toBeUndefined(); expect(storage.peach_decoy_vault).toBeUndefined(); expect(storage.peach_duress).toBeUndefined(); expect(storage.peach_vault_a).toBeDefined(); expect(storage.peach_vault_b).toBeDefined(); }); it("soft deletes entries while keeping deletedAt in encrypted storage", async () => { const vault = await import("./vault"); await vault.createVault("correct horse battery staple"); const entry = await vault.addEntry({ title: "GitHub", username: "june", password: "secret123", url: "github.com", }); await vault.deleteEntry(entry.id); expect(vault.getEntries()).toEqual([]); const stored = storage.peach_vault_a as StoredVault; const salt = Uint8Array.from( stored.salt.match(/.{1,2}/g)!.map((value: string) => parseInt(value, 16)) ); const key = await deriveKey("correct horse battery staple", salt); const plain = await decrypt(key, stored.nonce, stored.ciphertext, stored.tag, "slot_a_v1"); const parsed = JSON.parse(plain) as { entries: Array<{ id: string; deletedAt?: number }> }; expect(parsed.entries).toHaveLength(1); expect(parsed.entries[0].id).toBe(entry.id); expect(typeof parsed.entries[0].deletedAt).toBe("number"); }); it("accepts imported entries without a username or url", async () => { const vault = await import("./vault"); await vault.createVault("correct horse battery staple"); const entry = await vault.addEntry({ title: "Imported Login", username: "", password: "secret123", url: "", notes: "migrated from another manager", }); expect(entry.username).toBe(""); expect(entry.url).toBe(""); expect(vault.getEntries()).toHaveLength(1); }); it("changePassword happy path — new password unlocks, old is rejected", async () => { const vault = await import("./vault"); await vault.createVault("old-pass-abc123"); await vault.addEntry({ title: "GitHub", username: "june", password: "secret1", url: "github.com" }); await vault.addEntry({ title: "Gmail", username: "june", password: "secret2", url: "mail.google.com" }); expect(await vault.changePassword("old-pass-abc123", "new-pass-xyz789")).toBe(true); vault.lock(); expect(await vault.unlock("new-pass-xyz789")).toBe(true); expect(vault.getEntries()).toHaveLength(2); vault.lock(); expect(await vault.unlock("old-pass-abc123")).toBe(false); }); it("stamps peach_last_modified_at on createVault", async () => { const vault = await import("./vault"); const before = Date.now(); await new Promise((resolve) => setTimeout(resolve, 1)); await vault.createVault("stamp-create-pass"); expect(Number(storage.peach_last_modified_at)).toBeGreaterThanOrEqual(before); }); it("stamps peach_last_modified_at on entry mutations through persistEntries", async () => { const vault = await import("./vault"); await vault.createVault("stamp-entry-pass"); let previous = Number(storage.peach_last_modified_at); await new Promise((resolve) => setTimeout(resolve, 1)); const entry = await vault.addEntry({ title: "Stamp", username: "", password: "p", url: "", }); expect(Number(storage.peach_last_modified_at)).toBeGreaterThan(previous); previous = Number(storage.peach_last_modified_at); await new Promise((resolve) => setTimeout(resolve, 1)); await vault.updateEntry(entry.id, { title: "Stamped" }); expect(Number(storage.peach_last_modified_at)).toBeGreaterThan(previous); previous = Number(storage.peach_last_modified_at); await new Promise((resolve) => setTimeout(resolve, 1)); await vault.deleteEntry(entry.id); expect(Number(storage.peach_last_modified_at)).toBeGreaterThan(previous); }); it("stamps peach_last_modified_at on setupDuress and changePassword", async () => { const vault = await import("./vault"); await vault.createVault("stamp-real-pass"); let previous = Number(storage.peach_last_modified_at); await new Promise((resolve) => setTimeout(resolve, 1)); await vault.setupDuress("stamp-decoy-pass"); expect(Number(storage.peach_last_modified_at)).toBeGreaterThan(previous); previous = Number(storage.peach_last_modified_at); await new Promise((resolve) => setTimeout(resolve, 1)); expect(await vault.changePassword("stamp-real-pass", "stamp-new-pass")).toBe(true); expect(Number(storage.peach_last_modified_at)).toBeGreaterThan(previous); }); it("changePassword with wrong current password returns false and leaves slot A unchanged", async () => { const vault = await import("./vault"); await vault.createVault("correct-pass-abcde"); const slotABefore = JSON.stringify(storage.peach_vault_a); expect(await vault.changePassword("wrong-pass", "new-pass")).toBe(false); expect(JSON.stringify(storage.peach_vault_a)).toBe(slotABefore); vault.lock(); expect(await vault.unlock("correct-pass-abcde")).toBe(true); }); it("changePassword with duress configured leaves slot B byte-identical", async () => { const vault = await import("./vault"); await vault.createVault("real-pass-abcde"); await vault.setupDuress("duress-pass-xyzzz"); const slotBBefore = JSON.stringify(storage.peach_vault_b); expect(await vault.changePassword("real-pass-abcde", "new-real-pass-fghij")).toBe(true); expect(JSON.stringify(storage.peach_vault_b)).toBe(slotBBefore); // Duress path still works with the old decoy password. vault.lock(); expect(await vault.unlock("duress-pass-xyzzz")).toBe(true); expect(vault.getEntries()).toEqual([]); }); it("restoreEntries with mode 'replace' and empty incoming throws and leaves entries intact", async () => { const vault = await import("./vault"); await vault.createVault("pass-abcdef"); await vault.addEntry({ title: "One", username: "", password: "1", url: "" }); await vault.addEntry({ title: "Two", username: "", password: "2", url: "" }); await vault.addEntry({ title: "Three", username: "", password: "3", url: "" }); await expect(vault.restoreEntries([], "replace")).rejects.toThrow( /refuse to replace with empty entry set/i ); expect(vault.getEntries()).toHaveLength(3); }); it("changePassword swaps the active key before Slot B is regenerated", async () => { const vault = await import("./vault"); await vault.createVault("correct-pass-12345"); const entry = await vault.addEntry({ title: "GitHub", username: "june", password: "secret123", url: "https://github.com", }); const setMock = chrome.storage.local.set as unknown as ReturnType; const originalSet = setMock.getMockImplementation(); let injectedDuringSlotBWrite = false; setMock.mockImplementation(async (next: Record) => { Object.assign(storage, next); if (!injectedDuringSlotBWrite && "peach_vault_b" in next) { injectedDuringSlotBWrite = true; await vault.updateEntry(entry.id, { notes: "updated during password change" }); } }); expect(await vault.changePassword("correct-pass-12345", "new-pass-67890")).toBe(true); setMock.mockImplementation(originalSet ?? (async (next: Record) => { Object.assign(storage, next); })); vault.lock(); expect(await vault.unlock("new-pass-67890")).toBe(true); expect(vault.getEntryById(entry.id)?.notes).toBe("updated during password change"); vault.lock(); expect(await vault.unlock("correct-pass-12345")).toBe(false); }); it("restoreEntries replace mode replaces existing entries", async () => { const vault = await import("./vault"); await vault.createVault("replace-pass-12345"); await vault.addEntry({ title: "A", username: "june", password: "one", url: "https://a.test" }); await vault.addEntry({ title: "B", username: "june", password: "two", url: "https://b.test" }); const incoming = [ { id: "entry-c", type: "login" as const, title: "C", username: "june", password: "three", url: "https://c.test", createdAt: 10, modifiedAt: 10, }, { id: "entry-d", type: "login" as const, title: "D", username: "june", password: "four", url: "https://d.test", createdAt: 20, modifiedAt: 20, }, ]; await expect(vault.restoreEntries(incoming, "replace")).resolves.toEqual({ added: 2, updated: 0, skipped: 0, }); expect(vault.getEntries().map((entry) => entry.id)).toEqual(["entry-c", "entry-d"]); }); it("restoreEntries merge mode lets newer incoming entries win", async () => { const vault = await import("./vault"); await vault.createVault("merge-pass-12345"); const existing = await vault.addEntry({ title: "GitHub", username: "june", password: "old-secret", url: "https://github.com", }); await expect( vault.restoreEntries( [ { ...existing, password: "new-secret", modifiedAt: existing.modifiedAt + 100, }, ], "merge" ) ).resolves.toEqual({ added: 0, updated: 1, skipped: 0 }); expect(vault.getEntryById(existing.id)?.password).toBe("new-secret"); }); it("restoreEntries merge mode keeps newer local entries when incoming is older", async () => { const vault = await import("./vault"); await vault.createVault("merge-pass-67890"); const existing = await vault.addEntry({ title: "GitHub", username: "june", password: "current-secret", url: "https://github.com", }); await expect( vault.restoreEntries( [ { ...existing, password: "stale-secret", modifiedAt: existing.modifiedAt - 100, }, ], "merge" ) ).resolves.toEqual({ added: 0, updated: 0, skipped: 1 }); expect(vault.getEntryById(existing.id)?.password).toBe("current-secret"); }); it("clearVaultEntries without explicit confirmation throws", async () => { const vault = await import("./vault"); await vault.createVault("clear-pass-12345"); await vault.addEntry({ title: "A", username: "", password: "one", url: "" }); const clearVaultEntries = vault.clearVaultEntries as unknown as ( confirmation?: unknown ) => Promise; await expect(clearVaultEntries()).rejects.toThrow(/explicit confirmation/i); await expect(clearVaultEntries("wrong")).rejects.toThrow(/explicit confirmation/i); expect(vault.getEntries()).toHaveLength(1); }); it("clearVaultEntries with REPLACE_ALL soft-deletes all entries", async () => { const vault = await import("./vault"); await vault.createVault("clear-pass-67890"); await vault.addEntry({ title: "A", username: "", password: "one", url: "" }); await vault.addEntry({ title: "B", username: "", password: "two", url: "" }); await vault.addEntry({ title: "C", username: "", password: "three", url: "" }); await vault.clearVaultEntries("REPLACE_ALL"); expect(vault.getEntries()).toEqual([]); expect(vault.getDeletedEntries()).toHaveLength(3); expect(vault.getDeletedEntries().every((entry) => typeof entry.deletedAt === "number")).toBe(true); }); it("deduplicateEntries throws in decoy mode", async () => { const vault = await import("./vault"); await vault.createVault("real-pass-12345"); await vault.addEntry({ title: "GitHub", username: "june", password: "secret123", url: "https://github.com", }); await vault.setupDuress("duress-pass-12345"); vault.lock(); expect(await vault.unlock("duress-pass-12345")).toBe(true); await expect(vault.deduplicateEntries()).rejects.toThrow(/decoy mode/i); }); it("deduplicateEntries removes older duplicates in real mode", async () => { const vault = await import("./vault"); await vault.createVault("dedupe-pass-12345"); await vault.addEntry({ title: "GitHub", username: "june", password: "old-secret", url: "https://github.com", }); await new Promise((resolve) => setTimeout(resolve, 5)); const newer = await vault.addEntry({ title: "GitHub", username: "june", password: "new-secret", url: "https://github.com", }); expect(await vault.deduplicateEntries()).toBe(1); expect(vault.getEntries()).toHaveLength(1); expect(vault.getEntries()[0].id).toBe(newer.id); expect(vault.getDeletedEntries()).toHaveLength(1); }); it("updateEntry with an empty password on a login entry clears the password", async () => { const vault = await import("./vault"); await vault.createVault("update-pass-12345"); const entry = await vault.addEntry({ title: "GitHub", username: "june", password: "secret123", url: "https://github.com", }); await expect(vault.updateEntry(entry.id, { password: "" })).resolves.toBeUndefined(); expect(vault.getEntryById(entry.id)?.password).toBeUndefined(); }); it("updateEntry with an empty password on a note entry is allowed", async () => { const vault = await import("./vault"); await vault.createVault("note-pass-12345"); const entry = await vault.addEntry({ type: "note", title: "Secure note", notes: "keep me", }); await expect(vault.updateEntry(entry.id, { password: "", notes: "updated" })).resolves.toBeUndefined(); expect(vault.getEntryById(entry.id)).toMatchObject({ type: "note", notes: "updated", }); }); it("lock zeroes the derived key and resets unlock state", async () => { const aes = await import("./aes"); const zeroKeySpy = vi.spyOn(aes, "zeroKey"); const vault = await import("./vault"); await vault.createVault("lock-pass-12345"); expect(vault.isUnlocked()).toBe(true); vault.lock(); expect(zeroKeySpy).toHaveBeenCalled(); expect(vault.isUnlocked()).toBe(false); zeroKeySpy.mockRestore(); }); it("lock clears in-memory entries", async () => { const vault = await import("./vault"); await vault.createVault("lock-pass-67890"); const entry = await vault.addEntry({ title: "GitHub", username: "june", password: "secret123", url: "https://github.com", }); expect(vault.getEntryById(entry.id)?.title).toBe("GitHub"); vault.lock(); expect(vault.getEntryById(entry.id)).toBeUndefined(); }); it("unlock with neither slot decryptable returns false and leaves the vault locked", async () => { const vault = await import("./vault"); await vault.createVault("real-pass-67890"); vault.lock(); expect(await vault.unlock("totally-wrong-password")).toBe(false); expect(vault.isUnlocked()).toBe(false); }); it("isUnlocked tracks the full lock lifecycle", async () => { const vault = await import("./vault"); expect(vault.isUnlocked()).toBe(false); await vault.createVault("unlock-pass-12345"); expect(vault.isUnlocked()).toBe(true); vault.lock(); expect(vault.isUnlocked()).toBe(false); expect(await vault.unlock("unlock-pass-12345")).toBe(true); expect(vault.isUnlocked()).toBe(true); vault.lock(); expect(vault.isUnlocked()).toBe(false); }); it("persistEntries surfaces STORAGE_QUOTA_EXCEEDED when slot writes hit quota", async () => { const vault = await import("./vault"); await vault.createVault("quota-pass-12345"); const setMock = chrome.storage.local.set as unknown as ReturnType; setMock.mockImplementation(async (next: Record) => { if ("peach_vault_a" in next || "peach_vault_b" in next) { throw new Error("QUOTA_BYTES quota reached"); } Object.assign(storage, next); }); await expect( vault.addEntry({ title: "GitHub", username: "june", password: "secret123", url: "https://github.com", }) ).rejects.toThrow("STORAGE_QUOTA_EXCEEDED"); }); it("persistSettings surfaces STORAGE_QUOTA_EXCEEDED when settings writes hit quota", async () => { const vault = await import("./vault"); await vault.createVault("settings-pass-12345"); const setMock = chrome.storage.local.set as unknown as ReturnType; setMock.mockImplementation(async (next: Record) => { if ("peach_settings" in next) { throw new Error("QUOTA_BYTES quota reached"); } Object.assign(storage, next); }); await expect(vault.updateSettings({ autoLockMinutes: 10 })).rejects.toThrow( "STORAGE_QUOTA_EXCEEDED" ); }); it("card entry round-trips through vault — all fields preserved", async () => { const vault = await import("./vault"); await vault.createVault("card-test-pass-12345"); const entry = await vault.addEntry({ type: "card", title: "Personal Visa", cardholderName: "Jane Smith", cardNumber: "4111111111111111", expMonth: "05", expYear: "2028", cvc: "123", cardNickname: "Personal Visa", network: "visa", last4: "1111", }); expect(entry.type).toBe("card"); expect(entry.cardholderName).toBe("Jane Smith"); expect(entry.cardNumber).toBe("4111111111111111"); expect(entry.expMonth).toBe("05"); expect(entry.expYear).toBe("2028"); expect(entry.cvc).toBe("123"); expect(entry.network).toBe("visa"); expect(entry.last4).toBe("1111"); vault.lock(); await vault.unlock("card-test-pass-12345"); const entries = vault.getEntries(); expect(entries).toHaveLength(1); expect(entries[0].cardNumber).toBe("4111111111111111"); expect(entries[0].cvc).toBe("123"); }); it("updateEntry — cannot clear card number on card entry", async () => { const vault = await import("./vault"); await vault.createVault("card-guard-pass-12345"); const entry = await vault.addEntry({ type: "card", title: "My Card", cardholderName: "Jane Smith", cardNumber: "4111111111111111", expMonth: "05", expYear: "2028", cvc: "123", }); await expect( vault.updateEntry(entry.id, { cardNumber: "" }) ).rejects.toThrow("card number is required for card entries"); const entries = vault.getEntries(); expect(entries[0].cardNumber).toBe("4111111111111111"); }); it("declineCardSaveOffer — stores fingerprint in settings", async () => { const vault = await import("./vault"); await vault.createVault("decline-pass-12345"); await vault.declineCardSaveOffer("1111", "05", "2028"); const settings = vault.getSettings(); expect(settings.declinedCardFingerprints).toHaveLength(1); }); it("isCardDeclined — returns true for previously declined card", async () => { const vault = await import("./vault"); await vault.createVault("declined-check-pass-12345"); await vault.declineCardSaveOffer("1111", "05", "2028"); expect(await vault.isCardDeclined("1111", "05", "2028")).toBe(true); }); it("isCardDeclined — returns false for card with same last4 but different expiry", async () => { const vault = await import("./vault"); await vault.createVault("declined-diff-pass-12345"); await vault.declineCardSaveOffer("1111", "05", "2028"); expect(await vault.isCardDeclined("1111", "06", "2028")).toBe(false); }); it("queryDomain matches an entry against its additionalUrls", async () => { const vault = await import("./vault"); await vault.createVault("multi-domain-pass-12345"); const entry = await vault.addEntry({ type: "login", title: "Foo", username: "june", password: "secret", url: "https://foo.com", additionalUrls: ["https://bar.io", "https://baz.org"], }); // Vault index keys by eTLD+1 (tldts.getDomain), so additionalUrls only // add new buckets when the eTLD+1 is genuinely different from the primary. expect(vault.queryDomain("foo.com").map((e) => e.id)).toContain(entry.id); expect(vault.queryDomain("bar.io").map((e) => e.id)).toContain(entry.id); expect(vault.queryDomain("baz.org").map((e) => e.id)).toContain(entry.id); expect(vault.queryDomain("unrelated.com")).toEqual([]); }); it("updateEntry trims, dedupes, and clears empty additionalUrls", async () => { const vault = await import("./vault"); await vault.createVault("dedupe-additional-pass-12345"); const entry = await vault.addEntry({ type: "login", title: "Foo", username: "june", password: "secret", url: "https://foo.com", }); await vault.updateEntry(entry.id, { additionalUrls: [ " https://login.foo.com ", "https://login.foo.com", "", " ", "https://accounts.foo.com", ], }); const stored = vault.getEntryById(entry.id); expect(stored?.additionalUrls).toEqual([ "https://login.foo.com", "https://accounts.foo.com", ]); await vault.updateEntry(entry.id, { additionalUrls: ["", " "] }); expect(vault.getEntryById(entry.id)?.additionalUrls).toBeUndefined(); }); it("preserves email field through lock/unlock cycle", async () => { const vault = await import("./vault"); await vault.createVault("preserve-email-pass-12345"); await vault.addEntry({ type: "login", title: "GitHub", username: "june", email: "ada@proton.me", password: "secret", url: "https://github.com", }); vault.lock(); const ok = await vault.unlock("preserve-email-pass-12345"); expect(ok).toBe(true); const entry = vault.getEntries().find((e) => e.title === "GitHub"); expect(entry?.email).toBe("ada@proton.me"); }); it("preserves additionalUrls through lock/unlock cycle", async () => { const vault = await import("./vault"); await vault.createVault("preserve-urls-pass-12345"); await vault.addEntry({ type: "login", title: "Example", username: "june", password: "secret", url: "https://example.com", additionalUrls: [ "https://beta.example.com", "https://staging.example.com", ], }); vault.lock(); const ok = await vault.unlock("preserve-urls-pass-12345"); expect(ok).toBe(true); const entry = vault.getEntries().find((e) => e.title === "Example"); expect(entry?.additionalUrls).toEqual([ "https://beta.example.com", "https://staging.example.com", ]); }); });