// @vitest-environment node import { EventEmitter } from 'node:events'; import type { IncomingMessage, ServerResponse } from 'node:http'; import { describe, expect, it, vi } from 'vitest'; import type { HostApiContext } from '../../electron/api/context'; import { handlePluginMarketplaceRoutes } from '../../electron/api/routes/plugin-marketplace'; function request(method: string, body?: unknown): IncomingMessage { const req = new EventEmitter(); const raw = body === undefined ? undefined : JSON.stringify(body); Object.assign(req, { method, headers: raw === undefined ? {} : { 'content-length': String(Buffer.byteLength(raw)) }, [Symbol.asyncIterator]: async function* () { if (raw !== undefined) yield Buffer.from(raw); }, }); return req as IncomingMessage; } function response() { const chunks: string[] = []; const res = new EventEmitter(); Object.assign(res, { statusCode: 0, setHeader: vi.fn(), 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; }, json: () => JSON.parse(chunks.join('')) as Record, }; } async function invoke(ctx: HostApiContext, method: string, target: string, body?: unknown) { const output = response(); const handled = await handlePluginMarketplaceRoutes( request(method, body), output.res, new URL(`http://localhost${target}`), ctx, ); return { handled, status: output.status, payload: output.json() }; } describe('Main-owned plugin Marketplace routes', () => { it('forwards only bounded catalog query fields and detail IDs', async () => { const catalog = vi.fn().mockResolvedValue({ items: [], total: 0 }); const detail = vi.fn().mockResolvedValue({ pluginId: 'notes' }); const ctx = { codingProducts: { pluginMarketplace: { readCatalog: catalog, readDetail: detail, } } } as unknown as HostApiContext; await expect(invoke(ctx, 'GET', '/api/coding/plugin-marketplace/catalog?query=notes&featured=true&limit=3')) .resolves.toMatchObject({ handled: true, status: 200 }); expect(catalog).toHaveBeenCalledWith({ query: 'notes', featured: true, limit: 3 }); await expect(invoke(ctx, 'GET', '/api/coding/plugin-marketplace/plugins/notes')) .resolves.toMatchObject({ handled: true, status: 200 }); expect(detail).toHaveBeenCalledWith('notes'); const rejected = await invoke(ctx, 'GET', '/api/coding/plugin-marketplace/catalog?accountKey=forged'); expect(rejected).toMatchObject({ status: 400, payload: { code: 'plugin_request_invalid' } }); expect(catalog).toHaveBeenCalledOnce(); }); it('keeps acquire, install, update, and uninstall as explicit separate Main actions', async () => { const acquire = vi.fn().mockResolvedValue({ items: [] }); const remove = vi.fn().mockResolvedValue({ items: [] }); const install = vi.fn().mockResolvedValue({ status: 'installed', pluginId: 'notes' }); const update = vi.fn().mockResolvedValue({ status: 'installed', pluginId: 'notes' }); const uninstall = vi.fn().mockResolvedValue({ status: 'removed', pluginId: 'notes' }); const ctx = { codingProducts: { pluginMarketplace: { acquire, remove, install, update, uninstall, readCatalog: vi.fn(), readDetail: vi.fn(), readLibrary: vi.fn(), } } } as unknown as HostApiContext; expect((await invoke(ctx, 'PUT', '/api/coding/plugin-marketplace/library/notes')).status).toBe(200); expect((await invoke(ctx, 'DELETE', '/api/coding/plugin-marketplace/library/notes')).status).toBe(200); expect((await invoke(ctx, 'POST', '/api/coding/plugin-marketplace/install/notes')).status).toBe(200); expect((await invoke(ctx, 'POST', '/api/coding/plugin-marketplace/update/notes')).status).toBe(200); expect((await invoke(ctx, 'DELETE', '/api/coding/plugin-marketplace/install/notes')).status).toBe(200); expect(acquire).toHaveBeenCalledWith('notes'); expect(remove).toHaveBeenCalledWith('notes'); expect(install).toHaveBeenCalledWith('notes'); expect(update).toHaveBeenCalledWith('notes'); expect(uninstall).toHaveBeenCalledWith('notes'); }); it('rejects Renderer authority fields and never exposes backend errors', async () => { const acquire = vi.fn(); const ctx = { codingProducts: { pluginMarketplace: { acquire, readCatalog: vi.fn(), readDetail: vi.fn(), readLibrary: vi.fn(), remove: vi.fn(), install: vi.fn(), update: vi.fn(), uninstall: vi.fn(), } } } as unknown as HostApiContext; const forged = await invoke(ctx, 'PUT', '/api/coding/plugin-marketplace/library/notes', { accountId: 'forged', installRoot: 'C:\\untrusted', }); expect(forged).toMatchObject({ status: 400, payload: { code: 'plugin_request_invalid' } }); expect(acquire).not.toHaveBeenCalled(); }); });