summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorEuAndreh <eu@euandre.org>2024-03-21 09:43:43 -0300
committerEuAndreh <eu@euandre.org>2024-03-21 09:45:43 -0300
commite7e60205bda309c8aaecc36a03f6c3ad0ec84cd2 (patch)
tree555dd577672eaabb6c85b8a013865a252f9e5a62
parenttests/rand.c: s/Taken/Derived/ (diff)
downloadpapod-e7e60205bda309c8aaecc36a03f6c3ad0ec84cd2.tar.gz
papod-e7e60205bda309c8aaecc36a03f6c3ad0ec84cd2.tar.xz
src/hero.mjs: Retire code
-rw-r--r--deps.mk2
-rw-r--r--src/hero.mjs695
-rw-r--r--src/web.mjs18
-rw-r--r--tests/js/hero.mjs2430
4 files changed, 16 insertions, 3129 deletions
diff --git a/deps.mk b/deps.mk
index ae0edc6..df8aa4f 100644
--- a/deps.mk
+++ b/deps.mk
@@ -4,7 +4,6 @@ sources.mjs = \
src/bin.mjs \
src/db.mjs \
src/escape.mjs \
- src/hero.mjs \
src/ircd.mjs \
src/utils.mjs \
src/web.mjs \
@@ -13,7 +12,6 @@ tests.mjs = \
tests/js/accretion.mjs \
tests/js/db.mjs \
tests/js/escape.mjs \
- tests/js/hero.mjs \
tests/js/ircd.mjs \
tests/js/rand.mjs \
tests/js/utils.mjs \
diff --git a/src/hero.mjs b/src/hero.mjs
deleted file mode 100644
index 21bc4ba..0000000
--- a/src/hero.mjs
+++ /dev/null
@@ -1,695 +0,0 @@
-import assert from "node:assert/strict";
-import child_process from "node:child_process";
-import crypto from "node:crypto";
-import fs from "node:fs";
-import http from "node:http";
-import path from "node:path";
-import process from "node:process";
-import util from "node:util";
-
-import * as u from "./utils.mjs";
-
-
-export const loggerDefaults = {
- pid: process.pid,
- ppid: process.ppid,
- tool: "hero",
-};
-
-export let loggerGlobals = {};
-
-export const configLogger = o => loggerGlobals = o;
-
-export const logit = (writerFn, timestampFn, level, o) =>
- writerFn(JSON.stringify({
- ...loggerDefaults,
- ...loggerGlobals,
- level,
- timestamp: timestampFn(),
- ...o,
- }));
-
-export const now = () => (new Date()).toISOString();
-
-export const makeLogger = ({
- writerFn = console.log,
- timestampFn = now,
-} = {}) => ({
- debug: (...args) => process.env.DEBUG ?
- logit(writerFn, timestampFn, "DEBUG", ...args) :
- null,
- info: u.partial(logit, writerFn, timestampFn, "INFO"),
- warn: u.partial(logit, writerFn, timestampFn, "WARN"),
- error: u.partial(logit, writerFn, timestampFn, "ERROR"),
-});
-
-export const log = makeLogger();
-
-export const statusMessage = code =>
- `${http.STATUS_CODES[code]}\n`;
-
-export const statusResponse = code => ({
- status: code,
- body: statusMessage(code),
-});
-
-export const isValidMethod = method =>
- method === "GET";
-
-export const isValidUpgrade = val =>
- val.toLowerCase() === "websocket";
-
-export const isValidKey = key =>
- /^[0-9a-zA-Z+/]{22}==$/.test(key);
-
-export const isValidVersion = n =>
- n === 13;
-
-export const validateUpgrade = (method, headers) => {
- const upgrade = headers["upgrade"];
- const key = headers["sec-websocket-key"];
- const versionStr = headers["sec-websocket-version"];
- const version = parseInt(versionStr);
-
- if (!isValidMethod(method)) {
- /// Unreachable by default, unless one is constructing tables
- /// manually. Otherwise `findHandler()` will return a 404
- /// before the request gets here.
- return {
- isValid: false,
- response: statusResponse(405),
- };
- }
-
- if (!upgrade) {
- return {
- isValid: false,
- response: {
- status: 400,
- body: 'Missing "Upgrade" header\n',
- },
- };
- }
-
- if (!isValidUpgrade(upgrade)) {
- return {
- isValid: false,
- response: {
- status: 400,
- body: 'Invalid "Upgrade" value\n',
- },
- };
- }
-
- if (!key) {
- return {
- isValid: false,
- response: {
- status: 400,
- body: 'Missing "Sec-WebSocket-Key" header\n',
- },
- };
- }
-
- if (!isValidKey(key)) {
- return {
- isValid: false,
- response: {
- status: 400,
- body: 'Invalid "Sec-WebSocket-Key" value\n',
- },
- };
- }
-
- if (!version) {
- return {
- isValid: false,
- response: {
- status: 400,
- body: 'Missing "Sec-WebSocket-Version" header\n',
- },
- };
- }
-
- if (!isValidVersion(version)) {
- return {
- isValid: false,
- response: {
- status: 400,
- body: 'Invalid "Sec-WebSocket-Version" value\n',
- },
- };
- }
-
- return {
- isValid: true,
- };
-};
-
-const GUID_MAGIC_NUMBER = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
-export const computeHash = key =>
- crypto
- .createHash("SHA1")
- .update(key + GUID_MAGIC_NUMBER)
- .digest("base64");
-
-export const interceptorsFn = ({
- uuidFn = crypto.randomUUID,
- logger = log,
-} = {}) => ({
- requestId: (req, next) => next({ ...req, id: uuidFn() }),
- logged: async (req, next) => {
- const { id, url, method, upgrade } = req;
- logger.info({
- id,
- url,
- method,
- upgrade,
- type: "in-request",
- });
- const beforeDate = new Date();
- const response = await next(req);
- const afterDate = new Date();
- const { status } = response;
-
- const before = beforeDate.getTime();
- const after = afterDate.getTime();
- const duration = after - before;
- logger.info({
- id,
- status,
- type: "in-response",
- timings: {
- ms: { before, after, duration },
- },
- });
- return response;
- },
- contentType: async (req, next) => {
- const response = await next(req);
- const { status, body, headers } = response;
- assert.equal(typeof status, "number");
- const mappings = {
- string: () => [ "text/html", body ],
- undefined: () => [ "text/plain", statusMessage(status) ],
- FALLBACK: () => [ "application/json", JSON.stringify(body) ],
- };
- const type = typeof body;
- assert.notEqual(type, "FALLBACK");
- const [mimeType, renderedBody] =
- (mappings[type] || mappings.FALLBACK)();
- return {
- ...response,
- body: renderedBody,
- headers: {
- "Content-Type": mimeType,
- "Content-Length": Buffer.byteLength(renderedBody),
- ...(response.headers || {})
- },
- };
- },
- serverError: async (req, next) => {
- try {
- const response = await next(req);
- assert.ok("status" in response, `Missing "status"`);
- return response;
- } catch (error) {
- logger.error({
- id: req.id,
- type: "server-error-interceptor",
- message: error.message,
- stacktrace: error.stack
- });
- return statusResponse(500);
- }
- },
- websocketHandshake: async (req, next) => {
- if (!req.upgrade) {
- return await next(req);
- }
-
- const { method, headers} = req;
- const { isValid, response } = validateUpgrade(method, headers);
- if (!isValid) {
- return response;
- }
-
- const _response = await next(req);
- const hash = computeHash(headers["sec-websocket-key"]);
-
- return {
- status: 101,
- headers: {
- "Connection": "Upgrade",
- "Upgrade": "websocket",
- "Sec-WebSocket-Accept": hash,
- },
- };
- },
-});
-
-export const interceptors = interceptorsFn();
-
-export const defaultInterceptors = [
- interceptors.serverError,
- interceptors.requestId,
- interceptors.logged,
- interceptors.contentType,
- interceptors.websocketHandshake,
-];
-
-export const chainInterceptors = arr =>
- req => arr.length === 0 ?
- req :
- arr[0](req, chainInterceptors(arr.slice(1)));
-
-export const wrapHandler = (fn, arr) =>
- arr.length === 0 ?
- fn :
- chainInterceptors(arr.concat([ (req, _next) => fn(req) ]));
-
-export const normalizeSegments = segments =>
- segments.length === 1 && segments[0] === "" ?
- segments :
- segments.concat([""]);
-
-export const pathToSegments = path =>
- normalizeSegments(path
- .replace(/^\/*/, "")
- .replace(/\/*$/, "")
- .replace(/\/+/, "/")
- .split("/"));
-
-export const hasPathParams = segments =>
- segments.some(s => s.startsWith(":"));
-
-const HTTP_METHODS = new Set([
- "GET",
- "HEAD",
- "POST",
- "PUT",
- "PATCH",
- "DELETE",
- "OPTIONS",
-]);
-
-const HTTP_METHODS_ARR = [...HTTP_METHODS.keys()].sort(u.strSortFn);
-
-export const isValidLabel = name =>
- HTTP_METHODS.has(name) || name === "WEBSOCKET";
-
-export const comboForLabel = (label, keyword) =>
- label === "WEBSOCKET" ?
- [ "websocket", "GET" ] :
- [ keyword, label ];
-
-export const addRoute = (table, methods, path, handlerFn) => {
- if (methods === "*") {
- return addRoute(table, HTTP_METHODS_ARR, path, handlerFn);
- }
-
- if (!Array.isArray(methods)) {
- return addRoute(table, [methods], path, handlerFn);
- }
-
- assert.ok(methods.every(isValidLabel));
-
- const segments = pathToSegments(path);
- const kw = hasPathParams(segments) ? "dynamic" : "static";
- return methods.reduce(
- (acc, el) =>
- u.assocIn(
- acc,
- comboForLabel(el, kw).concat(segments),
- handlerFn,
- ),
- table,
- );
-};
-
-export const findStaticHandler = (table, method, segments, section) => {
- const handlerFn = u.getIn(table, [section, method].concat(segments));
- return !handlerFn ? null : { handlerFn, params: {} };
-};
-
-/**
- * "first" as is:
- * - depth-first, as we look for a full match and use it instead of searching
- * in parallel;
- * - the first param in the same level to show up alphabetically, e.g.
- * ":a-user" matches before ":id" does.
- */
-export const firstParamMatch = (tree, segments, params) => {
- assert.notEqual(segments.length, 0);
-
- const [seg, ...nextSegments] = segments;
-
- if (segments.length === 1) {
- assert.equal(seg, "");
- const handlerFn = tree[""];
-
- return !handlerFn ? null : { handlerFn, params };
- }
-
- const subtree = tree[seg];
- if (subtree) {
- const submatch = firstParamMatch(subtree, nextSegments, params);
- /// propagation of the end of recursion
- if (submatch) {
- return submatch;
- }
- }
-
- /// literal matching failed, we now look for patterns that might match
- const paramOptions = Object.keys(tree)
- .filter(s => s.startsWith(":"))
- .sort(u.strSortFn);
- return u.findFirst(paramOptions, param => firstParamMatch(tree[param], nextSegments, {
- ...params,
- [param.slice(1)]: seg
- }));
-};
-
-export const findDynamicHandler = (table, method, segments) => {
- const tree = table?.dynamic?.[method];
- return !tree ? null : firstParamMatch(tree, segments, {});
-};
-
-export const findHandler = (table, method, path, upgrade) => {
- const segments = pathToSegments(path);
- return upgrade ?
- findStaticHandler(table, method, segments, "websocket") :
- (
- findStaticHandler(table, method, segments, "static") ||
- findDynamicHandler(table, method, segments)
- );
-};
-
-export const extractQueryParams = s => {
- const ret = {};
- for (const [k, v] of new URLSearchParams(s)) {
- ret[k] = v;
- }
- return ret;
-};
-
-export const renderStatus = code =>
- `HTTP/1.1 ${code} ${http.STATUS_CODES[code]}`
-
-export const renderHeaders = (obj = {}) =>
- Object.keys(obj)
- .sort(u.strSortFn)
- .map(name => `${name}: ${obj[name]}`);
-
-export const buildHeader = (status, headers) =>
- [renderStatus(status)]
- .concat(renderHeaders(headers))
- .concat(["\r\n"])
- .join("\r\n");
-
-export const writeHead = (socket, status, headers) =>
- socket.write(buildHeader(status, headers));
-
-export const make404Handler = interceptors => ({
- params: {},
- handlerFn: wrapHandler(_ => statusResponse(404), interceptors),
-});
-
-export const handleRequest = async (table, reqHandle) => {
- const { method, url, headers, upgrade, socket } = reqHandle;
- const [ path, queryParams ] = url.split("?");
- const { params, handlerFn } = (
- findHandler(table, method, path, upgrade) ||
- make404Handler(table.interceptors)
- );
-
- const request = {
- params: {
- path: params,
- query: extractQueryParams(queryParams),
- },
- method,
- path,
- headers,
- upgrade,
- socket,
- handler: handlerFn,
- ref: reqHandle,
- };
-
- return await handlerFn(request);
-};
-
-export const makeRequestListener = table => async (req, res) => {
- const { status, headers, body } = await handleRequest(table, {
- ...req,
- upgrade: false,
- socket: null,
- headers: req.headers, /// API docs mention getHeaders(), but req doesn't have it...
- });
- res.writeHead(status, headers);
- res.end(body);
-};
-
-export const makeUpgradeListener = table => async (req, socket, _head) => {
- assert.ok(req.upgrade);
- const { status, headers, body } = await handleRequest(table, {
- ...req,
- socket,
- headers: req.headers,
- });
- writeHead(socket, status, headers);
- socket.write(body);
- if (status !== 101) {
- socket.end();
- }
-};
-
-export const actionsFn = ({
- logger = log,
-} = {}) => ({
- "toggle-debug-env": action => {
- const before = process.env.DEBUG;
- if (process.env.DEBUG) {
- delete process.env.DEBUG;
- } else {
- process.env.DEBUG = "1";
- }
- const after = process.env.DEBUG;
-
- logger.info({
- type: "action-response",
- action,
- message: "toggle process.env.DEBUG",
- before: u.undefinedAsNull(before),
- after: u.undefinedAsNull(after),
- });
- },
- "config-dump": action => logger.info({
- type: "action-response",
- action,
- data: {
- ...loggerDefaults,
- ...loggerGlobals,
- },
- }),
- "ping": _ => logger.info({ message: "pong" }),
-});
-
-export const actions = actionsFn();
-
-export const lineHandlerFn = ({
- logger = log,
- actionsMap = actions,
-} = {}) => line => {
- let cmd = null;
- try {
- cmd = JSON.parse(line);
- } catch (e) {
- logger.info({
- type: "invalid-cmd-input",
- message: e.message,
- });
- return;
- }
-
- if (typeof cmd?.action !== "string") {
- logger.info({
- type: "missing-key-action",
- message: `missing the "action" key from the given object`,
- });
- return;
- }
-
- const fn = actionsMap[cmd.action];
- if (!fn) {
- logger.info({
- type: "unsupported-action",
- message: `can't run given action: ${cmd.action}`,
- });
- return;
- }
-
- return fn(cmd.action, ...(cmd?.args || []));
-};
-
-export const lineHandler = lineHandlerFn();
-
-export const rmIf = path => {
- if (fs.existsSync(path)) {
- fs.unlinkSync(path);
- }
-};
-
-export const mkfifo = path =>
- child_process.execFileSync("mkfifo", [path]);
-
-export const makeLineEmitter = fn => {
- let data = "";
- return chunk => {
- const segments = chunk.split("\n");
- assert.ok(segments.length > 0);
-
- if (segments.length === 1) {
- data += segments[0];
- return;
- }
-
- [
- data + u.first(segments),
- ...u.butlast(u.rest(segments)),
- ].forEach(fn);
- data = u.last(segments);
- };
-};
-
-export const makeReopeningPipeReader = (shouldReopenPipe, path, {
- lineFn,
- logger,
-} = {}, out) => {
- const reader = fs.createReadStream(path, "UTF-8")
- out.ref = reader;
- reader.on("data", makeLineEmitter(lineFn));
- reader.on("close", () => {
- if (shouldReopenPipe.ref) {
- logger.debug({
- message: "pipe closed, reopening",
- });
- makeReopeningPipeReader(
- shouldReopenPipe,
- path,
- { lineFn, logger },
- out,
- );
- return;
- }
-
- logger.debug({
- message: "pipe closed, NOT reopening",
- });
- });
-};
-
-export const makePipeReaderFn = ({
- lineFn = lineHandler,
- logger = log,
-} = {}) => path => {
- mkfifo(path);
- let shouldReopenPipe = { ref: true };
- const pipe = {};
- makeReopeningPipeReader(
- shouldReopenPipe,
- path,
- { lineFn, logger },
- pipe,
- );
- return () => new Promise((resolve, reject) => {
- shouldReopenPipe.ref = false;
- fs.createWriteStream(path).end().close();
- pipe.ref.on("close", resolve);
- });
-};
-
-export const makePipeReader = makePipeReaderFn();
-
-export const buildRoutes = (routes, globalInterceptors = []) =>
- routes.reduce(
- (acc, [methods, path, handlerFn, interceptors = []]) =>
- addRoute(
- acc,
- methods,
- path,
- wrapHandler(
- handlerFn,
- globalInterceptors.concat(interceptors),
- ),
- ),
- {}
- );
-
-export const buildTable = (routes, globalInterceptors = []) =>
- u.assocIn(
- buildRoutes(routes, globalInterceptors),
- ["interceptors"],
- globalInterceptors,
- );
-
-export const promisifyServer = (name, serverHandle, socket, pipe) => {
- let closePipeFn = null;
- return {
- ref: serverHandle,
- info: () => ({ name, socket, pipe }),
- start: util.promisify((...args) => {
- assert.equal(typeof socket, "string");
- assert.equal(typeof pipe, "string");
-
- configLogger({ name });
-
- log.info({
- type: "starting-server",
- name,
- socket,
- pipe,
- node: {
- version: process.version,
- versions: process.versions,
- },
- });
-
- rmIf(pipe);
- closePipeFn = makePipeReader(pipe);
-
- rmIf(socket);
- return serverHandle.listen(socket, ...args)
- }),
- stop: util.promisify(async (...args) => {
- log.info({
- type: "stopping-server",
- name,
- socket,
- pipe,
- node: {
- version: process.version,
- versions: process.versions,
- },
- });
-
- await closePipeFn();
- return serverHandle.close(...args);
- }),
- events: serverHandle,
- };
-};
-
-export const buildServer = ({
- name = path.basename(process.cwd()),
- routes = [],
- socket = `${name}.socket`,
- pipe = `${name}.pipe`,
- globalInterceptors = defaultInterceptors,
-} = {}) => {
- const table = buildTable(routes, globalInterceptors);
- const requestListener = makeRequestListener(table);
- const server = http.createServer(requestListener);
- return promisifyServer(name, server, socket, pipe);
-};
diff --git a/src/web.mjs b/src/web.mjs
index b13640b..69be59c 100644
--- a/src/web.mjs
+++ b/src/web.mjs
@@ -4,8 +4,22 @@ import * as hero from "./hero.mjs";
const name = "papo";
-const newConnection = (req, ws) => {
- console.log({ ws, req });
+const newConnection = (req) => {
+ const { socket, websocket } = req;
+ websocket.onmessage(message => {
+ console.log({ message });
+ });
+ // console.log({ req, socket, websocket });
+ // websocket.onclose(() => console.log("closed"));
+ // websocket.onerror(() => console.log("errored"));
+ /*
+ req.socket.on("data", data => {
+ console.log("antes");
+ console.log({ data });
+ console.log({ data: new Uint8Array(data) });
+ console.log("depois");
+ });
+ */
// ws.on("error", console.error);
// ws.on("message", x => console.log(x.toString()));
// ws.send("hello from the server");
diff --git a/tests/js/hero.mjs b/tests/js/hero.mjs
deleted file mode 100644
index 6390ad0..0000000
--- a/tests/js/hero.mjs
+++ /dev/null
@@ -1,2430 +0,0 @@
-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: "<br />",
- }, next),
- {
- status: 200,
- body: "<br />",
- 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,
-]);