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 it("PB-26-005: iframe sender.url matching the entry wins over a non-matching top-frame tab.url", async () => {206 const entry = await vault.addEntry({207 type: "login",208 title: "Frame Entry",209 username: "framed",210 password: "pw12345",211 url: "https://iframe.example.net",212 });213214 215 216 const frameSender = {217 tab: { url: "https://top.example.com/page" },218 url: "https://iframe.example.net/form",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: false },225 },226 frameSender,227 );228229 const { ok } = result as { ok: boolean };230 expect(ok).toBe(true);231 });232233 it("PB-26-005: attacker iframe sender.url denies even when the top frame matches the entry", async () => {234 const entry = await vault.addEntry({235 type: "login",236 title: "Top Entry",237 username: "top",238 password: "pw12345",239 url: "https://example.com",240 });241242 const attackerFrameSender = {243 tab: { url: "https://example.com/page" },244 url: "https://attacker.example.net/frame",245 } as unknown as chrome.runtime.MessageSender;246247 const result = await messages.handleMessage(248 {249 type: "GET_ENTRY_FOR_EDIT",250 payload: { id: entry.id, explicitSummon: false },251 },252 attackerFrameSender,253 );254255 expect((result as { ok: boolean; error?: string }).ok).toBe(false);256 expect((result as { error?: string }).error).toBe("entry_not_found");257 });258259 260 261 262 263 it("T-022e: GET_ENTRY_FOR_EDIT with explicitSummon=true from cross-domain sender → returns entry", async () => {264 const entry = await vault.addEntry({265 type: "login",266 title: "Explicit Summon Entry",267 username: "user",268 password: "pw",269 url: "https://example.com",270 });271272 const crossDomainSender = {273 tab: { url: "https://completely-different.example.net/" },274 url: "https://completely-different.example.net/",275 } as unknown as chrome.runtime.MessageSender;276277 const result = await messages.handleMessage(278 {279 type: "GET_ENTRY_FOR_EDIT",280 payload: { id: entry.id, explicitSummon: true },281 },282 crossDomainSender,283 );284285 const { ok, data } = result as { ok: boolean; data: { title: string } };286 expect(ok).toBe(true);287 expect(data.title).toBe("Explicit Summon Entry");288 });289});