Files
makelore/tests/unit/device-package-routes.test.ts

88 lines
3.8 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 { handleDevicePackageRoutes } from '../../electron/api/routes/device-packages';
import { DevicePackageError } from '../../electron/coding-packages/device-package-manager';
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 handleDevicePackageRoutes(
request(method, body), output.res, new URL(`http://localhost${target}`), ctx,
);
return { handled, status: output.status, payload: output.json() };
}
describe('device package routes', () => {
it('lists, enables, and removes device packages without exposing an install route', async () => {
const empty = { schemaVersion: 1, generation: 0, packages: [] };
const list = vi.fn().mockResolvedValue(empty);
const setEnabled = vi.fn().mockResolvedValue({ ...empty, generation: 1 });
const uninstall = vi.fn().mockResolvedValue({ ...empty, generation: 2 });
const ctx = { codingProducts: { devicePackages: { list, setEnabled, uninstall } } } as unknown as HostApiContext;
expect(await invoke(ctx, 'GET', '/api/coding/device-packages')).toMatchObject({
handled: true, status: 200, payload: empty,
});
expect(await invoke(ctx, 'PATCH', '/api/coding/device-packages/pi-tools', { enabled: false }))
.toMatchObject({ handled: true, status: 200, payload: { generation: 1 } });
expect(await invoke(ctx, 'DELETE', '/api/coding/device-packages/pi-tools', {}))
.toMatchObject({ handled: true, status: 200, payload: { generation: 2 } });
expect(setEnabled).toHaveBeenCalledWith('pi-tools', false);
expect(uninstall).toHaveBeenCalledWith('pi-tools');
const unsupported = response();
expect(await handleDevicePackageRoutes(
request('POST', { source: 'npm:pi-web-search' }), unsupported.res,
new URL('http://localhost/api/coding/device-packages'), ctx,
)).toBe(false);
});
it('rejects forged fields and returns only bounded manager errors', async () => {
const setEnabled = vi.fn();
const uninstall = vi.fn().mockRejectedValue(new DevicePackageError(
'local_package_in_use', 'Package is still active',
));
const ctx = { codingProducts: { devicePackages: {
list: vi.fn(), setEnabled, uninstall,
} } } as unknown as HostApiContext;
expect(await invoke(ctx, 'PATCH', '/api/coding/device-packages/pi-tools', {
enabled: false, source: 'C:\\forged',
})).toMatchObject({ status: 400, payload: { code: 'local_package_request_invalid' } });
expect(setEnabled).not.toHaveBeenCalled();
expect(await invoke(ctx, 'DELETE', '/api/coding/device-packages/pi-tools', {}))
.toMatchObject({ status: 409, payload: { code: 'local_package_in_use' } });
});
});