/** * IPC App / Data 域测试(v0.7.2 覆盖补齐) * * 锁定安全与数据完整性契约: * 1. app:openExternal 协议白名单(M-12:file:/smb:/javascript: 拒绝) * 2. error:report 渲染层错误上报:超长字段截断 + 审计落库 * 3. audit:query 的 eventType 枚举校验与 limit 边界(M-45) * 4. data:export 的导出脱敏双保险(dataUrl 剥离 + 配置掩码 + 会话条数上限) * 5. data:clear* 危险操作的事务语义与审计日志 */ import { describe, it, expect, vi, beforeEach, type Mock } from 'vitest'; const ipcMainHandleMock = vi.fn(); const ipcMainOnMock = vi.fn(); vi.mock('electron', () => ({ ipcMain: { handle: (...args: unknown[]) => ipcMainHandleMock(...args), on: (...args: unknown[]) => ipcMainOnMock(...args), }, shell: { openExternal: vi.fn(async () => undefined), showItemInFolder: vi.fn() }, dialog: { showOpenDialog: vi.fn(async () => ({ canceled: true, filePaths: [] })) }, BrowserWindow: { fromWebContents: vi.fn(() => null) }, app: { getVersion: vi.fn(() => '0.7.2'), getPath: vi.fn(() => '/tmp/userdata'), relaunch: vi.fn(), exit: vi.fn(), }, })); vi.mock('electron-log', () => ({ default: { info: vi.fn(), warn: vi.fn(), error: vi.fn(), debug: vi.fn() }, })); import { registerAppHandlers } from '../app'; import { registerDataHandlers } from '../data'; import type { IPCContext } from '../context'; function getHandler(channel: string): (...args: unknown[]) => Promise { const call = ipcMainHandleMock.mock.calls.find(([ch]) => ch === channel); if (!call) throw new Error(`IPC handler not registered: ${channel}`); return call[1] as (...args: unknown[]) => Promise; } function getListener(channel: string): (...args: unknown[]) => void { const call = ipcMainOnMock.mock.calls.find(([ch]) => ch === channel); if (!call) throw new Error(`IPC listener not registered: ${channel}`); return call[1] as (...args: unknown[]) => void; } beforeEach(() => { ipcMainHandleMock.mockClear(); ipcMainOnMock.mockClear(); }); // ===== App 域 ===== describe('app:openExternal — 协议白名单(M-12)', () => { function makeCtx(): IPCContext { return { configService: { get: vi.fn(() => null) } } as unknown as IPCContext; } it('http/https/mailto 放行', async () => { registerAppHandlers(makeCtx()); const handler = getHandler('app:openExternal'); for (const url of ['https://example.com', 'http://example.com/x', 'mailto:a@b.com']) { expect(await handler(null, url)).toMatchObject({ success: true }); } }); it.each([ 'file:///etc/passwd', 'smb://host/share', 'javascript:alert(1)', 'data:text/html,