Files
makelore/tests/unit/coding-plugin-routes.test.ts

102 lines
3.9 KiB
TypeScript

// @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 { handleCodingPluginRoutes } from '../../electron/api/routes/coding-plugins';
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<string, unknown>,
};
}
async function invoke(ctx: HostApiContext, method: string, target: string, body?: unknown) {
const output = response();
const handled = await handleCodingPluginRoutes(
request(method, body),
output.res,
new URL(`http://localhost${target}`),
ctx,
);
return { handled, status: output.status, payload: output.json() };
}
describe('coding plugin Host routes', () => {
it('uses only the local project handle for GET and preserves the bounded projection', async () => {
const projection = {
schemaVersion: 1 as const,
project: { localProjectId: 'local-a', durableProjectId: 'durable-a' },
policyStatus: 'current' as const,
unknownPluginIds: ['makelore.removed'],
items: [],
};
const list = vi.fn().mockResolvedValue(projection);
const ctx = { codingProducts: { plugins: { list } } } as unknown as HostApiContext;
const result = await invoke(ctx, 'GET', '/api/coding/plugins?projectId=local-a');
expect(result).toMatchObject({ handled: true, status: 200, payload: projection });
expect(list).toHaveBeenCalledWith('local-a');
expect(JSON.stringify(result.payload)).not.toMatch(/projectPath|owner|token|entitlement_scope/u);
});
it('accepts only the exact PUT body and never forwards authority fields', async () => {
const setEnabled = vi.fn().mockResolvedValue({
schemaVersion: 1,
project: { localProjectId: 'local-a', durableProjectId: null },
policyStatus: 'unavailable',
unknownPluginIds: [],
items: [],
});
const ctx = { codingProducts: { plugins: { setEnabled } } } as unknown as HostApiContext;
const accepted = await invoke(ctx, 'PUT', '/api/coding/plugins/makelore.data-service', {
projectId: 'local-a', enabled: true,
});
const rejected = await invoke(ctx, 'PUT', '/api/coding/plugins/makelore.data-service', {
projectId: 'local-a', enabled: true, projectPath: 'C:\\untrusted',
});
expect(accepted.status).toBe(200);
expect(setEnabled).toHaveBeenCalledOnce();
expect(setEnabled).toHaveBeenCalledWith('local-a', 'makelore.data-service', true);
expect(rejected).toMatchObject({ status: 400, payload: { success: false, code: 'plugin_request_invalid' } });
});
it('rejects duplicate or unexpected query authority before calling Main services', async () => {
const list = vi.fn();
const ctx = { codingProducts: { plugins: { list } } } as unknown as HostApiContext;
const duplicate = await invoke(ctx, 'GET', '/api/coding/plugins?projectId=local-a&projectId=local-b');
const authority = await invoke(ctx, 'GET', '/api/coding/plugins?projectId=local-a&durableProjectId=forged');
expect(duplicate.status).toBe(400);
expect(authority.status).toBe(400);
expect(list).not.toHaveBeenCalled();
});
});