diff options
author | EuAndreh <eu@euandre.org> | 2024-03-21 09:43:43 -0300 |
---|---|---|
committer | EuAndreh <eu@euandre.org> | 2024-03-21 09:45:43 -0300 |
commit | e7e60205bda309c8aaecc36a03f6c3ad0ec84cd2 (patch) | |
tree | 555dd577672eaabb6c85b8a013865a252f9e5a62 | |
parent | tests/rand.c: s/Taken/Derived/ (diff) | |
download | papod-e7e60205bda309c8aaecc36a03f6c3ad0ec84cd2.tar.gz papod-e7e60205bda309c8aaecc36a03f6c3ad0ec84cd2.tar.xz |
src/hero.mjs: Retire code
-rw-r--r-- | deps.mk | 2 | ||||
-rw-r--r-- | src/hero.mjs | 695 | ||||
-rw-r--r-- | src/web.mjs | 18 | ||||
-rw-r--r-- | tests/js/hero.mjs | 2430 |
4 files changed, 16 insertions, 3129 deletions
@@ -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, -]); |