import { beforeEach, describe, expect, it, vi } from 'vitest'; const handleMock = vi.fn(); vi.mock('electron', () => ({ ipcMain: { handle: handleMock }, })); vi.mock('../../electron/utils/config', () => ({ getPort: () => 13210, })); vi.mock('../../electron/api/server', () => ({ getHostApiToken: () => 'host-token', })); vi.mock('../../electron/api/renderer-capability', () => ({ getRendererCapability: () => 'renderer-token', RENDERER_CAPABILITY_HEADER: 'x-niancode-renderer-capability', })); describe('Host API IPC proxy', () => { beforeEach(() => { vi.resetModules(); vi.clearAllMocks(); vi.unstubAllGlobals(); }); it('connects to the loopback Host API with direct fetch', async () => { const fetchMock = vi.fn().mockResolvedValue(new Response( JSON.stringify({ success: true }), { status: 200, headers: { 'content-type': 'application/json' } }, )); vi.stubGlobal('fetch', fetchMock); const { registerHostApiProxyHandlers } = await import('../../electron/main/ipc/host-api-proxy'); registerHostApiProxyHandlers(); const fetchHandler = handleMock.mock.calls.find(([channel]) => channel === 'hostapi:fetch')?.[1]; expect(fetchHandler).toBeTypeOf('function'); const result = await fetchHandler(undefined, { path: '/api/coding/projects/create', method: 'POST', body: JSON.stringify({ projectName: '测试项目' }), }); expect(fetchMock).toHaveBeenCalledWith( 'http://127.0.0.1:13210/api/coding/projects/create', expect.objectContaining({ method: 'POST', headers: expect.objectContaining({ Authorization: 'Bearer host-token', 'Content-Type': 'application/json', 'x-niancode-renderer-capability': 'renderer-token', }), }), ); expect(result).toEqual({ ok: true, data: { status: 200, ok: true, json: { success: true } }, }); }); it('preserves ordinary renderer headers but replaces forged security headers', async () => { const fetchMock = vi.fn().mockResolvedValue(new Response(null, { status: 204 })); vi.stubGlobal('fetch', fetchMock); const { registerHostApiProxyHandlers } = await import('../../electron/main/ipc/host-api-proxy'); registerHostApiProxyHandlers(); const fetchHandler = handleMock.mock.calls.find(([channel]) => channel === 'hostapi:fetch')?.[1]; expect(fetchHandler).toBeTypeOf('function'); await fetchHandler(undefined, { path: '/api/ai-hardware', headers: { Accept: 'application/json', 'X-Renderer-Request': 'hardware-overview', authorization: 'Bearer renderer-forged-token', 'X-NIANCODE-RENDERER-CAPABILITY': 'renderer-forged-capability', }, }); const [, init] = fetchMock.mock.calls[0] as [string, RequestInit]; const headers = new Headers(init.headers); expect(headers.get('accept')).toBe('application/json'); expect(headers.get('x-renderer-request')).toBe('hardware-overview'); expect(headers.get('authorization')).toBe('Bearer host-token'); expect(headers.get('x-niancode-renderer-capability')).toBe('renderer-token'); }); it('dispatches short JSON requests in Main without opening loopback HTTP', async () => { const fetchMock = vi.fn(); vi.stubGlobal('fetch', fetchMock); vi.doMock('../../electron/api/host-api-dispatcher', () => ({ dispatchHostApiRequest: vi.fn().mockResolvedValue({ status: 200, ok: true, json: { success: true, transport: 'test' }, }), })); const { registerHostApiProxyHandlers } = await import('../../electron/main/ipc/host-api-proxy'); registerHostApiProxyHandlers({} as never); const fetchHandler = handleMock.mock.calls.find(([channel]) => channel === 'hostapi:fetch')?.[1]; const result = await fetchHandler(undefined, { path: '/api/app/runtime-info' }); expect(fetchMock).not.toHaveBeenCalled(); expect(result).toEqual({ ok: true, data: { status: 200, ok: true, json: { success: true, transport: 'test' }, transport: 'dispatcher' }, }); }); it('keeps attachment upload bytes raw and returns image bytes through IPC', async () => { const fetchMock = vi.fn() .mockResolvedValueOnce(new Response( JSON.stringify({ attachmentId: 'attachment-1', mime: 'image/png', byteLength: 4 }), { status: 201, headers: { 'content-type': 'application/json' } }, )) .mockResolvedValueOnce(new Response( new Uint8Array([1, 2, 3, 4]), { status: 200, headers: { 'content-type': 'image/png' } }, )); vi.stubGlobal('fetch', fetchMock); const { registerHostApiProxyHandlers } = await import('../../electron/main/ipc/host-api-proxy'); registerHostApiProxyHandlers({} as never); const fetchHandler = handleMock.mock.calls.find(([channel]) => channel === 'hostapi:fetch')?.[1]; const upload = await fetchHandler(undefined, { path: '/api/coding/attachments', method: 'POST', headers: { 'Content-Type': 'image/png' }, body: new Uint8Array([1, 2, 3, 4]).buffer, }); const [, uploadInit] = fetchMock.mock.calls[0] as [string, RequestInit]; expect(Array.from(uploadInit.body as Uint8Array)).toEqual([1, 2, 3, 4]); expect(new Headers(uploadInit.headers).get('content-type')).toBe('image/png'); expect(upload).toMatchObject({ ok: true, data: { status: 201, json: { attachmentId: 'attachment-1' } }, }); const preview = await fetchHandler(undefined, { path: '/api/coding/attachments/attachment-1/content', method: 'GET', }); expect(preview).toMatchObject({ ok: true, data: { status: 200, contentType: 'image/png', bytes: new Uint8Array([1, 2, 3, 4]), }, }); }); });