import assert from "node:assert/strict";
import fs from "node:fs";
import http from "node:http";
import path from "node:path";
import process from "node:process";
import * as runner from "../runner.mjs";
import * as u from "../../src/utils.mjs";
import {
loggerDefaults,
loggerGlobals,
configLogger,
logit,
now,
makeLogger,
statusMessage,
statusResponse,
isValidMethod,
isValidUpgrade,
isValidKey,
isValidVersion,
validateUpgrade,
computeHash,
interceptorsFn,
interceptors,
defaultInterceptors,
chainInterceptors,
wrapHandler,
normalizeSegments,
pathToSegments,
hasPathParams,
isValidLabel,
comboForLabel,
addRoute,
findStaticHandler,
firstParamMatch,
findDynamicHandler,
findHandler,
extractQueryParams,
renderStatus,
renderHeaders,
buildHeader,
writeHead,
make404Handler,
handleRequest,
makeRequestListener,
makeUpgradeListener,
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_now = async t => {
t.start("now()");
await t.test("we get an ISO date", () => {
const s = now();
assert.deepEqual(s, new Date(s).toISOString());
});
};
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);
let i = 0;
const timestampFn = () => `${i++}`;
logit(writerFn, timestampFn, "my level", { a: 1, type: "log-test" });
assert.deepEqual(contents.map(JSON.parse), [{
...loggerDefaults,
app: "hero-based app",
level: "my level",
timestamp: "0",
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);
let i = 0;
const timestampFn = () => `${i++}`;
configLogger({
level: "unseen",
a: "unseen",
});
logit(writerFn, timestampFn, "overwritten by level", { a: "overwritten by o" });
configLogger({
pid: "overwritten by loggerGlobals",
});
logit(writerFn, timestampFn, "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",
timestamp: "0",
a: "overwritten by o",
},
{
...loggerDefaults,
pid: "overwritten by loggerGlobals",
level: "overwritten by o",
timestamp: "1",
},
]);
});
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);
let i = 0;
const timestampFn = () => `${i++}`;
const log = makeLogger({ writerFn, timestampFn });
log.info ({ type: "expected" });
log.warn ({ type: "worrysome" });
log.error({ type: "bad" });
assert.deepEqual(contents.map(JSON.parse), [
{
...loggerDefaults,
level: "INFO",
timestamp: "0",
type: "expected",
},
{
...loggerDefaults,
level: "WARN",
timestamp: "1",
type: "expected",
type: "worrysome",
},
{
...loggerDefaults,
level: "ERROR",
timestamp: "2",
type: "expected",
type: "bad",
},
]);
});
await t.test("debug only works when $DEBUG is set", () => {
const contents = [];
const writerFn = x => contents.push(x);
let i = 0;
const timestampFn = () => `${i++}`;
const log = makeLogger({ writerFn, timestampFn });
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",
timestamp: "0",
x: "seen",
}]);
process.env.DEBUG = previous;
});
};
const test_statusMessage = async t => {
t.start("statusMessage()");
await t.test("we get the expected values", () => {
assert.deepEqual(
[ 101, 200, 409, 422, 502, 503 ].map(statusMessage),
[
"Switching Protocols\n",
"OK\n",
"Conflict\n",
"Unprocessable Entity\n",
"Bad Gateway\n",
"Service Unavailable\n",
],
);
});
};
const test_statusResponse = async t => {
t.start("statusResponse()");
await t.test("we get a returnable body", () => {
assert.deepEqual(statusResponse(202), {
status: 202,
body: "Accepted\n",
});
});
};
const test_isValidMethod = async t => {
t.start("isValidMethod()");
await t.test("we only accept a single value", () => {
assert.ok(isValidMethod("GET"));
assert.ok(!isValidMethod("get"));
assert.ok(!isValidMethod("PUT"));
})
};
const test_isValidUpgrade = async t => {
t.start("isValidUpgrade()");
await t.test("we ignore the case", () => {
assert.ok(isValidUpgrade("Websocket"));
assert.ok(isValidUpgrade("WebSocket"));
assert.ok(isValidUpgrade("websocket"));
assert.ok(!isValidUpgrade("web socket"));
});
};
const test_isValidKey = async t => {
t.start("isValidKey()");
await t.test("RFC example value", () => {
const key = "dGhlIHNhbXBsZSBub25jZQ==";
assert.ok(isValidKey(key));
});
await t.test("wrong example values", () => {
const key1 = "_GhlIHNhbXBsZSBub25jZQ==";
const key2 = "dGhlIHNhbXBsZSBub25jZQ=";
assert.ok(!isValidKey(key1));
assert.ok(!isValidKey(key2));
});
};
const test_isValidVersion = async t => {
t.start("isValidVersion()");
await t.test("we only accept a single value", () => {
assert.ok(isValidVersion(13));
assert.ok(!isValidVersion(9));
assert.ok(!isValidVersion(10));
assert.ok(!isValidVersion(11));
assert.ok(!isValidVersion(12));
});
};
const test_validateUpgrade = async t => {
t.start("validateUpgrade()");
await t.test("invalid method", () => {
assert.deepEqual(validateUpgrade("POST", {}), {
isValid: false,
response: {
status: 405,
body: "Method Not Allowed\n",
},
});
});
await t.test("missing upgrade", () => {
assert.deepEqual(validateUpgrade("GET", {}), {
isValid: false,
response: {
status: 400,
body: 'Missing "Upgrade" header\n',
},
});
});
await t.test("invalid upgrade", () => {
assert.deepEqual(validateUpgrade("GET", {
"upgrade": "web socket",
}), {
isValid: false,
response: {
status: 400,
body: 'Invalid "Upgrade" value\n',
},
});
});
await t.test("missing sec-websocket-key", () => {
assert.deepEqual(validateUpgrade("GET", {
"upgrade": "websocket",
}), {
isValid: false,
response: {
status: 400,
body: 'Missing "Sec-WebSocket-Key" header\n',
},
});
});
await t.test("invalid sec-websocket-key", () => {
assert.deepEqual(validateUpgrade("GET", {
"upgrade": "websocket",
"sec-websocket-key": "bad value",
}), {
isValid: false,
response: {
status: 400,
body: 'Invalid "Sec-WebSocket-Key" value\n',
},
});
});
await t.test("missing sec-websocket-version", () => {
assert.deepEqual(validateUpgrade("GET", {
"upgrade": "websocket",
"sec-websocket-key": "aaaaabbbbbcccccdddddee==",
}), {
isValid: false,
response: {
status: 400,
body: 'Missing "Sec-WebSocket-Version" header\n',
},
});
});
await t.test("invalid sec-websocket-version", () => {
assert.deepEqual(validateUpgrade("GET", {
"upgrade": "websocket",
"sec-websocket-key": "aaaaabbbbbcccccdddddee==",
"sec-websocket-version": "12",
}), {
isValid: false,
response: {
status: 400,
body: 'Invalid "Sec-WebSocket-Version" value\n',
},
});
});
await t.test("valid upgrade", () => {
assert.deepEqual(validateUpgrade("GET", {
"upgrade": "websocket",
"sec-websocket-key": "aaaaabbbbbcccccdddddee==",
"sec-websocket-version": "13",
}), {
isValid: true,
});
});
};
const test_computeHash = async t => {
t.start("computeHash()");
await t.test("RFC example value", () => {
const key = "dGhlIHNhbXBsZSBub25jZQ==";
const hash = "s3pPLMBiTxaQ9kYGzzhZRbK+xOo=";
assert.equal(computeHash(key), hash);
});
await t.test("a key used in other tests", () => {
const key = "aaaaabbbbbcccccdddddee==";
const hash = "eHnDP9gUz224y002aFCe7swigxg=";
assert.equal(computeHash(key), hash);
});
};
const test_interceptorsFn = async t => {
const next = x => ({ ...x, nextCalled: true });
{
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",
nextCalled: true,
},
);
assert.deepEqual(
interceptorsFn({uuidFn}).requestId({ a: "existing data" }, next),
{
a: "existing data",
id: "1",
nextCalled: true,
},
);
});
await t.test(`we overwrite the "id" if it already exists`, async () => {
assert.deepEqual(
interceptorsFn({uuidFn}).requestId({ id: "before" }, next),
{
id: "2",
nextCalled: true,
},
);
});
};
{
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",
upgrade: true,
};
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 () => {
await assert.rejects(
async () => await interceptorsFn().contentType({}, next),
assert.AssertionError,
);
assert.deepEqual(
await interceptorsFn().contentType({
status: 202,
}, next),
{
status: 202,
body: "Accepted\n",
headers: {
"Content-Type": "text/plain",
"Content-Length": 9,
},
nextCalled: true,
},
);
assert.deepEqual(
await interceptorsFn().contentType({
status: 200,
body: "",
}, next),
{
status: 200,
body: "",
headers: {
"Content-Type": "text/html",
"Content-Length": 0,
},
nextCalled: true,
},
);
});
await t.test("body values", async () => {
assert.deepEqual(
await interceptorsFn().contentType({
status: 201,
body: { a: 1 },
}, next),
{
status: 201,
body: `{"a":1}`,
headers: {
"Content-Type": "application/json",
"Content-Length": 7,
},
nextCalled: true,
},
);
assert.deepEqual(
await interceptorsFn().contentType({
status: 200,
body: "
",
}, next),
{
status: 200,
body: "
",
headers: {
"Content-Type": "text/html",
"Content-Length": 6,
},
nextCalled: true,
},
);
});
await t.test("header values preference", async () => {
assert.deepEqual(
await interceptorsFn().contentType({
status: 503,
body: "",
headers: {
"Content-Type": "we have preference",
"Content-Length": "and so do we",
},
}, next),
{
status: 503,
body: "",
headers: {
"Content-Type": "we have preference",
"Content-Length": "and so do we",
},
nextCalled: true,
},
);
});
await t.test("headers get propagated", async () => {
assert.deepEqual(
await interceptorsFn().contentType({
status: 500,
body: "",
headers: {
"My": "Header",
},
}, next),
{
status: 500,
body: "",
headers: {
"My": "Header",
"Content-Type": "text/html",
"Content-Length": 0,
},
nextCalled: true,
},
);
});
};
{
t.start("interceptorsFn().serverError()");
await t.test("no-op when no error occurs", async () => {
assert.deepEqual(
await interceptorsFn().serverError({ status: 1 }, next),
{
status: 1,
nextCalled: true,
},
);
});
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.map(o => ({ ...o, stacktrace: typeof o.stacktrace })),
[{
id: 123,
type: "server-error-interceptor",
message: `Missing "status"`,
stacktrace: "string",
}],
);
});
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.map(o => ({ ...o, stacktrace: typeof o.stacktrace })),
[{
id: "some ID",
type: "server-error-interceptor",
message: "My test error message",
stacktrace: "string",
}],
);
});
};
{
t.start("interceptorsFn().websocketHandshake()");
await t.test("no-op when not an upgrade request", async () => {
assert.deepEqual(
await interceptorsFn().websocketHandshake({
upgrade: false,
}, next),
{
upgrade: false,
nextCalled: true,
},
);
});
await t.test("when invalid we forward what validateUpgrade() says", async () => {
assert.deepEqual(
await interceptorsFn().websocketHandshake({
upgrade: true,
method: "GET",
headers: {},
}, next),
{
status: 400,
body: 'Missing "Upgrade" header\n',
},
);
});
await t.test("otherwise we upgrade the connection", async () => {
assert.deepEqual(
await interceptorsFn().websocketHandshake({
upgrade: true,
method: "GET",
headers: {
"upgrade": "websocket",
"sec-websocket-key": "aaaaabbbbbcccccdddddee==",
"sec-websocket-version": "13",
},
}, next),
{
status: 101,
headers: {
"Connection": "Upgrade",
"Upgrade": "websocket",
"Sec-WebSocket-Accept": "eHnDP9gUz224y002aFCe7swigxg=",
},
},
);
});
};
};
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",
upgrade: false,
};
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",
upgrade: false,
},
{
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 fn4 = () => {};
const table = {
static: {
GET: {
api: {
home: { "": fn1 },
settings: { "": fn2 },
},
},
POST: {
api: {
settings: { "": fn3 },
},
},
},
websocket: {
GET: {
api: {
socket: { "": fn4 },
},
},
}
};
assert.deepEqual(
findStaticHandler(table, "GET", [ "api", "home", "" ], "static"),
{ handlerFn: fn1, params: {} },
);
assert.deepEqual(
findStaticHandler(table, "PUT", [ "api", "home", "" ], "static"),
null,
);
assert.deepEqual(
findStaticHandler(table, "GET", [ "api", "settings", "" ], "static"),
{ handlerFn: fn2, params: {} },
);
assert.deepEqual(
findStaticHandler(table, "POST", [ "api", "settings", "" ], "static"),
{ handlerFn: fn3, params: {} },
);
assert.deepEqual(
findStaticHandler(table, "PUT", [ "api", "settings", "" ], "static"),
null,
);
assert.deepEqual(
findStaticHandler(table, "GET", [ "api", "profile", "" ], "static"),
null,
);
assert.deepEqual(
findStaticHandler({}, "GET", [ "api", "profile", "" ], "static"),
null,
);
assert.deepEqual(
findStaticHandler(table, "GET", [ "api", "socket", "" ], "static"),
null,
);
assert.deepEqual(
findStaticHandler(table, "GET", [ "api", "socket", "" ], "websocket"),
{ handlerFn: fn4, params: {} },
);
});
};
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, dynamic and websocket routes", () => {
const static1 = () => {};
const static2 = () => {};
const static3 = () => {};
const dynamic1 = () => {};
const dynamic2 = () => {};
const dynamic3 = () => {};
const dynamic4 = () => {};
const websocket1 = () => {};
const websocket2 = () => {};
const table = {
static: {
GET: {
user: {
"": static1,
},
pages: {
"": static2,
home: {
"": static3,
},
},
},
},
dynamic: {
GET: {
user: {
":id": {
"": dynamic1,
},
},
},
PUT: {
user: {
":id": {
"": dynamic2,
"info": {
"": dynamic3,
},
"preferences": {
"": dynamic4,
},
},
},
},
},
websocket: {
GET: {
user: {
"": websocket1,
socket: {
"": websocket2,
},
},
},
},
};
assert.deepEqual(
findHandler(table, "GET", "/", false),
null,
);
assert.deepEqual(
findHandler(table, "GET", "/user/", false),
{ handlerFn: static1, params: {} },
);
assert.deepEqual(
findHandler(table, "GET", "/user/", true),
{ handlerFn: websocket1, params: {} },
);
assert.deepEqual(
findHandler(table, "GET", "/pages", false),
{ handlerFn: static2, params: {} },
);
assert.deepEqual(
findHandler(table, "GET", "/pages/home/", false),
{ handlerFn: static3, params: {} },
);
assert.deepEqual(
findHandler(table, "GET", "/user/some-id", false),
{ handlerFn: dynamic1, params: { id: "some-id" }},
);
assert.deepEqual(
findHandler(table, "GET", "/user/other-id/info", false),
null,
);
assert.deepEqual(
findHandler(table, "PUT", "/user/other-id/info", false),
{ handlerFn: dynamic3, params: { id: "other-id" }},
);
assert.deepEqual(
findHandler(table, "PUT", "/user/another-id/preferences", false),
{ handlerFn: dynamic4, params: { id: "another-id" }},
);
assert.deepEqual(
findHandler(table, "POST", "/user/another-id/preferences", false),
null,
);
assert.deepEqual(
findHandler(table, "GET", "/user/socket", true),
{ handlerFn: websocket2, params: {} },
);
});
};
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_renderStatus = async t => {
t.start("renderStatus()");
await t.test("good statuses", () => {
assert.equal(renderStatus(101), "HTTP/1.1 101 Switching Protocols");
assert.equal(renderStatus(202), "HTTP/1.1 202 Accepted");
assert.equal(renderStatus(409), "HTTP/1.1 409 Conflict");
});
};
const test_renderHeaders = async t => {
t.start("renderHeaders()");
await t.test("empty values", () => {
assert.deepEqual(renderHeaders({}), []);
assert.deepEqual(renderHeaders(), []);
})
await t.test("values are rendered and sorted", () => {
assert.deepEqual(renderHeaders({ a: "one", Z: "two" }), [
"a: one",
"Z: two",
]);
});
await t.test("GIGO for newlines", () => {
assert.deepEqual(renderHeaders({ a: "\nx: 1\r\n", }), [
"a: \nx: 1\r\n",
]);
});
};
const test_buildHeader = async t => {
t.start("buildHeader()");
await t.test("empty values", () => {
assert.equal(
buildHeader(200, {}),
"HTTP/1.1 200 OK\r\n" +
"\r\n",
);
});
await t.test("we compose the status line and the headers", () => {
assert.equal(
buildHeader(201, { a: "1", b: "2" }),
"HTTP/1.1 201 Created\r\n" +
"a: 1\r\n" +
"b: 2\r\n" +
"\r\n",
);
});
};
const test_writeHead = async t => {
t.start("writeHead()");
await t.test("we simply write what buildHeader() gives us", () => {
const contents = [];
const socket = { write: x => contents.push(x) };
writeHead(socket, 202, { "Key": "Value" });
assert.deepEqual(contents, [
"HTTP/1.1 202 Accepted\r\n" +
"Key: Value\r\n" +
"\r\n",
]);
});
};
const test_make404Handler = async t => {
t.start("make404Handler");
await t.test("empty interceptors", () => {
assert.deepEqual(
new Set(Object.keys(make404Handler([]))),
new Set(["handlerFn", "params"]),
);
assert.deepEqual(make404Handler([]).params, {});
assert.deepEqual(make404Handler([]).handlerFn(Math.random()), {
status: 404,
body: "Not Found\n",
});
});
};
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",
},
upgrade: false,
socket: null,
};
assert.deepEqual(
await handleRequest(table, req),
{
params: {
path: {},
query: {
q: "1",
},
},
method: "GET",
path: "/",
headers: {
a: "1",
b: "two",
},
ref: req,
handler: fn,
upgrade: false,
socket: null,
},
);
});
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",
},
upgrade: false,
socket: null,
};
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,
upgrade: false,
socket: null,
},
);
});
await t.test("upgrade request", async () => {
const socket = () => {};
const handler = req => {
assert.equal(req.socket, socket);
return "handler ret";
};
const table = {
websocket: {
GET: {
api: {
socket: {
"": handler,
},
},
},
},
interceptors: [],
};
const req = {
method: "GET",
url: "/api/socket",
upgrade: true,
socket,
};
assert.deepEqual(
await handleRequest(table, req),
"handler ret",
);
});
await t.test("missing route", async () => {
assert.deepEqual(
await handleRequest({ interceptors: [] }, {
method: "GET",
url: "/",
}),
{
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_makeUpgradeListener = async t => {
t.start("makeUpgradeListener()");
await t.test("straightforward connection stablishing", async () => {
const calls = [];
const fn = x => calls.push([x.upgrade, x.socket]);
const routes = [[ "WEBSOCKET", "/socket", fn ]];
const table = buildRoutes(routes, [
interceptors.contentType,
interceptors.websocketHandshake,
]);
const upgradeListener = makeUpgradeListener(table);
const req = {
method: "GET",
url: "/socket",
upgrade: true,
headers: {
"upgrade": "websocket",
"sec-websocket-key": "aaaaabbbbbcccccdddddee==",
"sec-websocket-version": "13",
},
};
const contents = [];
const socket = {
end: () => assert.ok(false),
write: x => contents.push(x),
};
await upgradeListener(req, socket);
assert.deepEqual(calls, [[true, socket]]);
assert.deepEqual(contents, [
"HTTP/1.1 101 Switching Protocols\r\n" +
"Connection: Upgrade\r\n" +
"Content-Length: 20\r\n" +
"Content-Type: text/plain\r\n" +
"Sec-WebSocket-Accept: eHnDP9gUz224y002aFCe7swigxg=\r\n" +
"Upgrade: websocket\r\n" +
"\r\n",
"Switching Protocols\n",
]);
});
await t.test("early termination cases", async () => {
const routes = [[ "WEBSOCKET", "/a-socket", null ]];
const table = buildRoutes(routes, [
interceptors.websocketHandshake,
]);
const upgradeListener = makeUpgradeListener(table);
const req = {
method: "GET",
url: "/a-socket",
upgrade: true,
headers: {
"upgrade": "websocket",
},
};
let ended = false;
const contents = [];
const socket = {
end: () => ended = true,
write: x => contents.push(x),
};
await upgradeListener(req, socket);
assert.ok(ended);
assert.deepEqual(contents, [
"HTTP/1.1 400 Bad Request\r\n\r\n",
'Missing "Sec-WebSocket-Key" header\n',
]);
});
};
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, headers = {}) =>
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,
headers,
}, 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" });
assert.deepEqual(server.info(), { name, socket, pipe });
});
await t.test("default values", () => {
const name = path.basename(process.cwd());
assert.deepEqual(buildServer().info(), {
name,
socket: `${name}.socket`,
pipe: `${name}.pipe`,
});
});
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_now,
test_makeLogger,
test_statusMessage,
test_statusResponse,
test_isValidMethod,
test_isValidUpgrade,
test_isValidKey,
test_isValidVersion,
test_validateUpgrade,
test_computeHash,
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_renderStatus,
test_renderHeaders,
test_buildHeader,
test_writeHead,
test_make404Handler,
test_handleRequest,
test_makeRequestListener,
test_makeUpgradeListener,
test_actionsFn,
test_lineHandlerFn,
test_rmIf,
test_mkfifo,
test_makeLineEmitter,
test_makeReopeningPipeReader,
test_makePipeReaderFn,
test_buildRoutes,
test_buildTable,
test_promisifyServer,
test_buildServer,
]);