n8n-nodes-teamorouter/test/mock-server.mjs

139 lines
5.2 KiB
JavaScript

// OpenAI-compatible stand-in for api.teamorouter.com — used by the node tests, no dependencies.
import { createServer } from 'node:http';
// 1x1 transparent PNG.
export const TINY_PNG_BASE64 =
'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8DwHwAFAAH/q842iQAAAABJRU5ErkJggg==';
export const MODELS = [
{ id: 'gpt-5.6-sol', object: 'model', owned_by: 'openai', pricing: { prompt: 0.00000053, completion: 0.00000318 } },
{ id: 'claude-sonnet-5', object: 'model', owned_by: 'anthropic', pricing: { prompt: 0.0000005, completion: 0.00000252 } },
{ id: 'deepseek-flash-free', object: 'model', owned_by: 'deepseek' },
{ id: 'gemini-3.1-pro-preview', object: 'model', owned_by: 'google', pricing: { input: 0.29, output: 1.73 } },
{ id: 'gpt-image-2', object: 'model', owned_by: 'openai' },
{ id: 'nano-banana-2.1', object: 'model', owned_by: 'google' },
];
const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
/**
* Models whose IDs start with "fail-" make the mock answer with the matching HTTP status,
* "slow-model" delays the answer, everything else echoes the user message back.
*/
export function createMockServer({ apiKey = 'test-key' } = {}) {
const requests = [];
const handle = async (request, response) => {
const chunks = [];
for await (const chunk of request) chunks.push(chunk);
const rawBody = Buffer.concat(chunks).toString('utf8');
let body;
try {
body = rawBody === '' ? undefined : JSON.parse(rawBody);
} catch {
body = undefined;
}
requests.push({ method: request.method, url: request.url, headers: request.headers, body });
const json = (status, payload) => {
response.writeHead(status, { 'content-type': 'application/json' });
response.end(JSON.stringify(payload));
};
if (!request.url.startsWith('/v1/')) {
return json(404, { error: { message: `unknown path ${request.url}` } });
}
if (request.headers.authorization !== `Bearer ${apiKey}`) {
return json(401, { error: { message: 'invalid api key' } });
}
if (request.method === 'GET' && request.url === '/v1/models') {
return json(200, { object: 'list', data: MODELS });
}
if (request.method === 'POST' && request.url === '/v1/chat/completions') {
const model = body?.model ?? '';
if (model.startsWith('fail-')) {
const status = Number(model.replace('fail-', ''));
return json(status, { error: { message: `forced failure ${status}` } });
}
if (model === 'slow-model') await delay(2500);
const userMessage = [...(body?.messages ?? [])].reverse().find((message) => message.role === 'user');
const content = `echo: ${userMessage?.content ?? ''}`;
if (body?.stream === true) {
response.writeHead(200, { 'content-type': 'text/event-stream' });
response.write(
`data: ${JSON.stringify({ object: 'chat.completion.chunk', choices: [{ index: 0, delta: { content } }] })}\n\n`,
);
response.write('data: [DONE]\n\n');
return response.end();
}
return json(200, {
object: 'chat.completion',
model,
choices: [{ index: 0, message: { role: 'assistant', content }, finish_reason: 'stop' }],
usage: { prompt_tokens: 11, completion_tokens: 4, total_tokens: 15 },
});
}
// LangChain's ChatOpenAI serves "gpt-5.6*" and "codex" model IDs through the Responses API.
if (request.method === 'POST' && request.url === '/v1/responses') {
const userMessage = (body?.input ?? []).find?.((message) => message.role === 'user');
const content = `echo: ${typeof body?.input === 'string' ? body.input : userMessage?.content ?? ''}`;
return json(200, {
id: 'resp_mock',
object: 'response',
created_at: 1,
status: 'completed',
model: body?.model,
output: [
{
type: 'message',
id: 'msg_mock',
role: 'assistant',
status: 'completed',
content: [{ type: 'output_text', text: content, annotations: [] }],
},
],
usage: { input_tokens: 11, output_tokens: 4, total_tokens: 15 },
});
}
if (request.method === 'POST' && request.url === '/v1/images/generations') {
if (String(body?.prompt ?? '').includes('noimage')) {
return json(400, { error: { message: 'prompt rejected' } });
}
return json(200, { created: 1, data: [{ b64_json: TINY_PNG_BASE64 }] });
}
return json(404, { error: { message: `unknown route ${request.method} ${request.url}` } });
};
const server = createServer((request, response) => {
handle(request, response).catch((error) => {
response.writeHead(500, { 'content-type': 'application/json' });
response.end(JSON.stringify({ error: { message: String(error) } }));
});
});
return {
requests,
/** Starts on `port` (0 = random) and returns the base URL, e.g. http://127.0.0.1:4123 */
async listen(port = 0) {
await new Promise((resolve) => server.listen(port, '127.0.0.1', resolve));
return `http://127.0.0.1:${server.address().port}`;
},
async close() {
await new Promise((resolve) => server.close(resolve));
},
};
}
if (process.argv[1]?.endsWith('mock-server.mjs') === true) {
const mock = createMockServer({ apiKey: process.env.MOCK_API_KEY ?? 'test-key' });
const baseUrl = await mock.listen(Number(process.env.PORT ?? 4123));
console.log(`mock TeamoRouter listening on ${baseUrl}/v1 (api key: ${process.env.MOCK_API_KEY ?? 'test-key'})`);
}