88 lines
3.8 KiB
TypeScript
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' } });
|
|
});
|
|
});
|