123456789101112131415161718192021import { beforeEach, describe, expect, it, vi } from "vitest";2223type StorageShape = Record<string, unknown>;2425function makeChromeStorageMock() {26 const storage: StorageShape = {};27 const chrome = {28 storage: {29 local: {30 get: vi.fn(async (key?: string | string[]) => {31 if (!key) return { ...storage };32 if (typeof key === "string") return { [key]: storage[key] };33 return (Array.isArray(key) ? key : [key]).reduce<StorageShape>(34 (acc, k) => { acc[k] = storage[k]; return acc; },35 {},36 );37 }),38 set: vi.fn(async (next: StorageShape) => { Object.assign(storage, next); }),39 remove: vi.fn(async (keys: string | string[]) => {40 const ks = Array.isArray(keys) ? keys : [keys];41 for (const k of ks) delete storage[k];42 }),43 },44 session: {45 get: vi.fn(async () => ({})),46 set: vi.fn(async () => {}),47 remove: vi.fn(async () => {}),48 },49 },50 alarms: { clear: vi.fn(), create: vi.fn(), onAlarm: { addListener: vi.fn() } },51 idle: { setDetectionInterval: vi.fn(), onStateChanged: { addListener: vi.fn() } },52 runtime: {53 id: "test-extension-id",54 getURL: vi.fn((path: string) => `chrome-extension://test-extension-id/${path}`),55 },56 } as unknown as typeof globalThis.chrome;57 return { storage, chrome };58}5960describe("T-022: Message router origin and lock guards", () => {61 let vault: typeof import("./vault");62 let messages: typeof import("./messages");6364 beforeEach(async () => {65 vi.resetModules();66 const mock = makeChromeStorageMock();67 (globalThis as Record<string, unknown>).chrome = mock.chrome;68 vi.stubGlobal("fetch", vi.fn(async () => { throw new Error("no network"); }));69 vi.stubGlobal("__PEACH_E2E_PASSKEYS__", false);7071 vault = await import("./vault");72 messages = await import("./messages");7374 await vault.createVault("test-messages-pass");75 });7677 78 79 80 81 82 83 84 it("T-022a: AUTOFILL_QUERY returns vault_locked when vault is not unlocked", async () => {85 86 vault.lock();87 expect(vault.isUnlocked()).toBe(false);8889 const result = await messages.handleMessage(90 { type: "AUTOFILL_QUERY", payload: { domain: "example.com", kind: "login" } },91 {} as chrome.runtime.MessageSender,92 );9394 expect((result as { ok: boolean; error: string }).ok).toBe(false);95 expect((result as { ok: boolean; error: string }).error).toBe("vault_locked");96 });9798 99 100 101 102 103 104 105 it("T-022b: AUTOFILL_QUERY includeAll=true without userGesture → treated as includeAll=false", async () => {106 107 108 109 await vault.addEntry({110 type: "login",111 title: "Other Site",112 username: "u",113 password: "p",114 url: "https://different-site.io",115 });116117 const result = await messages.handleMessage(118 {119 type: "AUTOFILL_QUERY",120 payload: {121 domain: "example.com",122 kind: "login",123 includeAll: true, 124 userGesture: false, 125 },126 },127 {} as chrome.runtime.MessageSender,128 );129130 const { ok, data } = result as { ok: boolean; data: unknown[] };131 expect(ok).toBe(true);132 133 134 const titles = (data as Array<{ title: string }>).map((e) => e.title);135 expect(titles).not.toContain("Other Site");136 });137138 139 140 141 142 143 144 145 146 it("T-022c: GET_ENTRY_FOR_EDIT from wrong-domain sender.url → entry_not_found", async () => {147 const entry = await vault.addEntry({148 type: "login",149 title: "Target Entry",150 username: "victim",151 password: "hunter2",152 url: "https://example.com",153 });154155 156 const crossDomainSender = {157 tab: { url: "https://attacker.example.net/steal" },158 url: "https://attacker.example.net/steal",159 } as unknown as chrome.runtime.MessageSender;160161 const result = await messages.handleMessage(162 {163 type: "GET_ENTRY_FOR_EDIT",164 payload: { id: entry.id, explicitSummon: false },165 },166 crossDomainSender,167 );168169 expect((result as { ok: boolean; error: string }).ok).toBe(false);170 expect((result as { ok: boolean; error: string }).error).toBe("entry_not_found");171 });172173 174 175 176 it("T-022d: GET_ENTRY_FOR_EDIT from matching sender.url → returns entry", async () => {177 const entry = await vault.addEntry({178 type: "login",179 title: "Same Domain Entry",180 username: "legit",181 password: "pass123",182 url: "https://example.com",183 });184185 const matchingSender = {186 tab: { url: "https://example.com/path" },187 url: "https://example.com/path",188 } as unknown as chrome.runtime.MessageSender;189190 const result = await messages.handleMessage(191 {192 type: "GET_ENTRY_FOR_EDIT",193 payload: { id: entry.id, explicitSummon: false },194 },195 matchingSender,196 );197198 const { ok, data } = result as { ok: boolean; data: { title: string } };199 expect(ok).toBe(true);200 expect(data.title).toBe("Same Domain Entry");201 });202203 204 205 206 207 it("T-022e: GET_ENTRY_FOR_EDIT with explicitSummon=true from cross-domain sender → returns entry", async () => {208 const entry = await vault.addEntry({209 type: "login",210 title: "Explicit Summon Entry",211 username: "user",212 password: "pw",213 url: "https://example.com",214 });215216 const crossDomainSender = {217 tab: { url: "https://completely-different.example.net/" },218 url: "https://completely-different.example.net/",219 } as unknown as chrome.runtime.MessageSender;220221 const result = await messages.handleMessage(222 {223 type: "GET_ENTRY_FOR_EDIT",224 payload: { id: entry.id, explicitSummon: true },225 },226 crossDomainSender,227 );228229 const { ok, data } = result as { ok: boolean; data: { title: string } };230 expect(ok).toBe(true);231 expect(data.title).toBe("Explicit Summon Entry");232 });233});