import assert from "node:assert/strict"; import child_process from "node:child_process"; import fs from "node:fs"; import http from "node:http"; import procees from "node:process"; import * as runner from "../runner.mjs"; import * as u from "../../src/utils.mjs"; import { normalizeSegments, pathToSegments, hasPathParams, addRoute, findStaticHandler, firstParamMatch, findDynamicHandler, findHandler, extractQueryParams, handleRequest, makeRequestListener, loggerDefaults, loggerGlobals, configLogger, logit, makeLogger, interceptorsFn, interceptors, defaultInterceptors, chainInterceptors, wrapHandler, actionsFn, lineHandlerFn, rmIf, mkfifo, buildRoutes, promisifyServer, buildServer, } from "../../src/hero.mjs"; const test_normalizeSegments = async t => { t.start("normalizeSegments()"); await t.test("unchanged when already normalized", () => { assert.deepEqual(normalizeSegments([""]), [""]); }); await t.test("empty terminator added when missing", () => { assert.deepEqual(normalizeSegments([]), [""]); assert.deepEqual(normalizeSegments([" "]), [" ", ""]); assert.deepEqual(normalizeSegments(["_"]), ["_", ""]); assert.deepEqual(normalizeSegments(["a"]), ["a", ""]); assert.deepEqual(normalizeSegments([":a"]), [":a", ""]); assert.deepEqual(normalizeSegments(["path"]), ["path", ""]); }); }; const test_pathToSegments = async t => { t.start("pathToSegments()"); await t.test("simple paths", () => { assert.deepEqual(pathToSegments("/"), [ "" ]); assert.deepEqual(pathToSegments("/simple"), ["simple", ""]); assert.deepEqual(pathToSegments("/simple/route"), ["simple", "route", ""]); assert.deepEqual(pathToSegments("/api/user/:id"), ["api", "user", ":id", ""]); assert.deepEqual(pathToSegments("/api/user/:id/info"), ["api", "user", ":id", "info", ""]); }); await t.test("extra separators", () => { assert.deepEqual(pathToSegments("/api/health/"), ["api", "health", ""]); assert.deepEqual(pathToSegments("/api///health"), ["api", "health", ""]); assert.deepEqual(pathToSegments("//api///health//"), ["api", "health", ""]); }); }; const test_hasPathParams = async t => { t.start("hasPathParam()"); await t.test("has it", () => { assert(hasPathParams(["some", ":path", ""])); assert(hasPathParams(["path", ":params", ""])); assert(hasPathParams(["api", "user", ":id", "info", ""])); }); await t.test("doesn't have it", () => { assert(!hasPathParams([])); assert(!hasPathParams([ "" ])); assert(!hasPathParams(["some", "path", ""])); assert(!hasPathParams(["s:o:m:e:", "p::ath:::"])); assert(!hasPathParams([" :"])); }); }; const test_addRoute = async t => { t.start("addRoute()"); const fn1 = () => {}; await t.test("daily usage examples", () => { assert.deepEqual( addRoute({}, "GET", "/home", fn1), { static: { GET: { home: { "": fn1 }}}}, ); assert.deepEqual( addRoute({}, ["PUT", "POST", "PATCH"], "/api/user", fn1), { static: { PUT: { api: { user: { "": fn1 }}}, POST: { api: { user: { "": fn1 }}}, PATCH: { api: { user: { "": fn1 }}}, }, }, ); assert.deepEqual( addRoute({}, "*", "/settings", fn1), { static: { GET: { settings: { "": fn1 }}, HEAD: { settings: { "": fn1 }}, POST: { settings: { "": fn1 }}, PUT: { settings: { "": fn1 }}, PATCH: { settings: { "": fn1 }}, DELETE: { settings: { "": fn1 }}, OPTIONS: { settings: { "": fn1 }}, }, }, ); assert.deepEqual( addRoute({}, "GET", "/", fn1), { static: { GET: { "": fn1 }}}, ); assert.deepEqual( addRoute({}, "GET", "/api/user/:id", fn1), { dynamic: { GET: { api: { user: { ":id": { "": fn1 }}}}}}, ); assert.deepEqual( addRoute({}, ["PUT", "PATCH"], "/api/org/:orgid/member/:memberid", fn1), { dynamic: { PUT: { api: { org: { ":orgid": { member: { ":memberid": { "": fn1 }}}}}}, PATCH: { api: { org: { ":orgid": { member: { ":memberid": { "": fn1 }}}}}}, }, }, ); }); await t.test("bad method", () => { assert.throws( () => addRoute({}, "VERB", "/path", fn1), assert.AssertionError, ); }); await t.test("empty methods array", () => { assert.deepEqual(addRoute({}, [], "", fn1), {}); }); await t.test("subpaths", () => { const fn1 = () => {}; const fn2 = () => {}; const t1 = addRoute({}, "GET", "/home", fn1); const t2 = addRoute(t1, "GET", "/home/details", fn2); assert.deepEqual( t2, { static: { GET: { home: { "": fn1, details: { "": fn2, }, }, }, }, }, ); }); }; const test_findStaticHandler = async t => { t.start("findStaticHandler()"); await t.test("multiple accesses to the same table", () => { const fn1 = () => {}; const fn2 = () => {}; const fn3 = () => {}; const table = { static: { GET: { api: { home: { "": fn1 }, settings: { "": fn2 }, }, }, POST: { api: { settings: { "": fn3 }, }, }, }, }; assert.deepEqual( findStaticHandler(table, "GET", [ "api", "home", "" ]), { handlerFn: fn1, params: {} }, ); assert.deepEqual( findStaticHandler(table, "PUT", [ "api", "home", "" ]), null, ); assert.deepEqual( findStaticHandler(table, "GET", [ "api", "settings", "" ]), { handlerFn: fn2, params: {} }, ); assert.deepEqual( findStaticHandler(table, "POST", [ "api", "settings", "" ]), { handlerFn: fn3, params: {} }, ); assert.deepEqual( findStaticHandler(table, "PUT", [ "api", "settings", "" ]), null, ); assert.deepEqual( findStaticHandler(table, "GET", [ "api", "profile", "" ]), null, ); assert.deepEqual( findStaticHandler({}, "GET", [ "api", "profile", "" ]), null, ); }); }; const test_firstParamMatch = async t => { t.start("firstParamMatch()"); const params = {}; const fn1 = () => {}; const fn2 = () => {}; await t.test("we BACKTRACK when searching down routes", () => { const segments = [ "path", "split", "match", "" ]; const tree = { path: { split: { MISMATCH: { "": fn1, }, }, ":param": { match: { "": fn2, }, }, }, }; assert.deepEqual( firstParamMatch(tree, segments, params), { handlerFn: fn2, params: { param: "split" }}, ); }); await t.test("ambiguous route prefers params at the end", () => { const segments = [ "path", "param1", "param2", "" ]; const tree1 = { path: { ":shallower": { param2: { "": fn2, }, }, }, }; const tree2 = u.assocIn(tree1, [ "path", "param1", ":deeper", "" ], fn1); assert.deepEqual( firstParamMatch(tree1, segments, params), { handlerFn: fn2, params: { shallower: "param1" }}, ); assert.deepEqual( firstParamMatch(tree2, segments, params), { handlerFn: fn1, params: { deeper: "param2" }}, ); }); await t.test("when 2 params are possible, we pick the first alphabetically", () => { const segments = [ "user", "someId", "" ]; const tree1 = { user: { ":bbb": { "": fn2, }, }, }; const tree2 = u.assocIn(tree1, ["user", ":aaa", ""], fn1); assert.deepEqual( firstParamMatch(tree1, segments, params), { handlerFn: fn2, params: { bbb: "someId" }}, ); assert.deepEqual( firstParamMatch(tree2, segments, params), { handlerFn: fn1, params: { aaa: "someId" }}, ); }); await t.test(`we deal with segments that start with ":"`, () => { const segments = [ "path", ":param", "" ]; const tree = { path: { ":arg": { "": fn1, }, }, }; assert.deepEqual( firstParamMatch(tree, segments, params), { handlerFn: fn1, params: { arg: ":param" }}, ); }); }; const test_findDynamicHandler = async t => { t.start("findDynamicHandler()"); await t.test("daily usage cases", () => { const fn1 = () => {}; const fn2 = () => {}; const table = { dynamic: { GET: { users: { "by-id": { ":id": { "": fn1 }, }, }, }, PUT: { user: { ":user-id": { info: { "": fn2 }, }, }, }, }, }; assert.deepEqual( findDynamicHandler(table, "GET", [ "users", "by-id", "123", "" ]), { handlerFn: fn1, params: { "id": "123" }}, ); assert.deepEqual( findDynamicHandler({}, "GET", [ "users", "by-id", "123", "" ]), null, ); assert.deepEqual( findDynamicHandler({ dynamic: { GET: { "": fn1 }}}, "GET", [ "users", "by-id", "123", "" ]), null, ); assert.deepEqual( findDynamicHandler(table, "PUT", [ "user", "deadbeef", "info", "" ]), { handlerFn: fn2, params: { "user-id": "deadbeef" }}, ); }); }; const test_findHandler = async t => { t.start("findHandler()"); await t.test("mix of static and dynamic routes", () => { const static1 = () => {}; const static2 = () => {}; const static3 = () => {}; const dynamic1 = () => {}; const dynamic2 = () => {}; const dynamic3 = () => {}; const dynamic4 = () => {}; const table = { static: { GET: { user: { "": static1, }, pages: { "": static2, home: { "": static3, }, }, }, }, dynamic: { GET: { user: { ":id": { "": dynamic1, }, }, }, PUT: { user: { ":id": { "": dynamic2, "info": { "": dynamic3, }, "preferences": { "": dynamic4, }, }, }, }, }, }; assert.deepEqual( findHandler(table, "GET", "/"), null, ); assert.deepEqual( findHandler(table, "GET", "/user/"), { handlerFn: static1, params: {} }, ); assert.deepEqual( findHandler(table, "GET", "/pages"), { handlerFn: static2, params: {} }, ); assert.deepEqual( findHandler(table, "GET", "/pages/home/"), { handlerFn: static3, params: {} }, ); assert.deepEqual( findHandler(table, "GET", "/user/some-id"), { handlerFn: dynamic1, params: { id: "some-id" }}, ); assert.deepEqual( findHandler(table, "GET", "/user/other-id/info"), null, ); assert.deepEqual( findHandler(table, "PUT", "/user/other-id/info"), { handlerFn: dynamic3, params: { id: "other-id" }}, ); assert.deepEqual( findHandler(table, "PUT", "/user/another-id/preferences"), { handlerFn: dynamic4, params: { id: "another-id" }}, ); assert.deepEqual( findHandler(table, "POST", "/user/another-id/preferences"), null, ); }); }; const test_extractQueryParams = async t => { t.start("extractQueryParams()"); await t.test("empty values", () => { assert.deepEqual(extractQueryParams(), {}); assert.deepEqual(extractQueryParams(null), {}); assert.deepEqual(extractQueryParams(undefined), {}); }); await t.test("we get a flat key-value strings", () => { assert.deepEqual( extractQueryParams("a[]=1&b=text&c=___"), { "a[]": "1", b: "text", c: "___", }, ); }); await t.test("duplicate values are suppressed, deterministically", () => { assert.deepEqual(extractQueryParams("a=1&a=2&a=3"), { a: "3" }); assert.deepEqual(extractQueryParams("a=1&b=2&a=3"), { a: "3", b: "2" }); }); }; const test_handleRequest = async t => { t.start("handleRequest()"); const fn = req => req; await t.test("request without params", async () => { const table = { static: { GET: { "": fn, }, }, }; assert.deepEqual( await handleRequest(table, "GET", "/?q=1"), { params: { path: {}, query: { q: "1", }, }, method: "GET", path: "/", handler: fn, }, ); }); await t.test("request with params", async () => { const table = { dynamic: { PUT: { api: { user: { ":userId": { "": fn, }, }, }, }, }, }; assert.deepEqual( await handleRequest(table, "PUT", "/api/user/2222"), { params: { path: { userId: "2222", }, query: {}, }, method: "PUT", path: "/api/user/2222", handler: fn, }, ); }); await t.test("missing route", async () => { assert.deepEqual( await handleRequest({}, "GET", "/"), { status: 404, body: "Not Found\n", }, ); }); }; const test_makeRequestListener = async t => { t.start("makeRequestListener()"); await t.test("straightforward body execution", async () => { const fn = _ => ({ status: "test status", body: "test body", headers: "test headers", }); const routes = [[ "GET", "/route1", fn ]]; const requestListener = makeRequestListener(buildRoutes(routes)); const req = { method: "GET", url: "/route1", }; const heads = []; const bodies = []; const res = { writeHead: (status, headers) => heads.push({ status, headers }), end: body => bodies.push(body), }; await requestListener(req, res); assert.deepEqual( heads, [{ status: "test status", headers: "test headers", }], ); assert.deepEqual( bodies, [ "test body" ], ); }); await t.test("we break if handleRequest() throws an error", async () => { const fn = _ => { throw new Error("handler error"); }; const routes = [[ "GET", "/route2", fn ]]; const requestListener = makeRequestListener(buildRoutes(routes)); const req = { method: "GET", url: "/route2", }; const heads = []; const bodies = []; const res = { writeHead: (status, headers) => heads.push({ status, headers }), end: body => bodies.push(body), }; await assert.rejects( async () => await requestListener(req, res), { message: "handler error" }, ); assert.deepEqual(heads, []); assert.deepEqual(bodies, []); }); }; const test_configLogger = async t => { t.start("configLogger()"); await t.test("globals starts empty", () => { assert.deepEqual(loggerGlobals, {}); }); await t.test("is gets becomes we assign it", () => { const globals = { app: "my-app", color: "green", version: "deadbeef", }; configLogger(globals); assert.deepEqual(loggerGlobals, globals); }); await t.test("we can reset it", () => { configLogger({}); assert.deepEqual(loggerGlobals, {}); }); }; const test_logit = async t => { t.start("logit()"); t.test("we can log data", () => { configLogger({ app: "hero-based app" }); const contents = []; const writerFn = x => contents.push(x); logit(writerFn, "my level", { a: 1, type: "log-test" }); assert.deepEqual(contents.map(JSON.parse), [{ ...loggerDefaults, app: "hero-based app", level: "my level", a: 1, type: "log-test", }]); // reset the logger out of the values specific to this test configLogger({}); }); await t.test("the object can change previous fallback values", () => { const contents = []; const writerFn = x => contents.push(x); configLogger({ level: "unseen", a: "unseen", }); logit(writerFn, "overwritten by level", { a: "overwritten by o" }); configLogger({ pid: "overwritten by loggerGlobals", }); logit(writerFn, "unseen", { level: "overwritten by o" }); // reset the logger out of the values specific to this test configLogger({}); assert.deepEqual(contents.map(JSON.parse), [ { ...loggerDefaults, pid: process.pid, level: "overwritten by level", a: "overwritten by o", }, { ...loggerDefaults, pid: "overwritten by loggerGlobals", level: "overwritten by o", }, ]); }); await t.test("we can't log unserializable things", () => { const obj = { self: null }; obj.self = obj; assert.throws(() => logit(obj), TypeError); }); }; const test_makeLogger = async t => { t.start("makeLogger()"); await t.test("various log levels", () => { const contents = []; const writerFn = x => contents.push(x); const log = makeLogger(writerFn); log.info ({ type: "expected" }); log.warn ({ type: "worrysome" }); log.error({ type: "bad" }); assert.deepEqual(contents.map(JSON.parse), [ { ...loggerDefaults, level: "INFO", type: "expected", }, { ...loggerDefaults, level: "WARN", type: "worrysome", }, { ...loggerDefaults, level: "ERROR", type: "bad", }, ]); }); await t.test("debug only works when $DEBUG is set", () => { const contents = []; const writerFn = x => contents.push(x); const log = makeLogger(writerFn); const previous = process.env.DEBUG; delete process.env.DEBUG; log.debug({ x: "ignored" }); process.env.DEBUG = "1"; log.debug({ x: "seen" }); delete process.env.DEBUG; // call the function that toggles log.debug({ x: "ignored" }); assert.deepEqual(contents.map(JSON.parse), [{ ...loggerDefaults, level: "DEBUG", x: "seen", }]); process.env.DEBUG = previous; }); }; const test_interceptorsFn = async t => { const next = x => x; { t.start("interceptorsFn().requestId()"); let i = 0; const uuidFn = () => `${i++}`; await t.test("we add an id to whatever we receive", () => { assert.deepEqual( interceptorsFn({uuidFn}).requestId({}, next), { id: "0", }, ); assert.deepEqual( interceptorsFn({uuidFn}).requestId({ a: "existing data" }, next), { a: "existing data", id: "1", }, ); }); await t.test(`we overwrite the "id" if it already exists`, async () => { assert.deepEqual( interceptorsFn({uuidFn}).requestId({ id: "before" }, next), { id: "2", }, ); }); }; { t.start("interceptorsFn().logged()"); await t.test("we log before and after next()", async () => { const contents = []; const logger = { info: x => contents.push(x) }; const status = 201; const req = { id: "an ID", url: "a URL", method: "a method", }; assert.deepEqual( await interceptorsFn({logger}).logged(req, _ => ({ status })), { status }, ); assert.deepEqual( contents, [ { ...req, type: "in-request" }, { id: req.id, status, type: "in-response" }, ], ); }); }; { t.start("interceptorsFn().contentType()"); await t.test("empty values", async () => { assert.deepEqual( await interceptorsFn().contentType({}, next), { body: "", headers: { "Content-Type": "application/json", "Content-Length": 0, }, }, ); assert.deepEqual( await interceptorsFn().contentType({ body: "" }, next), { body: "", headers: { "Content-Type": "text/html", "Content-Length": 0, }, }, ); }); await t.test("body values", async () => { assert.deepEqual( await interceptorsFn().contentType({ body: { a: 1 }}, next), { body: `{"a":1}`, headers: { "Content-Type": "application/json", "Content-Length": 7, }, }, ); assert.deepEqual( await interceptorsFn().contentType({ body: "
" }, next), { body: "
", headers: { "Content-Type": "text/html", "Content-Length": 6, }, }, ); }); await t.test("header values preference", async () => { assert.deepEqual( await interceptorsFn().contentType({ body: "", headers: { "Content-Type": "we have preference", "Content-Length": "and so do we", }, }, next), { body: "", headers: { "Content-Type": "we have preference", "Content-Length": "and so do we", }, }, ); }); await t.test("headers get propagated", async () => { assert.deepEqual( await interceptorsFn().contentType({ body: "", headers: { "My": "Header", }, }, next), { body: "", headers: { "My": "Header", "Content-Type": "text/html", "Content-Length": 0, }, }, ); }); }; { t.start("interceptorsFn().serverError()"); await t.test("no-op when no error occurs", async () => { assert.deepEqual( await interceptorsFn().serverError({ status: 1 }, next), { status: 1 }, ); }); await t.test(`an error is thrown if "status" is missing`, async () => { const contents = []; const logger = { error: x => contents.push(x) }; assert.deepEqual( await interceptorsFn({ logger }).serverError({ id: 123 }, next), { status: 500, body: "Internal Server Error\n", }, ); assert.deepEqual( contents, [{ id: 123, type: "server-error-interceptor", message: `Missing "status"`, }], ); }); await t.test("we turn a handler error into a 500 response", async () => { const contents = []; const logger = { error: x => contents.push(x) }; assert.deepEqual( await interceptorsFn({ logger }).serverError( { id: "some ID" }, _ => { throw new Error("My test error message"); }, ), { status: 500, body: "Internal Server Error\n", }, ); assert.deepEqual( contents, [{ id: "some ID", type: "server-error-interceptor", message: "My test error message", }], ); }); }; }; const test_chainInterceptors = async t => { t.start("chainInterceptors()"); await t.test("empty values", () => { assert.equal(chainInterceptors([])("req"), "req"); }); await t.test("the order of interceptors matter", () => { const a = []; const i1 = (req, next) => { a.push("i1"); return next(req); }; const i2 = (req, next) => { a.push("i2"); return next(req); }; assert.equal(chainInterceptors([i1, i2])("req"), "req"); assert.deepEqual(a, ["i1", "i2"]); assert.equal(chainInterceptors([i2, i1])("req"), "req"); assert.deepEqual(a, ["i1", "i2", "i2", "i1"]); }); await t.test("with ordering, interceptors implicitly depend on each other", () => { const i1 = (req, next) => next({ ...req, id: 1 }); const i2 = (req, next) => next({ ...req, id: req.id + 1 }); assert.deepEqual( chainInterceptors([i1, i2])({}), { id: 2 }, ); assert.deepEqual( chainInterceptors([i2])({}), { id: NaN }, ); assert.deepEqual( chainInterceptors([i2, i1])({}), { id: 1 }, ); }); await t.test("we can chain async interceptors", async () => { const i1 = async (req, next) => await next({ ...req, i1: true }); const i2 = async (req, next) => await next({ ...req, i2: true }); assert.deepEqual( await chainInterceptors([i1, i2])({}), { i1: true, i2: true, }, ); }); }; const test_wrapHandler = async t => { t.start("wrapHandler()"); await t.test("noop when arr is empty", () => { const fn = () => {}; const wrappedFn1 = wrapHandler(fn, []); assert.deepEqual(fn, wrappedFn1); const wrappedFn2 = wrapHandler(fn, [ interceptors.requestId ]); assert.notDeepEqual(fn, wrappedFn2); }); await t.test("a handler with chained interceptors change its behaviour", async () => { let i = 0; const uuidFn = () => `${i++}`; const contents = []; const logger = { info: x => contents.push(x) }; const interceptors = interceptorsFn({uuidFn, logger}); const fn = async _ => await { status: 1, body: { a: 1 }}; const wrappedFn = wrapHandler(fn, [ interceptors.requestId, interceptors.logged, interceptors.contentType, ]); const req = { url: "URL", method: "METHOD", }; assert.deepEqual( await fn(req), { status: 1, body: { a: 1 }}, ); assert.deepEqual(contents, []); assert.deepEqual( await wrappedFn(req), { status: 1, body: `{"a":1}`, headers: { "Content-Type": "application/json", "Content-Length": 7, }, }, ); assert.deepEqual( contents, [ { id: "0", url: "URL", method: "METHOD", type: "in-request", }, { id: "0", status: 1, type: "in-response", }, ], ); }); }; const test_actionsFn = async t => { { t.start(`actionsFn()["toggle-debug-env"()]`); await t.test("we can toggle back and forth", () => { const contents = []; const logger = { info: x => contents.push(x) }; const actions = actionsFn({logger}); const previous = process.env.DEBUG; delete process.env.DEBUG; actions["toggle-debug-env"]("action-text-1"); assert.equal(process.env.DEBUG, "1"); actions["toggle-debug-env"]("action-text-2"); assert.equal(process.env.DEBUG, undefined); assert.deepEqual(contents, [ { type: "action-response", action: "action-text-1", message: "toggle process.env.DEBUG", before: null, after: "1", }, { type: "action-response", action: "action-text-2", message: "toggle process.env.DEBUG", before: "1", after: null, }, ]); process.env.DEBUG = previous; }); }; { t.start(`actionsFn()["config-dump"]()`); await t.test("we just dump data as a log entry", () => { const contents = []; const logger = { info: x => contents.push(x) }; const actions = actionsFn({logger}); configLogger({}); actions["config-dump"]("first-call"); configLogger({ some: "thing", }); actions["config-dump"]("second-call"); configLogger({}); assert.deepEqual(contents, [ { type: "action-response", action: "first-call", data: { pid: process.pid, ppid: process.ppid, tool: "hero", }, }, { type: "action-response", action: "second-call", data: { pid: process.pid, ppid: process.ppid, tool: "hero", some: "thing", }, }, ]); }); }; }; const test_lineHandlerFn = async t => { t.start("lineHandlerFn()"); await t.test("empty values", () => { const contents = []; const logger = { info: x => contents.push(x) }; const lineHandler = lineHandlerFn({logger, actionsMap: {}}); lineHandler(""); lineHandler("{}"); lineHandler(`{"action": "this-action-does-not-exist"}`); assert.deepEqual(contents.map(x => x.type), [ "invalid-cmd-input", "missing-key-action", "unsupported-action", ]); }); await t.test("calling an action", () => { const contents = []; const logger = { info: x => contents.push(x) }; const lineHandler = lineHandlerFn({ logger: null, actionsMap: { "an-action": (arg1, arg2, arg3) => [arg1, arg2, arg3], }}); const ret1 = lineHandler(`{"action": "an-action"}`); const ret2 = lineHandler(`{"action": "an-action", "args": [1, "text", 2]}`); assert.deepEqual(ret1, ["an-action", undefined, undefined]); assert.deepEqual(ret2, ["an-action", 1, "text"]); }); }; const test_rmIf = async t => { t.start("rmIf()"); const path = "tests/hero-0.txt"; await t.test("rm when exists", async () => { fs.writeFileSync(path, " ", { flush: true }); assert.ok(fs.existsSync(path)); rmIf(path); assert.ok(!fs.existsSync(path)); }); await t.test("noop otherwise", async () => { assert.ok(!fs.existsSync(path)); rmIf(path); assert.ok(!fs.existsSync(path)); }); }; const test_mkfifo = async t => { t.start("mkfifo()"); await t.test("invalid paths", () => { assert.throws( () => mkfifo("tests/this/dir/does/not/exist/file.fifo"), { status: 1 }, ); assert.throws( () => mkfifo(""), { status: 1 }, ); }); await t.test("error when path already exists", async () => { const path = "tests/hero-0.pipe" fs.writeFileSync(path, " ", { flush: true }); const stats = fs.statSync(path); assert.ok(!stats.isFIFO()); assert.throws( () => mkfifo(path), { status: 1 }, ); }); await t.test("new pipe file", async () => { const path = "tests/hero-1.pipe" rmIf(path); assert.ok(!fs.existsSync(path)); mkfifo(path); assert.ok(fs.existsSync(path)); const stats = fs.statSync(path); assert.ok(stats.isFIFO()); }); }; const test_buildRoutes = async t => { t.start("buildRoutes()"); await t.test("empty values", () => { assert.deepEqual(buildRoutes([]), {}); }); await t.test("overwrites", () => { const fn1 = () => {}; const fn2 = () => {}; const r1 = [ [ "GET", "/", fn1 ] ]; const r2 = [ [ "GET", "/", fn2 ] ]; assert.deepEqual( buildRoutes(r1), { static: { GET: { "": fn1, }, }, }, ); assert.deepEqual( buildRoutes(r2), { static: { GET: { "": fn2, }, }, }, ); assert.deepEqual( buildRoutes(r1.concat(r2)), buildRoutes(r2), ); assert.deepEqual( buildRoutes(r2.concat(r1)), buildRoutes(r1), ); }); await t.test("wrapped handler functions", async () => { const handler = req => ({ ...req, handled: true }); const interceptor = (req, next) => next({ ...req, intercepted: true }); const routes = [ [ "GET", "/without", handler ], [ "GET", "/with", handler, [ interceptor ] ], ]; const table = buildRoutes(routes); { const { handled, intercepted } = await handleRequest(table, "GET", "/without"); assert.deepEqual( { handled, intercepted }, { handled: true, intercepted: undefined }, ); }; { const { handled, intercepted } = await handleRequest(table, "GET", "/with"); assert.deepEqual( { handled, intercepted }, { handled: true, intercepted: true }, ); }; }); await t.test("interceptors are combined", async () => { const handler = req => ({ ...req, handled: true }); const interceptor1 = (req, next) => next({ ...req, interceptor1: true }); const interceptor2 = (req, next) => next({ ...req, interceptor2: true }); const routes = [ [ "GET", "/global-only", handler ], [ "GET", "/global-and-local", handler, [ interceptor2 ] ], ]; const table = buildRoutes(routes, [ interceptor1 ]); { const { handled, interceptor1, interceptor2 } = await handleRequest(table, "GET", "/global-only"); assert.deepEqual( { handled, interceptor1, interceptor2 }, { handled: true, interceptor1: true, interceptor2: undefined }, ); }; { const { handled, interceptor1, interceptor2 } = await handleRequest(table, "GET", "/global-and-local"); assert.deepEqual( { handled, interceptor1, interceptor2 }, { handled: true, interceptor1: true, interceptor2: true }, ); }; }); await t.test("multiple routes built", () => { const fn1 = () => {}; const fn2 = () => {}; const fn3 = () => {}; const fn4 = () => {}; const fn5 = () => {}; const routes = [ [ "GET", "/", fn1 ], [ "GET", "/home", fn2 ], [ "GET", "/user", fn3 ], [ "POST", "/user", fn4 ], [ "GET", "/user/:id", fn5 ], ]; assert.deepEqual( buildRoutes(routes), { static: { GET: { "": fn1, home: { "": fn2, }, user: { "": fn3, }, }, POST: { user: { "": fn4, }, }, }, dynamic: { GET: { user: { ":id": { "": fn5, }, }, }, }, }, ); }); }; const test_promisifyServer = async t => { t.start("promisifyServer()"); await t.test("we can access the underlying server ref", () => { const server = promisifyServer("app-name", http.createServer(() => {})); assert.ok(server.ref instanceof http.Server); }); }; const test_buildServer = async t => { t.start("buildServer()"); const socketRequest = (socketPath, path) => new Promise((resolve, reject) => { const callback = res => { let body = ""; res.on("data", chunk => body += chunk); res.on("error", reject); res.on("end", () => resolve({ body, status: res.statusCode, })); }; const request = http.request({ socketPath, path, }, callback); request.end(); }); await t.test("empty values", async () => { const socket = "tests/hero-4.socket"; const pipe = "tests/hero-4.pipe"; const server = buildServer("my app", []); await server.listen(socket, pipe); const response = await socketRequest(socket, "/anything"); await server.close(); assert.deepEqual(response, { status: 404, body: "Not Found\n" }); }); await t.test("integrated application server", async () => { const socket = "tests/hero-5.socket"; const pipe = "tests/hero-5.pipe"; const pathHandler = req => ({ status: 200, body: "something" }); const routes = [ [ "GET", "/path", pathHandler ] ]; const server = buildServer("the-app", routes, defaultInterceptors); await server.listen(socket, pipe); const response = await socketRequest(socket, "/path"); await server.close(); assert.deepEqual(response, { status: 200, body: "something" }); }); }; await runner.runTests([ test_normalizeSegments, test_pathToSegments, test_hasPathParams, test_addRoute, test_findStaticHandler, test_firstParamMatch, test_findDynamicHandler, test_findHandler, test_extractQueryParams, test_handleRequest, test_makeRequestListener, test_configLogger, test_logit, test_makeLogger, test_interceptorsFn, test_chainInterceptors, test_wrapHandler, test_actionsFn, test_lineHandlerFn, test_buildRoutes, test_rmIf, test_mkfifo, test_promisifyServer, test_buildServer, ]);