123456789101112import { beforeEach, describe, expect, it, vi } from "vitest";1314type StorageShape = Record<string, unknown>;1516describe("T-008: PCH-01-016 batch-add canary/tag sanitization", () => {17 let storage: StorageShape;18 let vault: typeof import("./vault");1920 beforeEach(async () => {21 vi.resetModules();22 storage = {};2324 const chromeMock = {25 storage: {26 local: {27 get: vi.fn(async (key?: string | string[]) => {28 if (!key) return storage;29 if (typeof key === "string") return { [key]: storage[key] };30 return (Array.isArray(key) ? key : [key]).reduce<StorageShape>(31 (acc, k) => { acc[k] = storage[k]; return acc; },32 {},33 );34 }),35 set: vi.fn(async (next: StorageShape) => { Object.assign(storage, next); }),36 remove: vi.fn(async (keys: string | string[]) => {37 const ks = Array.isArray(keys) ? keys : [keys];38 for (const k of ks) delete storage[k];39 }),40 },41 session: {42 get: vi.fn(async () => ({})),43 set: vi.fn(async () => {}),44 remove: vi.fn(async () => {}),45 },46 },47 alarms: {48 clear: vi.fn(),49 create: vi.fn(),50 onAlarm: { addListener: vi.fn() },51 },52 idle: {53 setDetectionInterval: vi.fn(),54 onStateChanged: { addListener: vi.fn() },55 },56 } as unknown as typeof chrome;5758 (globalThis as Record<string, unknown>).chrome = chromeMock;59 vi.stubGlobal("fetch", vi.fn(async () => { throw new Error("no network"); }));6061 vault = await import("./vault");62 await vault.createVault("test-batch-sanitize-pass");63 });6465 66 67 68 69 it("T-008a: non-HTTPS canary webhook stripped (entry saved, canary absent)", async () => {70 const added = await vault.batchAddEntries([71 {72 type: "login",73 title: "Canary Test",74 username: "u",75 password: "p",76 url: "https://example.com",77 canary: { webhookUrl: "http://attacker.example.com/steal" },78 },79 ]);8081 expect(added).toHaveLength(1);82 expect(added[0].title).toBe("Canary Test");83 84 expect(added[0].canary).toBeUndefined();85 });8687 88 89 it("T-008b: HTTPS canary webhook URL is preserved", async () => {90 const added = await vault.batchAddEntries([91 {92 type: "login",93 title: "Legit Canary",94 username: "u",95 password: "p",96 url: "https://example.com",97 canary: { webhookUrl: "https://legit.example.com/notify" },98 },99 ]);100101 expect(added).toHaveLength(1);102 expect(added[0].canary?.webhookUrl).toBe("https://legit.example.com/notify");103 });104105 106 107 108 109 it("T-008c: oversized tags array is capped at 255 items", async () => {110 const massiveTags = Array.from({ length: 500 }, (_, i) => `tag-${i}`);111 const added = await vault.batchAddEntries([112 {113 type: "login",114 title: "Tag Test",115 username: "u",116 password: "p",117 url: "https://example.com",118 tags: massiveTags,119 },120 ]);121122 expect(added).toHaveLength(1);123 expect(added[0].tags!.length).toBeLessThanOrEqual(255);124 });125126 it("T-008c: individual tag exceeding 256 chars is truncated to 256 chars", async () => {127 const longTag = "x".repeat(300);128 const added = await vault.batchAddEntries([129 {130 type: "login",131 title: "Long Tag Test",132 username: "u",133 password: "p",134 url: "https://example.com",135 tags: [longTag],136 },137 ]);138139 expect(added).toHaveLength(1);140 expect(added[0].tags![0].length).toBeLessThanOrEqual(256);141 });142143 144 145 146 it("T-008d: __proto__ key in payload is absent from the saved entry", async () => {147 const maliciousDraft = {148 type: "login" as const,149 title: "Proto Test",150 username: "u",151 password: "p",152 url: "https://example.com",153 154 ...({ __proto__: { poisoned: true } } as object),155 };156157 const added = await vault.batchAddEntries([maliciousDraft]);158 expect(added).toHaveLength(1);159 160 expect((added[0] as unknown as Record<string, unknown>).poisoned).toBeUndefined();161 expect(Object.getPrototypeOf(added[0])).toBe(Object.prototype);162 });163164 165 166 167 168 it("T-008e: passkey entry missing credentialId is rejected (null from sanitizeEntryDraft)", async () => {169 const added = await vault.batchAddEntries([170 {171 type: "login" as const,172 title: "Bad Passkey",173 username: "u",174 password: "",175 url: "https://example.com",176 passkey: {177 rpId: "example.com",178 rpName: "Example",179 credentialId: "", 180 publicKeyCose: "a401020326",181 privateKeyJwk: { kty: "EC", crv: "P-256" },182 userHandle: "uh",183 userName: "user",184 userDisplayName: "User",185 algorithm: -7 as const,186 signCount: 0,187 aaguid: "AAAAAAAAAAAAAAAAAAAAAA",188 backupEligible: false,189 backupState: false,190 createdAt: Date.now(),191 },192 },193 ]);194195 196 expect(added).toHaveLength(0);197 });198});