Files
crank/apps/ui/scripts/stream-fixture-server.js
T
2026-04-11 02:01:54 +03:00

235 lines
5.9 KiB
JavaScript

const crypto = require('crypto');
const http = require('http');
const PORT = Number(process.env.CRANK_E2E_STREAM_FIXTURE_PORT || 3310);
function writeJson(response, status, payload) {
response.writeHead(status, {
'Content-Type': 'application/json; charset=utf-8',
'Cache-Control': 'no-store',
});
response.end(JSON.stringify(payload));
}
function readJsonBody(request) {
return new Promise((resolve, reject) => {
let buffer = '';
request.setEncoding('utf8');
request.on('data', (chunk) => {
buffer += chunk;
});
request.on('end', () => {
if (!buffer) {
resolve({});
return;
}
try {
resolve(JSON.parse(buffer));
} catch (error) {
reject(error);
}
});
request.on('error', reject);
});
}
const server = http.createServer((request, response) => {
const url = new URL(request.url, `http://127.0.0.1:${PORT}`);
if (url.pathname === '/health') {
writeJson(response, 200, { ok: true });
return;
}
if (url.pathname === '/sse/logs') {
response.writeHead(200, {
'Content-Type': 'text/event-stream; charset=utf-8',
'Cache-Control': 'no-cache',
Connection: 'keep-alive',
});
const events = [
{ level: 'info', message: 'billing started', cursor: 'c1' },
{ level: 'warn', message: 'cache warmup slow', cursor: 'c2' },
{ level: 'error', message: 'invoice timeout', cursor: 'c3' },
];
let index = 0;
const timer = setInterval(() => {
if (index >= events.length) {
clearInterval(timer);
response.end();
return;
}
response.write('event: message\n');
response.write(`data: ${JSON.stringify(events[index])}\n\n`);
index += 1;
}, 150);
request.on('close', () => {
clearInterval(timer);
});
return;
}
if (url.pathname === '/snapshot/metrics') {
writeJson(response, 200, {
summary: {
service: 'billing',
error_rate: 0.12,
},
items: [
{ timestamp: '2026-04-06T10:00:00Z', cpu: 0.41, memory: 0.67 },
{ timestamp: '2026-04-06T10:00:05Z', cpu: 0.39, memory: 0.65 },
],
done: true,
});
return;
}
if (url.pathname === '/crm/leads' && request.method === 'POST') {
readJsonBody(request)
.then((payload) => {
writeJson(response, 200, {
id: 'lead_123',
email: payload.email || null,
});
})
.catch(() => {
writeJson(response, 400, { error: 'invalid_json' });
});
return;
}
if (url.pathname === '/soap/leads' && request.method === 'POST') {
let buffer = '';
request.setEncoding('utf8');
request.on('data', (chunk) => {
buffer += chunk;
});
request.on('end', () => {
const hasEmail = buffer.includes('<email>user@example.com</email>')
|| buffer.includes('<m:email>user@example.com</m:email>');
if (!hasEmail) {
response.writeHead(400, {
'Content-Type': 'text/xml; charset=utf-8',
'Cache-Control': 'no-store',
});
response.end([
'<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">',
'<soap:Body>',
'<soap:Fault>',
'<faultcode>Client</faultcode>',
'<faultstring>missing email</faultstring>',
'</soap:Fault>',
'</soap:Body>',
'</soap:Envelope>',
].join(''));
return;
}
response.writeHead(200, {
'Content-Type': 'text/xml; charset=utf-8',
'Cache-Control': 'no-store',
});
response.end([
'<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">',
'<soap:Body>',
'<CreateLeadResponse>',
'<id>lead_soap_123</id>',
'<status>created</status>',
'</CreateLeadResponse>',
'</soap:Body>',
'</soap:Envelope>',
].join(''));
});
request.on('error', () => {
response.writeHead(500, {
'Content-Type': 'text/plain; charset=utf-8',
'Cache-Control': 'no-store',
});
response.end('fixture_error');
});
return;
}
writeJson(response, 404, { error: 'not_found' });
});
function writeWebSocketFrame(socket, payload) {
const body = Buffer.from(payload, 'utf8');
const header = [];
header.push(0x81);
if (body.length < 126) {
header.push(body.length);
} else if (body.length < 65536) {
header.push(126, (body.length >> 8) & 0xff, body.length & 0xff);
} else {
throw new Error('fixture payload is unexpectedly large');
}
socket.write(Buffer.concat([Buffer.from(header), body]));
}
function acceptWebSocket(request, socket) {
const key = request.headers['sec-websocket-key'];
if (!key) {
socket.destroy();
return;
}
const accept = crypto
.createHash('sha1')
.update(`${key}258EAFA5-E914-47DA-95CA-C5AB0DC85B11`)
.digest('base64');
socket.write(
[
'HTTP/1.1 101 Switching Protocols',
'Upgrade: websocket',
'Connection: Upgrade',
`Sec-WebSocket-Accept: ${accept}`,
'\r\n',
].join('\r\n'),
);
const messages = [
{ type: 'tick', seq: 1, value: 101 },
{ type: 'tick', seq: 2, value: 102 },
{ type: 'tick', seq: 3, value: 103 },
];
let index = 0;
const timer = setInterval(() => {
if (index >= messages.length) {
clearInterval(timer);
socket.end();
return;
}
writeWebSocketFrame(socket, JSON.stringify(messages[index]));
index += 1;
}, 150);
socket.on('close', () => clearInterval(timer));
socket.on('end', () => clearInterval(timer));
socket.on('error', () => clearInterval(timer));
}
server.on('upgrade', (request, socket, head) => {
if (request.url !== '/events') {
socket.destroy();
return;
}
if (head && head.length) {
socket.unshift(head);
}
acceptWebSocket(request, socket);
});
server.listen(PORT, '127.0.0.1', () => {
console.log(`Streaming fixtures listening on http://127.0.0.1:${PORT}`);
});