From 78d72cc4a9bb1c546bce79c17eed508a813be1af Mon Sep 17 00:00:00 2001 From: Codex Date: Wed, 18 Feb 2026 12:37:46 +0000 Subject: [PATCH] feat: add HTTP server adapter and request-mapping tests --- src/server.js | 79 +++++++++++++++++++++++++++++++++++++++++++++ test/server.test.js | 31 ++++++++++++++++++ 2 files changed, 110 insertions(+) create mode 100644 src/server.js create mode 100644 test/server.test.js diff --git a/src/server.js b/src/server.js new file mode 100644 index 0000000..9c597b8 --- /dev/null +++ b/src/server.js @@ -0,0 +1,79 @@ +"use strict"; + +const http = require("node:http"); +const { buildApp } = require("./app"); +const { config } = require("./config"); + +function readBody(req) { + return new Promise((resolve, reject) => { + let data = ""; + req.setEncoding("utf8"); + req.on("data", (chunk) => { + data += chunk; + if (data.length > 2_000_000) { + reject(new Error("payload_too_large")); + } + }); + req.on("end", () => resolve(data)); + req.on("error", reject); + }); +} + +function normalizeHeaders(rawHeaders) { + return Object.fromEntries( + Object.entries(rawHeaders || {}).map(([k, v]) => [ + String(k).toLowerCase(), + Array.isArray(v) ? v.join(",") : (v || ""), + ]), + ); +} + +function mapToAppRequest({ req, rawBody }) { + const url = new URL(req.url, "http://localhost"); + return { + method: req.method || "GET", + path: url.pathname, + headers: normalizeHeaders(req.headers), + rawBody, + }; +} + +function createHttpServer({ app }) { + return http.createServer(async (req, res) => { + try { + const rawBody = await readBody(req); + const response = app.handleRequest(mapToAppRequest({ req, rawBody })); + + res.statusCode = response.status; + for (const [key, value] of Object.entries(response.headers || {})) { + res.setHeader(key, value); + } + res.end(response.body || ""); + } catch (error) { + res.statusCode = 500; + res.setHeader("content-type", "application/json; charset=utf-8"); + res.end(JSON.stringify({ error: error.message || "internal_error" })); + } + }); +} + +function start() { + const app = buildApp({ config }); + const server = createHttpServer({ app }); + + server.listen(config.port, () => { + // eslint-disable-next-line no-console + console.log(`xartaudio server listening on :${config.port}`); + }); +} + +if (require.main === module) { + start(); +} + +module.exports = { + mapToAppRequest, + normalizeHeaders, + createHttpServer, + start, +}; diff --git a/test/server.test.js b/test/server.test.js new file mode 100644 index 0000000..89ebff2 --- /dev/null +++ b/test/server.test.js @@ -0,0 +1,31 @@ +"use strict"; + +const test = require("node:test"); +const assert = require("node:assert/strict"); +const { mapToAppRequest, normalizeHeaders } = require("../src/server"); + +test("normalizeHeaders lowercases and joins array values", () => { + const headers = normalizeHeaders({ + "X-User-Id": "u1", + "X-Forwarded-For": ["a", "b"], + }); + + assert.equal(headers["x-user-id"], "u1"); + assert.equal(headers["x-forwarded-for"], "a,b"); +}); + +test("mapToAppRequest extracts method/path/headers/body correctly", () => { + const request = mapToAppRequest({ + req: { + method: "POST", + url: "/api/webhooks/x?debug=1", + headers: { "X-Signature": "sha256=abc" }, + }, + rawBody: "{\"ok\":true}", + }); + + assert.equal(request.method, "POST"); + assert.equal(request.path, "/api/webhooks/x"); + assert.equal(request.headers["x-signature"], "sha256=abc"); + assert.equal(request.rawBody, "{\"ok\":true}"); +});