// main/ipc-sender.js —— assertMainSender / assertTopFrame / assertAiRequestId 单测 // // 覆盖: // - assertMainSender 四种拒绝路径:event 缺 sender / mainWindow 未创建 / // mainWindow 已销毁 / sender 不是主窗口 webContents // - assertMainSender 放行路径:sender 匹配 + mainWindow 存活 // - assertTopFrame:senderFrame 缺失拒绝 / parent 真值拒绝 / parent null 放行 // - assertAiRequestId:两种合法格式(ai-- / 裸 uuid)+ 各种垃圾拒绝 // @vitest-environment node import { describe, it, expect, vi } from 'vitest'; const { assertMainSender, assertTopFrame, assertAiRequestId, SENDER_NOT_ALLOWED, INVALID_REQUEST_ID, } = await import('../../main/ipc-sender.js'); // ---- helpers ---- function makeWin(webContents) { return { webContents, isDestroyed: () => false, }; } function makeEvent(sender) { return { sender }; } describe('assertMainSender', () => { const sender = { id: 42 }; it('event 缺 sender → 拒绝(SENDER_NOT_ALLOWED)', () => { const getMainWindow = () => makeWin(sender); const r = assertMainSender({}, getMainWindow); expect(r).toEqual({ ok: false, error: SENDER_NOT_ALLOWED }); }); it('event 完全 undefined → 拒绝', () => { const r = assertMainSender(undefined, () => makeWin(sender)); expect(r).toEqual({ ok: false, error: SENDER_NOT_ALLOWED }); }); it('mainWindow 未创建(getMainWindow 返回 null) → 拒绝', () => { const r = assertMainSender(makeEvent(sender), () => null); expect(r).toEqual({ ok: false, error: SENDER_NOT_ALLOWED }); }); it('mainWindow 已销毁 → 拒绝', () => { const win = { isDestroyed: () => true }; const r = assertMainSender(makeEvent(sender), () => win); expect(r).toEqual({ ok: false, error: SENDER_NOT_ALLOWED }); }); it('sender 不等于 mainWindow.webContents → 拒绝(防 webview / 子窗口)', () => { const otherSender = { id: 99 }; const getMainWindow = () => makeWin(sender); const r = assertMainSender(makeEvent(otherSender), getMainWindow); expect(r).toEqual({ ok: false, error: SENDER_NOT_ALLOWED }); }); it('sender 等于 mainWindow.webContents + mainWindow 存活 → 放行', () => { const getMainWindow = () => makeWin(sender); const r = assertMainSender(makeEvent(sender), getMainWindow); expect(r).toEqual({ ok: true }); }); it('getMainWindow 不是函数 → 拒绝(防御性)', () => { const r = assertMainSender(makeEvent(sender), null); expect(r).toEqual({ ok: false, error: SENDER_NOT_ALLOWED }); }); it('主窗口在调用过程中被销毁(getMainWindow 返回已销毁 win) → 拒绝', () => { // 模拟:handler 入口时主窗口还活着,但 getter 被调时已销毁(destroy 事件刚好触发) const sender = { id: 42 }; const getMainWindow = vi.fn(() => ({ isDestroyed: () => true })); const r = assertMainSender(makeEvent(sender), getMainWindow); expect(r).toEqual({ ok: false, error: SENDER_NOT_ALLOWED }); }); }); describe('assertTopFrame', () => { it('event.senderFrame 缺失 → 拒绝(保守)', () => { const r = assertTopFrame({}); expect(r).toEqual({ ok: false, error: SENDER_NOT_ALLOWED }); }); it('event 完全 undefined → 拒绝', () => { expect(assertTopFrame(undefined)).toEqual({ ok: false, error: SENDER_NOT_ALLOWED }); }); it('senderFrame.parent 真值(iframe / webview) → 拒绝', () => { const r = assertTopFrame({ senderFrame: { parent: { id: 99 } } }); expect(r).toEqual({ ok: false, error: SENDER_NOT_ALLOWED }); }); it('senderFrame.parent null(顶层 frame) → 放行', () => { const r = assertTopFrame({ senderFrame: { parent: null } }); expect(r).toEqual({ ok: true }); }); }); describe('assertAiRequestId', () => { // 标准 UUIDv4 形式 const UUID_V4 = 'f47ac10b-58cc-4372-a567-0e02b2c3d479'; // main/ai.js:968 的格式:ai-- const AI_TS_UUID = `ai-1700000000000-${UUID_V4}`; it('renderer 侧格式:裸 UUIDv4 → 放行', () => { const r = assertAiRequestId(UUID_V4); expect(r).toEqual({ ok: true, requestId: UUID_V4 }); }); it('main 侧 fallback 格式:ai-- → 放行', () => { const r = assertAiRequestId(AI_TS_UUID); expect(r).toEqual({ ok: true, requestId: AI_TS_UUID }); }); it('大写 UUIDv4 也放行(正则 case-insensitive)', () => { const r = assertAiRequestId(UUID_V4.toUpperCase()); expect(r.ok).toBe(true); }); it('非 string(number / object / null) → 拒绝', () => { expect(assertAiRequestId(123).error).toBe(INVALID_REQUEST_ID); expect(assertAiRequestId({}).error).toBe(INVALID_REQUEST_ID); expect(assertAiRequestId(null).error).toBe(INVALID_REQUEST_ID); expect(assertAiRequestId(undefined).error).toBe(INVALID_REQUEST_ID); }); it('空串 → 拒绝', () => { expect(assertAiRequestId('').error).toBe(INVALID_REQUEST_ID); }); it('过长(>200 字符) → 拒绝(防内存放大)', () => { expect(assertAiRequestId('a'.repeat(201)).error).toBe(INVALID_REQUEST_ID); }); it('UUIDv4 但缺一位 → 拒绝(hex 字符数错)', () => { const short = 'f47ac10b-58cc-4372-a567-0e02b2c3d47'; expect(assertAiRequestId(short).error).toBe(INVALID_REQUEST_ID); }); it('UUIDv4 但第 3 段首位不是 4 → 拒绝', () => { const v3 = 'f47ac10b-58cc-4372-3567-0e02b2c3d479'; expect(assertAiRequestId(v3).error).toBe(INVALID_REQUEST_ID); }); it('UUIDv4 但第 4 段首位不是 [89ab] → 拒绝', () => { const badVariant = 'f47ac10b-58cc-4372-4567-0e02b2c3d479'; expect(assertAiRequestId(badVariant).error).toBe(INVALID_REQUEST_ID); }); it('ai- 前缀但 timestamp 不是纯数字 → 拒绝', () => { const bad = `ai-notanumber-${UUID_V4}`; expect(assertAiRequestId(bad).error).toBe(INVALID_REQUEST_ID); }); it('ai- 前缀但 timestamp 过长(>18 位)→ 拒绝(防 DoS)', () => { const bad = `ai-${'1'.repeat(19)}-${UUID_V4}`; expect(assertAiRequestId(bad).error).toBe(INVALID_REQUEST_ID); }); it('包含 SQL 注入 / shell 元字符 → 拒绝', () => { expect(assertAiRequestId(`' OR 1=1; --`).error).toBe(INVALID_REQUEST_ID); expect(assertAiRequestId(`$(rm -rf /)`).error).toBe(INVALID_REQUEST_ID); }); });