#!/usr/bin/env node
/**
* conductor-mock-server.js
*
* A minimal HTTP mock server for local Conductor development and validation.
* It echoes request bodies through routes that mimic the public HTTPBin API,
* so Conductor project examples can run fully offline or from within Docker.
*
* Usage:
* node examples/mock-server/conductor-mock-server.js
*
* Listens on:
* http://localhost:8787 (host access)
* http://host.docker.internal:8787 (Docker container access on Mac/Win)
*
* Routes:
* * /anything — parse JSON body, return { json:
, method, url, headers }
* GET /get — return { args: , method, url, headers }
* * /delay/1 — wait one second, then return the /anything response
* * /status/503 — return the requested HTTP status with a JSON body
* GET /environments — deterministic dependent-data options
* GET /inventory — deterministic dashboard rows
* GET /inventory/:id — deterministic selected-row details
* POST /launch — deterministic workflow-launch response
* * — 404 JSON error
*/
'use strict';
const http = require('http');
const url = require('url');
const PORT = 8787;
const HOST = '0.0.0.0';
// ── Request-body reader ───────────────────────────────────────────────────────
function readBody(req) {
return new Promise((resolve, reject) => {
const chunks = [];
req.on('data', (chunk) => chunks.push(chunk));
req.on('end', () => resolve(Buffer.concat(chunks).toString('utf8')));
req.on('error', reject);
});
}
// ── Response helpers ──────────────────────────────────────────────────────────
function sendJson(res, status, body) {
const payload = JSON.stringify(body, null, 2);
res.writeHead(status, {
'Content-Type': 'application/json',
'Content-Length': Buffer.byteLength(payload),
'Access-Control-Allow-Origin': '*',
});
res.end(payload);
}
// ── Route handlers ────────────────────────────────────────────────────────────
async function handleAnything(req, res) {
// Parse body (if any)
let parsedJson = null;
const rawBody = await readBody(req);
if (rawBody.trim()) {
try {
parsedJson = JSON.parse(rawBody);
} catch (e) {
sendJson(res, 400, {
error: 'invalid_json',
message: `Request body is not valid JSON: ${e.message}`,
});
return;
}
}
const parsed = url.parse(req.url, true);
sendJson(res, 200, {
method: req.method,
url: req.url,
args: parsed.query,
headers: req.headers,
json: parsedJson,
data: rawBody || '',
});
}
function handleGet(req, res) {
const parsed = url.parse(req.url, true);
sendJson(res, 200, {
method: req.method,
url: req.url,
args: parsed.query,
headers: req.headers,
});
}
const environments = [
{ label: 'Development', value: 'dev' },
{ label: 'Production', value: 'prod' },
];
const inventory = [
{ id: 'srv-101', hostname: 'alpha.example', environment: 'dev', status: 'ready' },
{ id: 'srv-202', hostname: 'bravo.example', environment: 'prod', status: 'maintenance' },
];
// ── Server ────────────────────────────────────────────────────────────────────
const server = http.createServer(async (req, res) => {
// Strip query string for routing
const pathname = url.parse(req.url).pathname;
// OPTIONS preflight (CORS)
if (req.method === 'OPTIONS') {
res.writeHead(204, { 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Headers': '*' });
res.end();
return;
}
try {
if (pathname === '/anything') {
await handleAnything(req, res);
} else if (/^\/delay\/\d+$/.test(pathname)) {
const seconds = Math.min(Number(pathname.split('/')[2]), 5);
await new Promise((resolve) => setTimeout(resolve, seconds * 1000));
await handleAnything(req, res);
} else if (/^\/status\/\d{3}$/.test(pathname)) {
const status = Number(pathname.split('/')[2]);
sendJson(res, status, { status, message: `Controlled HTTP ${status} response` });
} else if (pathname === '/get' && req.method === 'GET') {
handleGet(req, res);
} else if (pathname === '/environments' && req.method === 'GET') {
sendJson(res, 200, { environments });
} else if (pathname === '/inventory' && req.method === 'GET') {
sendJson(res, 200, { items: inventory });
} else if (/^\/inventory\/[^/]+$/.test(pathname) && req.method === 'GET') {
const id = decodeURIComponent(pathname.split('/')[2]);
const item = inventory.find((candidate) => candidate.id === id);
if (item) sendJson(res, 200, { item });
else sendJson(res, 404, { error: 'not_found', message: `No inventory item ${id}` });
} else if (pathname === '/launch' && req.method === 'POST') {
const rawBody = await readBody(req);
let request;
try { request = JSON.parse(rawBody || '{}'); }
catch { sendJson(res, 400, { error: 'invalid_json' }); return; }
sendJson(res, 200, {
accepted: true,
requestId: 'req-demo-001',
environment: request.environment,
hostname: request.hostname,
});
} else {
sendJson(res, 404, {
error: 'not_found',
message: `No mock route for ${req.method} ${pathname}. ` +
`Available: POST /anything, GET /get`,
});
}
} catch (err) {
sendJson(res, 500, {
error: 'internal_error',
message: err instanceof Error ? err.message : String(err),
});
}
});
server.listen(PORT, HOST, () => {
console.log(`Conductor mock server listening on http://${HOST}:${PORT}`);
console.log(' POST /anything — echoes JSON body as { json: }');
console.log(' GET /get — echoes query parameters as { args: }');
console.log(' * /delay/1 — controlled one-second response delay');
console.log(' * /status/503 — controlled upstream failure');
console.log('Press Ctrl+C to stop.');
});