1import { beforeEach, describe, expect, it, vi } from "vitest";2import { decrypt } from "./aes";3import { deriveKey } from "../crypto/argon2";4import type { StoredVault } from "../shared/types";56type StorageShape = Record<string, unknown>;78describe("background vault", () => {9 let storage: StorageShape;10 let alarms = { created: [] as Array<{ name: string; delayInMinutes: number }>, cleared: [] as string[] };1112 beforeEach(() => {13 vi.resetModules();14 storage = {};15 alarms = { created: [], cleared: [] };1617 const chromeMock = {18 storage: {19 local: {20 get: vi.fn(async (key?: string | string[]) => {21 if (!key) return storage;22 if (typeof key === "string") {23 return { [key]: storage[key] };24 }2526 return key.reduce<Record<string, unknown>>((result, entry) => {27 result[entry] = storage[entry];28 return result;29 }, {});30 }),31 set: vi.fn(async (next: Record<string, unknown>) => {32 Object.assign(storage, next);33 }),34 remove: vi.fn(async (keys: string | string[]) => {35 const list = Array.isArray(keys) ? keys : [keys];36 for (const key of list) {37 delete storage[key];38 }39 }),40 },41 },42 alarms: {43 clear: vi.fn((name: string) => {44 alarms.cleared.push(name);45 }),46 create: vi.fn((name: string, info: { delayInMinutes: number }) => {47 alarms.created.push({ name, delayInMinutes: info.delayInMinutes });48 }),49 onAlarm: { addListener: vi.fn() },50 },51 idle: {52 setDetectionInterval: vi.fn(),53 onStateChanged: { addListener: vi.fn() },54 },55 } as unknown as typeof chrome;5657 (globalThis as { chrome?: typeof chrome }).chrome = chromeMock;5859 60 vi.stubGlobal("fetch", vi.fn(async () => {61 throw new Error("network disabled in tests");62 }));63 });6465 it("creates a vault and stores encrypted payload", async () => {66 const vault = await import("./vault");6768 await vault.createVault("correct horse battery staple");6970 expect(storage.peach_vault_a).toBeDefined();71 expect((storage.peach_vault_a as StoredVault).ciphertext).toBeTruthy();72 expect(vault.getEntries()).toEqual([]);73 expect(alarms.created).toContainEqual({74 name: "autolock",75 delayInMinutes: 5,76 });77 });7879 it("writes both slots A and B on vault creation", async () => {80 const vault = await import("./vault");81 await vault.createVault("correct horse battery staple");8283 expect(storage.peach_vault_a).toBeDefined();84 expect(storage.peach_vault_b).toBeDefined();85 expect(storage.peach_duress).toBeUndefined();86 expect(storage.peach_vault).toBeUndefined();87 expect(storage.peach_decoy_vault).toBeUndefined();88 });8990 it("slot A and slot B have matching ciphertext size at fresh install", async () => {91 const vault = await import("./vault");92 await vault.createVault("correct horse battery staple");9394 const a = storage.peach_vault_a as StoredVault;95 const b = storage.peach_vault_b as StoredVault;96 expect(a.ciphertext.length).toBe(b.ciphertext.length);97 });9899 it("does not expose a plaintext duress flag after setupDuress", async () => {100 const vault = await import("./vault");101 await vault.createVault("real-password-12345");102 await vault.setupDuress("decoy-password-67890");103104 expect(storage.peach_duress).toBeUndefined();105 expect(storage.peach_decoy_vault).toBeUndefined();106 const keys = Object.keys(storage).filter((k) => k.startsWith("peach_"));107 108 109 for (const key of keys) {110 expect(111 ["peach_vault_a", "peach_vault_b", "peach_settings", "peach_vault_history"]112 ).toContain(key);113 }114 expect(keys).toContain("peach_vault_a");115 expect(keys).toContain("peach_vault_b");116 });117118 it("unlocks with real password and loads real entries", async () => {119 const vault = await import("./vault");120 await vault.createVault("real-password-12345");121 await vault.addEntry({122 title: "GitHub",123 username: "june",124 password: "realsecret",125 url: "github.com",126 });127 vault.lock();128129 const ok = await vault.unlock("real-password-12345");130 expect(ok).toBe(true);131 expect(vault.getEntries()).toHaveLength(1);132 expect(vault.getEntries()[0].title).toBe("GitHub");133 });134135 it("unlocks with duress password and loads empty entries", async () => {136 const vault = await import("./vault");137 await vault.createVault("real-password-12345");138 await vault.addEntry({139 title: "GitHub",140 username: "june",141 password: "realsecret",142 url: "github.com",143 });144 await vault.setupDuress("decoy-password-67890");145 vault.lock();146147 const ok = await vault.unlock("decoy-password-67890");148 expect(ok).toBe(true);149 expect(vault.getEntries()).toEqual([]);150 });151152 it("runs both slot derivations in parallel for constant-time unlock (DUR-002)", async () => {153 154 155 156 157 const argon2 = await import("../crypto/argon2");158 const deriveSpy = vi.spyOn(argon2, "deriveKey");159160 const vault = await import("./vault");161 await vault.createVault("real-password-12345");162 await vault.setupDuress("decoy-password-67890");163 vault.lock();164165 deriveSpy.mockClear();166 await vault.unlock("real-password-12345");167 expect(deriveSpy).toHaveBeenCalledTimes(2);168 vault.lock();169170 deriveSpy.mockClear();171 await vault.unlock("decoy-password-67890");172 expect(deriveSpy).toHaveBeenCalledTimes(2);173 vault.lock();174175 deriveSpy.mockClear();176 await vault.unlock("wrong-password-xxxxx");177 expect(deriveSpy).toHaveBeenCalledTimes(2);178179 deriveSpy.mockRestore();180 });181182 it("rejects wrong passwords", async () => {183 const vault = await import("./vault");184 await vault.createVault("real-password-12345");185 vault.lock();186187 const ok = await vault.unlock("nope-nope-nope");188 expect(ok).toBe(false);189 });190191 it("persists duressConfigured across lock/unlock in encrypted payload", async () => {192 const vault = await import("./vault");193 await vault.createVault("real-password-12345");194 await vault.setupDuress("decoy-password-67890");195 vault.lock();196197 await vault.unlock("real-password-12345");198 expect(await vault.isDuressEnabled()).toBe(true);199200 await vault.disableDuress();201 expect(await vault.isDuressEnabled()).toBe(false);202 vault.lock();203204 await vault.unlock("real-password-12345");205 expect(await vault.isDuressEnabled()).toBe(false);206 });207208 it("migrates legacy peach_vault / peach_decoy_vault / peach_duress schema", async () => {209 210 const firstVault = await import("./vault");211 await firstVault.createVault("legacy-password-abcde");212 const legacySlotA = storage.peach_vault_a as StoredVault;213 storage.peach_vault = legacySlotA;214 delete storage.peach_vault_a;215 delete storage.peach_vault_b;216 storage.peach_duress = { enabled: true };217 storage.peach_decoy_vault = legacySlotA; 218 vi.resetModules();219220 221 const vault = await import("./vault");222 await vault.hasVault();223224 225 expect(storage.peach_vault).toBeUndefined();226 expect(storage.peach_decoy_vault).toBeUndefined();227 expect(storage.peach_duress).toBeUndefined();228 expect(storage.peach_vault_a).toBeDefined();229 expect(storage.peach_vault_b).toBeDefined();230 });231232 it("soft deletes entries while keeping deletedAt in encrypted storage", async () => {233 const vault = await import("./vault");234235 await vault.createVault("correct horse battery staple");236 const entry = await vault.addEntry({237 title: "GitHub",238 username: "june",239 password: "secret123",240 url: "github.com",241 });242243 await vault.deleteEntry(entry.id);244245 expect(vault.getEntries()).toEqual([]);246247 const stored = storage.peach_vault_a as StoredVault;248 const salt = Uint8Array.from(249 stored.salt.match(/.{1,2}/g)!.map((value: string) => parseInt(value, 16))250 );251 const key = await deriveKey("correct horse battery staple", salt);252 const plain = await decrypt(key, stored.nonce, stored.ciphertext, stored.tag);253 const parsed = JSON.parse(plain) as { entries: Array<{ id: string; deletedAt?: number }> };254255 expect(parsed.entries).toHaveLength(1);256 expect(parsed.entries[0].id).toBe(entry.id);257 expect(typeof parsed.entries[0].deletedAt).toBe("number");258 });259260 it("accepts imported entries without a username or url", async () => {261 const vault = await import("./vault");262263 await vault.createVault("correct horse battery staple");264 const entry = await vault.addEntry({265 title: "Imported Login",266 username: "",267 password: "secret123",268 url: "",269 notes: "migrated from another manager",270 });271272 expect(entry.username).toBe("");273 expect(entry.url).toBe("");274 expect(vault.getEntries()).toHaveLength(1);275 });276277 it("changePassword happy path — new password unlocks, old is rejected", async () => {278 const vault = await import("./vault");279280 await vault.createVault("old-pass-abc123");281 await vault.addEntry({ title: "GitHub", username: "june", password: "secret1", url: "github.com" });282 await vault.addEntry({ title: "Gmail", username: "june", password: "secret2", url: "mail.google.com" });283284 expect(await vault.changePassword("old-pass-abc123", "new-pass-xyz789")).toBe(true);285286 vault.lock();287 expect(await vault.unlock("new-pass-xyz789")).toBe(true);288 expect(vault.getEntries()).toHaveLength(2);289290 vault.lock();291 expect(await vault.unlock("old-pass-abc123")).toBe(false);292 });293294 it("changePassword with wrong current password returns false and leaves slot A unchanged", async () => {295 const vault = await import("./vault");296297 await vault.createVault("correct-pass-abcde");298 const slotABefore = JSON.stringify(storage.peach_vault_a);299300 expect(await vault.changePassword("wrong-pass", "new-pass")).toBe(false);301 expect(JSON.stringify(storage.peach_vault_a)).toBe(slotABefore);302303 vault.lock();304 expect(await vault.unlock("correct-pass-abcde")).toBe(true);305 });306307 it("changePassword with duress configured leaves slot B byte-identical", async () => {308 const vault = await import("./vault");309310 await vault.createVault("real-pass-abcde");311 await vault.setupDuress("duress-pass-xyzzz");312313 const slotBBefore = JSON.stringify(storage.peach_vault_b);314 expect(await vault.changePassword("real-pass-abcde", "new-real-pass-fghij")).toBe(true);315 expect(JSON.stringify(storage.peach_vault_b)).toBe(slotBBefore);316317 318 vault.lock();319 expect(await vault.unlock("duress-pass-xyzzz")).toBe(true);320 expect(vault.getEntries()).toEqual([]);321 });322323 it("restoreEntries with mode 'replace' and empty incoming throws and leaves entries intact", async () => {324 const vault = await import("./vault");325326 await vault.createVault("pass-abcdef");327 await vault.addEntry({ title: "One", username: "", password: "1", url: "" });328 await vault.addEntry({ title: "Two", username: "", password: "2", url: "" });329 await vault.addEntry({ title: "Three", username: "", password: "3", url: "" });330331 await expect(vault.restoreEntries([], "replace")).rejects.toThrow(332 /refuse to replace with empty entry set/i333 );334 expect(vault.getEntries()).toHaveLength(3);335 });336337 it("changePassword swaps the active key before Slot B is regenerated", async () => {338 const vault = await import("./vault");339340 await vault.createVault("correct-pass-12345");341 const entry = await vault.addEntry({342 title: "GitHub",343 username: "june",344 password: "secret123",345 url: "https://github.com",346 });347348 const setMock = chrome.storage.local.set as unknown as ReturnType<typeof vi.fn>;349 const originalSet = setMock.getMockImplementation();350 let injectedDuringSlotBWrite = false;351352 setMock.mockImplementation(async (next: Record<string, unknown>) => {353 Object.assign(storage, next);354355 if (!injectedDuringSlotBWrite && "peach_vault_b" in next) {356 injectedDuringSlotBWrite = true;357 await vault.updateEntry(entry.id, { notes: "updated during password change" });358 }359 });360361 expect(await vault.changePassword("correct-pass-12345", "new-pass-67890")).toBe(true);362363 setMock.mockImplementation(originalSet ?? (async (next: Record<string, unknown>) => {364 Object.assign(storage, next);365 }));366367 vault.lock();368 expect(await vault.unlock("new-pass-67890")).toBe(true);369 expect(vault.getEntryById(entry.id)?.notes).toBe("updated during password change");370371 vault.lock();372 expect(await vault.unlock("correct-pass-12345")).toBe(false);373 });374375 it("restoreEntries replace mode replaces existing entries", async () => {376 const vault = await import("./vault");377378 await vault.createVault("replace-pass-12345");379 await vault.addEntry({ title: "A", username: "june", password: "one", url: "https://a.test" });380 await vault.addEntry({ title: "B", username: "june", password: "two", url: "https://b.test" });381382 const incoming = [383 {384 id: "entry-c",385 type: "login" as const,386 title: "C",387 username: "june",388 password: "three",389 url: "https://c.test",390 createdAt: 10,391 modifiedAt: 10,392 },393 {394 id: "entry-d",395 type: "login" as const,396 title: "D",397 username: "june",398 password: "four",399 url: "https://d.test",400 createdAt: 20,401 modifiedAt: 20,402 },403 ];404405 await expect(vault.restoreEntries(incoming, "replace")).resolves.toEqual({406 added: 2,407 updated: 0,408 skipped: 0,409 });410411 expect(vault.getEntries().map((entry) => entry.id)).toEqual(["entry-c", "entry-d"]);412 });413414 it("restoreEntries merge mode lets newer incoming entries win", async () => {415 const vault = await import("./vault");416417 await vault.createVault("merge-pass-12345");418 const existing = await vault.addEntry({419 title: "GitHub",420 username: "june",421 password: "old-secret",422 url: "https://github.com",423 });424425 await expect(426 vault.restoreEntries(427 [428 {429 ...existing,430 password: "new-secret",431 modifiedAt: existing.modifiedAt + 100,432 },433 ],434 "merge"435 )436 ).resolves.toEqual({ added: 0, updated: 1, skipped: 0 });437438 expect(vault.getEntryById(existing.id)?.password).toBe("new-secret");439 });440441 it("restoreEntries merge mode keeps newer local entries when incoming is older", async () => {442 const vault = await import("./vault");443444 await vault.createVault("merge-pass-67890");445 const existing = await vault.addEntry({446 title: "GitHub",447 username: "june",448 password: "current-secret",449 url: "https://github.com",450 });451452 await expect(453 vault.restoreEntries(454 [455 {456 ...existing,457 password: "stale-secret",458 modifiedAt: existing.modifiedAt - 100,459 },460 ],461 "merge"462 )463 ).resolves.toEqual({ added: 0, updated: 0, skipped: 1 });464465 expect(vault.getEntryById(existing.id)?.password).toBe("current-secret");466 });467468 it("clearVaultEntries without explicit confirmation throws", async () => {469 const vault = await import("./vault");470471 await vault.createVault("clear-pass-12345");472 await vault.addEntry({ title: "A", username: "", password: "one", url: "" });473474 const clearVaultEntries = vault.clearVaultEntries as unknown as (475 confirmation?: unknown476 ) => Promise<void>;477478 await expect(clearVaultEntries()).rejects.toThrow(/explicit confirmation/i);479 await expect(clearVaultEntries("wrong")).rejects.toThrow(/explicit confirmation/i);480 expect(vault.getEntries()).toHaveLength(1);481 });482483 it("clearVaultEntries with REPLACE_ALL soft-deletes all entries", async () => {484 const vault = await import("./vault");485486 await vault.createVault("clear-pass-67890");487 await vault.addEntry({ title: "A", username: "", password: "one", url: "" });488 await vault.addEntry({ title: "B", username: "", password: "two", url: "" });489 await vault.addEntry({ title: "C", username: "", password: "three", url: "" });490491 await vault.clearVaultEntries("REPLACE_ALL");492493 expect(vault.getEntries()).toEqual([]);494 expect(vault.getDeletedEntries()).toHaveLength(3);495 expect(vault.getDeletedEntries().every((entry) => typeof entry.deletedAt === "number")).toBe(true);496 });497498 it("deduplicateEntries throws in decoy mode", async () => {499 const vault = await import("./vault");500501 await vault.createVault("real-pass-12345");502 await vault.addEntry({503 title: "GitHub",504 username: "june",505 password: "secret123",506 url: "https://github.com",507 });508 await vault.setupDuress("duress-pass-12345");509 vault.lock();510511 expect(await vault.unlock("duress-pass-12345")).toBe(true);512 await expect(vault.deduplicateEntries()).rejects.toThrow(/decoy mode/i);513 });514515 it("deduplicateEntries removes older duplicates in real mode", async () => {516 const vault = await import("./vault");517518 await vault.createVault("dedupe-pass-12345");519 await vault.addEntry({520 title: "GitHub",521 username: "june",522 password: "old-secret",523 url: "https://github.com",524 });525 await new Promise<void>((resolve) => setTimeout(resolve, 5));526 const newer = await vault.addEntry({527 title: "GitHub",528 username: "june",529 password: "new-secret",530 url: "https://github.com",531 });532533 expect(await vault.deduplicateEntries()).toBe(1);534 expect(vault.getEntries()).toHaveLength(1);535 expect(vault.getEntries()[0].id).toBe(newer.id);536 expect(vault.getDeletedEntries()).toHaveLength(1);537 });538539 it("updateEntry with an empty password on a login entry throws and rolls back", async () => {540 const vault = await import("./vault");541542 await vault.createVault("update-pass-12345");543 const entry = await vault.addEntry({544 title: "GitHub",545 username: "june",546 password: "secret123",547 url: "https://github.com",548 });549550 await expect(vault.updateEntry(entry.id, { password: "" })).rejects.toThrow(551 /password or passkey required/i552 );553 expect(vault.getEntryById(entry.id)?.password).toBe("secret123");554 });555556 it("updateEntry with an empty password on a note entry is allowed", async () => {557 const vault = await import("./vault");558559 await vault.createVault("note-pass-12345");560 const entry = await vault.addEntry({561 type: "note",562 title: "Secure note",563 notes: "keep me",564 });565566 await expect(vault.updateEntry(entry.id, { password: "", notes: "updated" })).resolves.toBeUndefined();567 expect(vault.getEntryById(entry.id)).toMatchObject({568 type: "note",569 notes: "updated",570 });571 });572573 it("lock zeroes the derived key and resets unlock state", async () => {574 const aes = await import("./aes");575 const zeroKeySpy = vi.spyOn(aes, "zeroKey");576 const vault = await import("./vault");577578 await vault.createVault("lock-pass-12345");579 expect(vault.isUnlocked()).toBe(true);580581 vault.lock();582583 expect(zeroKeySpy).toHaveBeenCalled();584 expect(vault.isUnlocked()).toBe(false);585 zeroKeySpy.mockRestore();586 });587588 it("lock clears in-memory entries", async () => {589 const vault = await import("./vault");590591 await vault.createVault("lock-pass-67890");592 const entry = await vault.addEntry({593 title: "GitHub",594 username: "june",595 password: "secret123",596 url: "https://github.com",597 });598599 expect(vault.getEntryById(entry.id)?.title).toBe("GitHub");600 vault.lock();601 expect(vault.getEntryById(entry.id)).toBeUndefined();602 });603604 it("unlock with neither slot decryptable returns false and leaves the vault locked", async () => {605 const vault = await import("./vault");606607 await vault.createVault("real-pass-67890");608 vault.lock();609610 expect(await vault.unlock("totally-wrong-password")).toBe(false);611 expect(vault.isUnlocked()).toBe(false);612 });613614 it("isUnlocked tracks the full lock lifecycle", async () => {615 const vault = await import("./vault");616617 expect(vault.isUnlocked()).toBe(false);618 await vault.createVault("unlock-pass-12345");619 expect(vault.isUnlocked()).toBe(true);620 vault.lock();621 expect(vault.isUnlocked()).toBe(false);622 expect(await vault.unlock("unlock-pass-12345")).toBe(true);623 expect(vault.isUnlocked()).toBe(true);624 vault.lock();625 expect(vault.isUnlocked()).toBe(false);626 });627628 it("persistEntries surfaces STORAGE_QUOTA_EXCEEDED when slot writes hit quota", async () => {629 const vault = await import("./vault");630631 await vault.createVault("quota-pass-12345");632633 const setMock = chrome.storage.local.set as unknown as ReturnType<typeof vi.fn>;634 setMock.mockImplementation(async (next: Record<string, unknown>) => {635 if ("peach_vault_a" in next || "peach_vault_b" in next) {636 throw new Error("QUOTA_BYTES quota reached");637 }638 Object.assign(storage, next);639 });640641 await expect(642 vault.addEntry({643 title: "GitHub",644 username: "june",645 password: "secret123",646 url: "https://github.com",647 })648 ).rejects.toThrow("STORAGE_QUOTA_EXCEEDED");649 });650651 it("persistSettings surfaces STORAGE_QUOTA_EXCEEDED when settings writes hit quota", async () => {652 const vault = await import("./vault");653654 await vault.createVault("settings-pass-12345");655656 const setMock = chrome.storage.local.set as unknown as ReturnType<typeof vi.fn>;657 setMock.mockImplementation(async (next: Record<string, unknown>) => {658 if ("peach_settings" in next) {659 throw new Error("QUOTA_BYTES quota reached");660 }661 Object.assign(storage, next);662 });663664 await expect(vault.updateSettings({ autoLockMinutes: 10 })).rejects.toThrow(665 "STORAGE_QUOTA_EXCEEDED"666 );667 });668669 it("card entry round-trips through vault — all fields preserved", async () => {670 const vault = await import("./vault");671672 await vault.createVault("card-test-pass-12345");673 const entry = await vault.addEntry({674 type: "card",675 title: "Personal Visa",676 cardholderName: "Jane Smith",677 cardNumber: "4111111111111111",678 expMonth: "05",679 expYear: "2028",680 cvc: "123",681 cardNickname: "Personal Visa",682 network: "visa",683 last4: "1111",684 });685686 expect(entry.type).toBe("card");687 expect(entry.cardholderName).toBe("Jane Smith");688 expect(entry.cardNumber).toBe("4111111111111111");689 expect(entry.expMonth).toBe("05");690 expect(entry.expYear).toBe("2028");691 expect(entry.cvc).toBe("123");692 expect(entry.network).toBe("visa");693 expect(entry.last4).toBe("1111");694695 vault.lock();696 await vault.unlock("card-test-pass-12345");697 const entries = vault.getEntries();698 expect(entries).toHaveLength(1);699 expect(entries[0].cardNumber).toBe("4111111111111111");700 expect(entries[0].cvc).toBe("123");701 });702703 it("updateEntry — cannot clear card number on card entry", async () => {704 const vault = await import("./vault");705706 await vault.createVault("card-guard-pass-12345");707 const entry = await vault.addEntry({708 type: "card",709 title: "My Card",710 cardholderName: "Jane Smith",711 cardNumber: "4111111111111111",712 expMonth: "05",713 expYear: "2028",714 cvc: "123",715 });716717 await expect(718 vault.updateEntry(entry.id, { cardNumber: "" })719 ).rejects.toThrow("card number is required for card entries");720721 const entries = vault.getEntries();722 expect(entries[0].cardNumber).toBe("4111111111111111");723 });724725 it("declineCardSaveOffer — stores fingerprint in settings", async () => {726 const vault = await import("./vault");727728 await vault.createVault("decline-pass-12345");729 await vault.declineCardSaveOffer("1111", "05", "2028");730731 const settings = vault.getSettings();732 expect(settings.declinedCardFingerprints).toHaveLength(1);733 });734735 it("isCardDeclined — returns true for previously declined card", async () => {736 const vault = await import("./vault");737738 await vault.createVault("declined-check-pass-12345");739 await vault.declineCardSaveOffer("1111", "05", "2028");740741 expect(await vault.isCardDeclined("1111", "05", "2028")).toBe(true);742 });743744 it("isCardDeclined — returns false for card with same last4 but different expiry", async () => {745 const vault = await import("./vault");746747 await vault.createVault("declined-diff-pass-12345");748 await vault.declineCardSaveOffer("1111", "05", "2028");749750 expect(await vault.isCardDeclined("1111", "06", "2028")).toBe(false);751 });752753 it("queryDomain matches an entry against its additionalUrls", async () => {754 const vault = await import("./vault");755756 await vault.createVault("multi-domain-pass-12345");757 const entry = await vault.addEntry({758 type: "login",759 title: "Foo",760 username: "june",761 password: "secret",762 url: "https://foo.com",763 additionalUrls: ["https://bar.io", "https://baz.org"],764 });765766 767 768 expect(vault.queryDomain("foo.com").map((e) => e.id)).toContain(entry.id);769 expect(vault.queryDomain("bar.io").map((e) => e.id)).toContain(entry.id);770 expect(vault.queryDomain("baz.org").map((e) => e.id)).toContain(entry.id);771 expect(vault.queryDomain("unrelated.com")).toEqual([]);772 });773774 it("updateEntry trims, dedupes, and clears empty additionalUrls", async () => {775 const vault = await import("./vault");776777 await vault.createVault("dedupe-additional-pass-12345");778 const entry = await vault.addEntry({779 type: "login",780 title: "Foo",781 username: "june",782 password: "secret",783 url: "https://foo.com",784 });785786 await vault.updateEntry(entry.id, {787 additionalUrls: [788 " https://login.foo.com ",789 "https://login.foo.com",790 "",791 " ",792 "https://accounts.foo.com",793 ],794 });795796 const stored = vault.getEntryById(entry.id);797 expect(stored?.additionalUrls).toEqual([798 "https://login.foo.com",799 "https://accounts.foo.com",800 ]);801802 await vault.updateEntry(entry.id, { additionalUrls: ["", " "] });803 expect(vault.getEntryById(entry.id)?.additionalUrls).toBeUndefined();804 });805});