#!/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.'); });