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 chrome;57 return { storage, chrome };58}5960describe("T-022: Message router origin and lock guards", () => {61 let vault: typeof import("../../background/vault");62 let messages: typeof import("../../background/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"); }));6970 vault = await import("../../background/vault");71 messages = await import("../../background/messages");7273 await vault.createVault("test-messages-pass");74 });7576 77 78 79 80 81 82 83 it("T-022a: AUTOFILL_QUERY returns vault_locked when vault is not unlocked", async () => {84 85 vault.lock();86 expect(vault.isUnlocked()).toBe(false);8788 const result = await messages.handleMessage(89 { type: "AUTOFILL_QUERY", payload: { domain: "example.com", kind: "login" } },90 {} as chrome.runtime.MessageSender,91 );9293 expect((result as { ok: boolean; error: string }).ok).toBe(false);94 expect((result as { ok: boolean; error: string }).error).toBe("vault_locked");95 });9697 98 99 100 101 102 103 104 it("T-022b: AUTOFILL_QUERY includeAll=true without userGesture → treated as includeAll=false", async () => {105 106 await vault.addEntry({107 type: "login",108 title: "Other Site",109 username: "u",110 password: "p",111 url: "https://other.example.com",112 });113114 const result = await messages.handleMessage(115 {116 type: "AUTOFILL_QUERY",117 payload: {118 domain: "example.com",119 kind: "login",120 includeAll: true, 121 userGesture: false, 122 },123 },124 {} as chrome.runtime.MessageSender,125 );126127 const { ok, data } = result as { ok: boolean; data: unknown[] };128 expect(ok).toBe(true);129 130 131 const titles = (data as Array<{ title: string }>).map((e) => e.title);132 expect(titles).not.toContain("Other Site");133 });134135 136 137 138 139 140 141 142 143 it("T-022c: GET_ENTRY_FOR_EDIT from wrong-domain sender.url → entry_not_found", async () => {144 const entry = await vault.addEntry({145 type: "login",146 title: "Target Entry",147 username: "victim",148 password: "hunter2",149 url: "https://example.com",150 });151152 153 const crossDomainSender = {154 tab: { url: "https://attacker.example.net/steal" },155 url: "https://attacker.example.net/steal",156 } as unknown as chrome.runtime.MessageSender;157158 const result = await messages.handleMessage(159 {160 type: "GET_ENTRY_FOR_EDIT",161 payload: { id: entry.id, explicitSummon: false },162 },163 crossDomainSender,164 );165166 expect((result as { ok: boolean; error: string }).ok).toBe(false);167 expect((result as { ok: boolean; error: string }).error).toBe("entry_not_found");168 });169170 171 172 173 it("T-022d: GET_ENTRY_FOR_EDIT from matching sender.url → returns entry", async () => {174 const entry = await vault.addEntry({175 type: "login",176 title: "Same Domain Entry",177 username: "legit",178 password: "pass123",179 url: "https://example.com",180 });181182 const matchingSender = {183 tab: { url: "https://example.com/path" },184 url: "https://example.com/path",185 } as unknown as chrome.runtime.MessageSender;186187 const result = await messages.handleMessage(188 {189 type: "GET_ENTRY_FOR_EDIT",190 payload: { id: entry.id, explicitSummon: false },191 },192 matchingSender,193 );194195 const { ok, data } = result as { ok: boolean; data: { title: string } };196 expect(ok).toBe(true);197 expect(data.title).toBe("Same Domain Entry");198 });199200 201 202 203 204 it("T-022e: GET_ENTRY_FOR_EDIT with explicitSummon=true from cross-domain sender → returns entry", async () => {205 const entry = await vault.addEntry({206 type: "login",207 title: "Explicit Summon Entry",208 username: "user",209 password: "pw",210 url: "https://example.com",211 });212213 const crossDomainSender = {214 tab: { url: "https://completely-different.example.net/" },215 url: "https://completely-different.example.net/",216 } as unknown as chrome.runtime.MessageSender;217218 const result = await messages.handleMessage(219 {220 type: "GET_ENTRY_FOR_EDIT",221 payload: { id: entry.id, explicitSummon: true },222 },223 crossDomainSender,224 );225226 const { ok, data } = result as { ok: boolean; data: { title: string } };227 expect(ok).toBe(true);228 expect(data.title).toBe("Explicit Summon Entry");229 });230});