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,
]);