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 {
loggerDefaults,
loggerGlobals,
configLogger,
logit,
makeLogger,
interceptorsFn,
interceptors,
defaultInterceptors,
chainInterceptors,
wrapHandler,
normalizeSegments,
pathToSegments,
hasPathParams,
isValidLabel,
comboForLabel,
addRoute,
findStaticHandler,
firstParamMatch,
findDynamicHandler,
findHandler,
extractQueryParams,
handle404,
make404Handler,
handleRequest,
emitHeaders,
buildHttpPayload,
fallback404Handler,
fallback405Handler,
handlerForConnection,
makeRequestListener,
actionsFn,
lineHandlerFn,
rmIf,
mkfifo,
makeLineEmitter,
makeReopeningPipeReader,
makePipeReaderFn,
buildRoutes,
buildTable,
promisifyServer,
buildServer,
} from "../../src/hero.mjs";
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()");
await 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.map(o => u.dissoc(o, "timings")),
[
{ ...req, type: "in-request" },
{ id: req.id, status, type: "in-response" },
],
);
assert.equal(typeof contents[1].timings.ms.before, "number");
assert.equal(typeof contents[1].timings.ms.after, "number");
assert.equal(typeof contents[1].timings.ms.duration, "number");
});
};
{
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.map(o => u.dissoc(o, "timings")),
[
{
id: "0",
url: "URL",
method: "METHOD",
type: "in-request",
},
{
id: "0",
status: 1,
type: "in-response",
},
],
);
});
};
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_isValidLabel = async t => {
t.start("isValidLabel()");
await t.test("typo examples", () => {
assert.ok(!isValidLabel("get"));
assert.ok(!isValidLabel("WebSocket"));
assert.ok(!isValidLabel("WEBSOCKETS"));
assert.ok(!isValidLabel("ws"));
});
await t.test("valid usages", () => {
assert.ok(isValidLabel("GET"));
assert.ok(isValidLabel("PUT"));
assert.ok(isValidLabel("WEBSOCKET"));
});
};
const test_comboForLabel = async t => {
t.start("comboForLabel()");
await t.test("websocket gets its own combo", () => {
assert.deepEqual(
comboForLabel("WEBSOCKET", "IGNORED"),
[ "websocket", "GET" ],
);
});
await t.test("otherwise we get what pass", () => {
assert.deepEqual(
comboForLabel("not-websocket", "a-keyword"),
[ "a-keyword", "not-websocket" ],
);
});
};
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({}, "WEBSOCKET", "/socket", fn1),
{ websocket: { GET: { socket: { "": 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_make404Handler = async t => {
t.start("make404Handler");
await t.test("empty interceptors", () => {
assert.deepEqual(make404Handler([]), {
params: {},
handlerFn: handle404,
});
});
};
const test_handleRequest = async t => {
t.start("handleRequest()");
const fn = req => req;
await t.test("request without params", async () => {
const table = {
static: {
GET: {
"": fn,
},
},
};
const req = {
method: "GET",
url: "/?q=1",
headers: {
a: "1",
b: "two",
},
};
assert.deepEqual(
await handleRequest(table, req),
{
params: {
path: {},
query: {
q: "1",
},
},
method: "GET",
path: "/",
headers: {
a: "1",
b: "two",
},
ref: req,
handler: fn,
},
);
});
await t.test("request with params", async () => {
const table = {
dynamic: {
PUT: {
api: {
user: {
":userId": {
"": fn,
},
},
},
},
},
interceptors: [],
};
const req = {
method: "PUT",
url: "/api/user/2222",
headers: {
h1: "H1",
h2: "h2",
},
};
assert.deepEqual(
await handleRequest(table, req),
{
params: {
path: {
userId: "2222",
},
query: {},
},
method: "PUT",
path: "/api/user/2222",
headers: {
h1: "H1",
h2: "h2",
},
handler: fn,
ref: req,
},
);
});
await t.test("missing route", async () => {
assert.deepEqual(
await handleRequest({ interceptors: [] }, {
method: "GET",
url: "/",
}),
{
status: 404,
body: "Not Found\n",
},
);
});
};
const test_emitHeaders = async t => {
t.start("emitHeaders()");
await t.test("empty values", () => {
assert.equal(emitHeaders({}), "");
assert.equal(emitHeaders({ "": "" }), ": ");
assert.equal(emitHeaders({ " ": " " }), " : ");
assert.equal(emitHeaders({ "_": "_" }), "_: _");
});
await t.test("newlines are forwarded", () => {
assert.equal(emitHeaders({"\na\n": "\nb\n"}), "\na\n: \nb\n");
});
await t.test("keys are always sorted", () => {
assert.equal(
emitHeaders({ "a": "one", "Z": "two" }),
"a: one\r\nZ: two",
);
});
};
const test_buildHttpPayload = async t => {
t.start("buildHttpPayload()");
await t.test("empty values", () => {
assert.equal(
buildHttpPayload(404),
"HTTP/1.1 404 Not Found\r\n" +
"Connection: close\r\n" +
"Content-Length: 10\r\n" +
"Content-Type: text/plain; charset=UTF-8\r\n" +
"\r\n" +
"Not Found\n",
);
assert.equal(
buildHttpPayload(405),
"HTTP/1.1 405 Method Not Allowed\r\n" +
"Connection: close\r\n" +
"Content-Length: 19\r\n" +
"Content-Type: text/plain; charset=UTF-8\r\n" +
"\r\n" +
"Method Not Allowed\n",
);
});
await t.test("we can add headers and customise the message", () => {
assert.equal(
buildHttpPayload(404, {
headers: {
"X-Something": "something",
"Aaaa": "ZzZz",
"Content-Type": "text/plain",
},
message: "the message\n"
}),
"HTTP/1.1 404 Not Found\r\n" +
"Aaaa: ZzZz\r\n" +
"Connection: close\r\n" +
"Content-Length: 12\r\n" +
"Content-Type: text/plain\r\n" +
"X-Something: something\r\n" +
"\r\n" +
"the message\n",
);
});
};
export const test_handlerForConnection = async t => {
t.start("handlerForConnection()");
await t.test("405 handler no matter the table when not GET method", () => {
assert.equal(
handlerForConnection(null, "POST", null),
fallback405Handler,
);
});
await t.test("404 handler when there is no route", () => {
assert.equal(
handlerForConnection({}, "GET", "/the/websocket"),
fallback404Handler,
);
});
await t.test("the declared handler when it exists", () => {
const fn1 = () => {};
assert.equal(
handlerForConnection({
websocket: {
GET: {
known: {
path: {
"": fn1,
},
},
},
},
}, "GET", "/known/path"),
fn1,
);
});
};
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_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",
},
},
]);
});
};
{
t.start(`actionsFn()["ping"]()`);
await t.test("simple pinging", () => {
const contents = [];
const logger = { info: x => contents.push(x) };
const actions = actionsFn({ logger });
configLogger({});
actions["ping"]("blah");
actions["ping"](null);
assert.deepEqual(contents, [
{ message: "pong" },
{ message: "pong" },
]);
});
};
};
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-mkfifo-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-mkfifo-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_makeLineEmitter = async t => {
t.start("makeLineEmitter()");
await t.test("noop when we only get empty strings", async () => {
const entries = [];
const record = x => entries.push(x);
const emitter = makeLineEmitter(record);
emitter("");
emitter("");
emitter("");
emitter("");
emitter("");
assert.deepEqual(entries, []);
});
await t.test("empty strings when we only get newlines", async () => {
const entries = [];
const record = x => entries.push(x);
const emitter = makeLineEmitter(record);
emitter("\n\n\n");
emitter("\n\n");
emitter("\n");
assert.deepEqual(entries, [ "", "", "", "", "", "" ]);
});
await t.test("noop also if we never get a newline", async () => {
const entries = [];
const record = x => entries.push(x);
const emitter = makeLineEmitter(record);
emitter(" ");
emitter("some string");
emitter(" ");
emitter("a lot of text");
assert.deepEqual(entries, []);
emitter("\n");
assert.deepEqual(entries, [ " some string a lot of text" ]);
});
await t.test("if a newline always comes, we always emit", async () => {
const entries = [];
const record = x => entries.push(x);
const emitter = makeLineEmitter(record);
emitter("first\n");
emitter("second\n");
emitter("third\n");
assert.deepEqual(entries, [ "first", "second", "third" ]);
});
await t.test("lines can acummulate accross multiple writes", async () => {
const entries = [];
const record = x => entries.push(x);
const emitter = makeLineEmitter(record);
emitter("fir");
emitter("s");
emitter("t\ns");
emitter("econd\nthir");
emitter("d");
emitter("\n");
emitter("fourth\nfifth\nsixth");
assert.deepEqual(entries, [
"first",
"second",
"third",
"fourth",
"fifth",
]);
});
};
const test_makeReopeningPipeReader = async t => {
t.start("makeReopeningPipeReader()");
await t.test("we can init to not reopen from the start", async () => {
const path = "tests/hero-makeReopeningPipeReader-0.pipe";
const shouldReopenPipe = { ref: false };
const lines = []
const logs = [];
const lineFn = x => lines.push(x);
const logger = { debug: x => logs.push(x) };
const previous = process.env.DEBUG;
delete process.env.DEBUG;
rmIf(path);
mkfifo(path);
const pipe = {};
makeReopeningPipeReader(
shouldReopenPipe,
path,
{ lineFn, logger },
pipe,
);
return new Promise((resolve, reject) => {
fs.createWriteStream(path).end().close();
pipe.ref.on("close", () => {
assert.deepEqual(lines, []);
assert.deepEqual(logs, [{
message: "pipe closed, NOT reopening",
}]);
process.env.DEBUG = previous;
resolve();
});
});
});
await t.test("we can reopen more than once", async () => {
const path = "tests/hero-makeReopeningPipeReader-1.pipe";
const shouldReopenPipe = { ref: true };
const lines = [];
const logs = [];
const lineFn = x => lines.push(x);
const logger = { debug: x => logs.push(x) };
const previous = process.env.DEBUG;
delete process.env.DEBUG;
rmIf(path);
mkfifo(path);
const pipe = {};
makeReopeningPipeReader(
shouldReopenPipe,
path,
{ lineFn, logger },
pipe,
);
return new Promise((resolve, reject) => {
fs.createWriteStream(path).end("first\n").close();
pipe.ref.on("close", () => {
fs.createWriteStream(path).end("second\n").close();
pipe.ref.on("close", () => {
shouldReopenPipe.ref = false;
fs.createWriteStream(path).end("third\n").close();
pipe.ref.on("close", () => {
assert.deepEqual(lines, [
"first",
"second",
"third",
]);
assert.deepEqual(logs, [
{ message: "pipe closed, reopening" },
{ message: "pipe closed, reopening" },
{ message: "pipe closed, NOT reopening" },
]);
process.env.DEBUG = previous;
resolve();
});
});
});
});
});
};
const test_makePipeReaderFn = async t => {
t.start("makePipeReaderFn()");
await t.test("we can close it directly on creation with no data", async () => {
const path = "tests/hero-makePipeReader-0.pipe";
const lines = [];
const logs = [];
const lineFn = x => lines.push(x);
const logger = { debug: x => logs.push(x) };
const makePipeReader = makePipeReaderFn({ lineFn, logger });
rmIf(path);
await makePipeReader(path)();
assert.deepEqual(lines, []);
assert.deepEqual(logs, [{ message: "pipe closed, NOT reopening" }]);
});
};
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, {
method: "GET",
url: "/without",
});
assert.deepEqual(
{ handled, intercepted },
{ handled: true, intercepted: undefined },
);
};
{
const { handled, intercepted } =
await handleRequest(table, {
method: "GET",
url: "/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, {
method: "GET",
url: "/global-only",
});
assert.deepEqual(
{ handled, interceptor1, interceptor2 },
{ handled: true, interceptor1: true, interceptor2: undefined },
);
};
{
const { handled, interceptor1, interceptor2 } =
await handleRequest(table, {
method: "GET",
url: "/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_buildTable = async t => {
t.start("buildTable()");
await t.test('we just add the "interceptors" key to what buildRoutes() gives us', () => {
assert.deepEqual(
buildTable([], [ "i1", "i2" ]),
{ interceptors: [ "i1", "i2" ] },
);
});
};
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-buildServer-0.socket";
const pipe = "tests/hero-buildServer-0.pipe";
const name = "my-empty-app";
const server = buildServer({ name, socket, pipe });
await server.start();
const response = await socketRequest(socket, "/anything");
await server.stop();
assert.deepEqual(response, { status: 404, body: "Not Found\n" });
});
await t.test("integrated application server", async () => {
const socket = "tests/hero-buildServer-1.socket";
const pipe = "tests/hero-buildServer-1.pipe";
const name = "the-app";
const pathHandler = req => ({ status: 200, body: "something" });
const routes = [ [ "GET", "/path", pathHandler ] ];
const server = buildServer({ name, routes, socket, pipe });
await server.start();
const response = await socketRequest(socket, "/path");
await server.stop();
assert.deepEqual(response, { status: 200, body: "something" });
});
};
await runner.runTests([
test_configLogger,
test_logit,
test_makeLogger,
test_interceptorsFn,
test_chainInterceptors,
test_wrapHandler,
test_normalizeSegments,
test_pathToSegments,
test_hasPathParams,
test_isValidLabel,
test_comboForLabel,
test_addRoute,
test_findStaticHandler,
test_firstParamMatch,
test_findDynamicHandler,
test_findHandler,
test_extractQueryParams,
test_make404Handler,
test_handleRequest,
test_emitHeaders,
test_buildHttpPayload,
test_handlerForConnection,
test_makeRequestListener,
test_actionsFn,
test_lineHandlerFn,
test_buildRoutes,
test_buildTable,
test_rmIf,
test_mkfifo,
test_makeLineEmitter,
test_makeReopeningPipeReader,
test_makePipeReaderFn,
test_promisifyServer,
test_buildServer,
]);