import { EventEmitter } from 'node:events'; import type { IncomingMessage, ServerResponse } from 'node:http'; import { describe, expect, it, vi } from 'vitest'; import { handleDataServiceRoutes } from '@electron/api/routes/data-service'; import type { HostApiContext } from '@electron/api/context'; import type { DataServiceOperations } from '@electron/services/data-service-client'; function request( method: string, body?: unknown, headers: Record = {}, ): IncomingMessage { const req = new EventEmitter(); const raw = body === undefined ? undefined : typeof body === 'string' ? body : JSON.stringify(body); Object.assign(req, { method, headers: { ...(raw === undefined ? {} : { 'content-length': String(Buffer.byteLength(raw, 'utf8')) }), ...headers, }, [Symbol.asyncIterator]: async function* () { if (raw !== undefined) yield Buffer.from(raw, 'utf8'); }, }); return req as IncomingMessage; } function response() { const chunks: string[] = []; const headers = new Map(); const res = new EventEmitter(); Object.assign(res, { statusCode: 0, setHeader: vi.fn((name: string, value: string) => headers.set(name.toLowerCase(), value)), end: vi.fn((chunk?: string) => { if (chunk) chunks.push(chunk); }), }); return { res: res as unknown as ServerResponse, get status() { return (res as { statusCode: number }).statusCode; }, header: (name: string) => headers.get(name.toLowerCase()), json: () => JSON.parse(chunks.join('')) as Record, }; } function success(data: T): { success: true; status: 200; code: null; error: null; retryable: false; data: T } { return { success: true, status: 200, code: null, error: null, retryable: false, data }; } function setup(overrides: Partial>> = {}) { const operations = { configure: vi.fn().mockResolvedValue(success({})), inspect: vi.fn().mockResolvedValue(success({})), listProjects: vi.fn().mockResolvedValue(success({ items: [], total: 0, instance_limit: 20 })), getDocument: vi.fn().mockResolvedValue(success({})), listDocuments: vi.fn().mockResolvedValue(success({ items: [], next_cursor: null, limit: 50 })), putDocument: vi.fn().mockResolvedValue(success({})), deleteDocument: vi.fn().mockResolvedValue(success(null)), removeCollection: vi.fn().mockResolvedValue(success({ removed: true, usage: { document_count: 0, total_bytes: 0 } })), reset: vi.fn().mockResolvedValue(success({})), removeProject: vi.fn().mockResolvedValue(success({ removed: true })), ...overrides, } as unknown as DataServiceOperations; const ctx = { codingProducts: { dataService: operations } } as unknown as HostApiContext; return { operations, ctx }; } async function invoke( ctx: HostApiContext, method: string, path: string, body?: unknown, headers: Record = {}, ) { const target = response(); const handled = await handleDataServiceRoutes( request(method, body, headers), target.res, new URL(`http://localhost${path}`), ctx, ); return { handled, ...target, payload: target.json() }; } describe('Data Service Host routes', () => { it('uses the owner-wide listing without an active-project argument', async () => { const { operations, ctx } = setup(); const result = await invoke(ctx, 'GET', '/api/works/data-service/projects'); expect(result.handled).toBe(true); expect(result.status).toBe(200); expect(result.header('cache-control')).toBe('private, no-store'); expect(operations.listProjects).toHaveBeenCalledOnce(); expect(result.payload).toMatchObject({ success: true, data: { total: 0 } }); }); it('passes only strict collection input to active-project configure', async () => { const { operations, ctx } = setup(); await invoke(ctx, 'PUT', '/api/works/data-service/project', { collections: ['todos', 'settings'] }, { 'content-type': 'application/json', }); expect(operations.configure).toHaveBeenCalledWith({ collections: ['todos', 'settings'] }); }); it('requires the literal confirmation before removing the active project', async () => { const { operations, ctx } = setup(); const rejected = await invoke(ctx, 'DELETE', '/api/works/data-service/project'); const accepted = await invoke(ctx, 'DELETE', '/api/works/data-service/project?confirmed=true'); expect(rejected.payload).toMatchObject({ success: false, status: 400, code: 'confirmation_required', data: null }); expect(operations.removeProject).toHaveBeenCalledOnce(); expect(operations.removeProject).toHaveBeenCalledWith({ confirmed: true }); expect(accepted.payload).toMatchObject({ success: true, status: 200 }); }); it('projects a document precondition and rejects extra request fields locally', async () => { const { operations, ctx } = setup(); await invoke( ctx, 'PUT', '/api/works/data-service/project/collections/todos/documents/todo-1', { data: { done: false } }, { 'content-type': 'application/json', 'if-match': '"7"' }, ); const rejected = await invoke( ctx, 'PUT', '/api/works/data-service/project/collections/todos/documents/todo-2', { data: {}, extra: true }, { 'content-type': 'application/json' }, ); expect(operations.putDocument).toHaveBeenCalledWith({ collection: 'todos', document_id: 'todo-1', data: { done: false }, if_revision: 7, }); expect(rejected.payload).toMatchObject({ success: false, status: 422, code: 'invalid_request', data: null }); expect(operations.putDocument).toHaveBeenCalledOnce(); }); it('keeps malformed paths and unsupported query keys out of the operations adapter', async () => { const { operations, ctx } = setup(); const result = await invoke( ctx, 'GET', '/api/works/data-service/project/collections/todos/documents?limit=101&unexpected=x', ); expect(result.payload).toMatchObject({ success: false, status: 422, code: 'invalid_request', data: null }); expect(operations.listDocuments).not.toHaveBeenCalled(); }); });