diff options
-rw-r--r-- | src/hero.mjs | 208 | ||||
-rw-r--r-- | tests/js/hero.mjs | 982 |
2 files changed, 595 insertions, 595 deletions
diff --git a/src/hero.mjs b/src/hero.mjs index 2355bbf..9c6df64 100644 --- a/src/hero.mjs +++ b/src/hero.mjs @@ -9,6 +9,110 @@ 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, level, o) => + writerFn(JSON.stringify({ + ...loggerDefaults, + ...loggerGlobals, + level, + ...o, + })); + +export const makeLogger = (writerFn = console.error) => ({ + debug: (...args) => process.env.DEBUG ? + logit(writerFn, "DEBUG", ...args) : + null, + info: u.partial(logit, writerFn, "INFO"), + warn: u.partial(logit, writerFn, "WARN"), + error: u.partial(logit, writerFn, "ERROR"), +}); + +export const log = makeLogger(); + +export const interceptorsFn = ({ + uuidFn = crypto.randomUUID, + logger = log, +} = {}) => ({ + requestId: (req, next) => next({ ...req, id: uuidFn() }), + logged: async (req, next) => { + const { id, url, method } = req; + logger.info({ + id, + url, + method, + type: "in-request", + }); + const response = await next(req); + const { status } = response; + logger.info({ + id, + status, + type: "in-response", + }); + return response; + }, + contentType: async (req, next) => { + const response = await next(req); + const [mimeType, body] = typeof response.body === "string" ? + ["text/html", response.body] : + ["application/json", JSON.stringify(response.body) || ""]; + return { + ...response, + body, + headers: { + "Content-Type": mimeType, + "Content-Length": Buffer.byteLength(body), + ...(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, + }); + return { + status: 500, + body: "Internal Server Error\n", + }; + } + }, +}); + +export const interceptors = interceptorsFn(); + +export const defaultInterceptors = [ + interceptors.serverError, + interceptors.contentType, + interceptors.requestId, + interceptors.logged, +]; + +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 : @@ -155,110 +259,6 @@ export const makeRequestListener = table => async (req, res) => { res.end(response.body); }; -export const loggerDefaults = { - pid: process.pid, - ppid: process.ppid, - tool: "hero", -}; - -export let loggerGlobals = {}; - -export const configLogger = o => loggerGlobals = o; - -export const logit = (writerFn, level, o) => - writerFn(JSON.stringify({ - ...loggerDefaults, - ...loggerGlobals, - level, - ...o, - })); - -export const makeLogger = (writerFn = console.error) => ({ - debug: (...args) => process.env.DEBUG ? - logit(writerFn, "DEBUG", ...args) : - null, - info: u.partial(logit, writerFn, "INFO"), - warn: u.partial(logit, writerFn, "WARN"), - error: u.partial(logit, writerFn, "ERROR"), -}); - -export const log = makeLogger(); - -export const interceptorsFn = ({ - uuidFn = crypto.randomUUID, - logger = log, -} = {}) => ({ - requestId: (req, next) => next({ ...req, id: uuidFn() }), - logged: async (req, next) => { - const { id, url, method } = req; - logger.info({ - id, - url, - method, - type: "in-request", - }); - const response = await next(req); - const { status } = response; - logger.info({ - id, - status, - type: "in-response", - }); - return response; - }, - contentType: async (req, next) => { - const response = await next(req); - const [mimeType, body] = typeof response.body === "string" ? - ["text/html", response.body] : - ["application/json", JSON.stringify(response.body) || ""]; - return { - ...response, - body, - headers: { - "Content-Type": mimeType, - "Content-Length": Buffer.byteLength(body), - ...(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, - }); - return { - status: 500, - body: "Internal Server Error\n", - }; - } - }, -}); - -export const interceptors = interceptorsFn(); - -export const defaultInterceptors = [ - interceptors.serverError, - interceptors.contentType, - interceptors.requestId, - interceptors.logged, -]; - -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 actionsFn = ({ logger = log, } = {}) => ({ diff --git a/tests/js/hero.mjs b/tests/js/hero.mjs index a8883d5..9a5cf25 100644 --- a/tests/js/hero.mjs +++ b/tests/js/hero.mjs @@ -7,6 +7,16 @@ import procees from "node:process"; import * as runner from "../runner.mjs"; import * as u from "../../src/utils.mjs"; import { + loggerDefaults, + loggerGlobals, + configLogger, + logit, + makeLogger, + interceptorsFn, + interceptors, + defaultInterceptors, + chainInterceptors, + wrapHandler, normalizeSegments, pathToSegments, hasPathParams, @@ -20,16 +30,6 @@ import { make404Handler, handleRequest, makeRequestListener, - loggerDefaults, - loggerGlobals, - configLogger, - logit, - makeLogger, - interceptorsFn, - interceptors, - defaultInterceptors, - chainInterceptors, - wrapHandler, actionsFn, lineHandlerFn, rmIf, @@ -44,6 +44,481 @@ import { } from "../../src/hero.mjs"; +const test_configLogger = async t => { + t.start("configLogger()"); + + await t.test("globals starts empty", () => { + assert.deepEqual(loggerGlobals, {}); + }); + + await t.test("is gets becomes we assign it", () => { + const globals = { + app: "my-app", + color: "green", + version: "deadbeef", + }; + configLogger(globals); + assert.deepEqual(loggerGlobals, globals); + }); + + await t.test("we can reset it", () => { + configLogger({}); + assert.deepEqual(loggerGlobals, {}); + }); +}; + +const test_logit = async t => { + t.start("logit()"); + + await t.test("we can log data", () => { + configLogger({ app: "hero-based app" }); + const contents = []; + const writerFn = x => contents.push(x); + + logit(writerFn, "my level", { a: 1, type: "log-test" }); + assert.deepEqual(contents.map(JSON.parse), [{ + ...loggerDefaults, + app: "hero-based app", + level: "my level", + a: 1, + type: "log-test", + }]); + + /// reset the logger out of the values specific to this test + configLogger({}); + }); + + await t.test("the object can change previous fallback values", () => { + const contents = []; + const writerFn = x => contents.push(x); + + configLogger({ + level: "unseen", + a: "unseen", + }); + logit(writerFn, "overwritten by level", { a: "overwritten by o" }); + + configLogger({ + pid: "overwritten by loggerGlobals", + }); + logit(writerFn, "unseen", { level: "overwritten by o" }); + + /// reset the logger out of the values specific to this test + configLogger({}); + + assert.deepEqual(contents.map(JSON.parse), [ + { + ...loggerDefaults, + pid: process.pid, + level: "overwritten by level", + a: "overwritten by o", + }, + { + ...loggerDefaults, + pid: "overwritten by loggerGlobals", + level: "overwritten by o", + }, + ]); + + }); + + await t.test("we can't log unserializable things", () => { + const obj = { self: null }; + obj.self = obj; + assert.throws(() => logit(obj), TypeError); + }); +}; + +const test_makeLogger = async t => { + t.start("makeLogger()"); + + await t.test("various log levels", () => { + const contents = []; + const writerFn = x => contents.push(x); + const log = makeLogger(writerFn); + + log.info ({ type: "expected" }); + log.warn ({ type: "worrysome" }); + log.error({ type: "bad" }); + assert.deepEqual(contents.map(JSON.parse), [ + { + ...loggerDefaults, + level: "INFO", + type: "expected", + }, + { + ...loggerDefaults, + level: "WARN", + type: "worrysome", + }, + { + ...loggerDefaults, + level: "ERROR", + type: "bad", + }, + ]); + }); + + await t.test("debug only works when $DEBUG is set", () => { + const contents = []; + const writerFn = x => contents.push(x); + const log = makeLogger(writerFn); + + const previous = process.env.DEBUG; + delete process.env.DEBUG; + + log.debug({ x: "ignored" }); + process.env.DEBUG = "1"; + log.debug({ x: "seen" }); + delete process.env.DEBUG; + /// call the function that toggles + log.debug({ x: "ignored" }); + + assert.deepEqual(contents.map(JSON.parse), [{ + ...loggerDefaults, + level: "DEBUG", + x: "seen", + }]); + + process.env.DEBUG = previous; + }); +}; + +const test_interceptorsFn = async t => { + const next = x => x; + + { + t.start("interceptorsFn().requestId()"); + + let i = 0; + const uuidFn = () => `${i++}`; + + await t.test("we add an id to whatever we receive", () => { + assert.deepEqual( + interceptorsFn({uuidFn}).requestId({}, next), + { + id: "0", + }, + ); + assert.deepEqual( + interceptorsFn({uuidFn}).requestId({ a: "existing data" }, next), + { + a: "existing data", + id: "1", + }, + ); + }); + + await t.test(`we overwrite the "id" if it already exists`, async () => { + assert.deepEqual( + interceptorsFn({uuidFn}).requestId({ id: "before" }, next), + { + id: "2", + }, + ); + }); + }; + + { + t.start("interceptorsFn().logged()"); + + await t.test("we log before and after next()", async () => { + const contents = []; + const logger = { info: x => contents.push(x) }; + const status = 201; + const req = { + id: "an ID", + url: "a URL", + method: "a method", + }; + + assert.deepEqual( + await interceptorsFn({logger}).logged(req, _ => ({ status })), + { status }, + ); + assert.deepEqual( + contents, + [ + { ...req, type: "in-request" }, + { id: req.id, status, type: "in-response" }, + ], + ); + }); + }; + + { + t.start("interceptorsFn().contentType()"); + + await t.test("empty values", async () => { + assert.deepEqual( + await interceptorsFn().contentType({}, next), + { + body: "", + headers: { + "Content-Type": "application/json", + "Content-Length": 0, + }, + }, + ); + + assert.deepEqual( + await interceptorsFn().contentType({ body: "" }, next), + { + body: "", + headers: { + "Content-Type": "text/html", + "Content-Length": 0, + }, + }, + ); + }); + + await t.test("body values", async () => { + assert.deepEqual( + await interceptorsFn().contentType({ body: { a: 1 }}, next), + { + body: `{"a":1}`, + headers: { + "Content-Type": "application/json", + "Content-Length": 7, + }, + }, + ); + + assert.deepEqual( + await interceptorsFn().contentType({ body: "<br />" }, next), + { + body: "<br />", + headers: { + "Content-Type": "text/html", + "Content-Length": 6, + }, + }, + ); + }); + + await t.test("header values preference", async () => { + assert.deepEqual( + await interceptorsFn().contentType({ + body: "", + headers: { + "Content-Type": "we have preference", + "Content-Length": "and so do we", + }, + }, next), + { + body: "", + headers: { + "Content-Type": "we have preference", + "Content-Length": "and so do we", + }, + }, + ); + }); + + await t.test("headers get propagated", async () => { + assert.deepEqual( + await interceptorsFn().contentType({ + body: "", + headers: { + "My": "Header", + }, + }, next), + { + body: "", + headers: { + "My": "Header", + "Content-Type": "text/html", + "Content-Length": 0, + }, + }, + ); + }); + }; + + { + t.start("interceptorsFn().serverError()"); + + await t.test("no-op when no error occurs", async () => { + assert.deepEqual( + await interceptorsFn().serverError({ status: 1 }, next), + { status: 1 }, + ); + }); + + await t.test(`an error is thrown if "status" is missing`, async () => { + const contents = []; + const logger = { error: x => contents.push(x) }; + assert.deepEqual( + await interceptorsFn({ logger }).serverError({ id: 123 }, next), + { + status: 500, + body: "Internal Server Error\n", + }, + ); + assert.deepEqual( + contents, + [{ + id: 123, + type: "server-error-interceptor", + message: `Missing "status"`, + }], + ); + }); + + await t.test("we turn a handler error into a 500 response", async () => { + const contents = []; + const logger = { error: x => contents.push(x) }; + assert.deepEqual( + await interceptorsFn({ logger }).serverError( + { id: "some ID" }, + _ => { throw new Error("My test error message"); }, + ), + { + status: 500, + body: "Internal Server Error\n", + }, + ); + assert.deepEqual( + contents, + [{ + id: "some ID", + type: "server-error-interceptor", + message: "My test error message", + }], + ); + }); + }; +}; + +const test_chainInterceptors = async t => { + t.start("chainInterceptors()"); + + await t.test("empty values", () => { + assert.equal(chainInterceptors([])("req"), "req"); + }); + + await t.test("the order of interceptors matter", () => { + const a = []; + + const i1 = (req, next) => { + a.push("i1"); + return next(req); + }; + const i2 = (req, next) => { + a.push("i2"); + return next(req); + }; + + assert.equal(chainInterceptors([i1, i2])("req"), "req"); + assert.deepEqual(a, ["i1", "i2"]); + assert.equal(chainInterceptors([i2, i1])("req"), "req"); + assert.deepEqual(a, ["i1", "i2", "i2", "i1"]); + }); + + await t.test("with ordering, interceptors implicitly depend on each other", () => { + const i1 = (req, next) => next({ ...req, id: 1 }); + const i2 = (req, next) => next({ ...req, id: req.id + 1 }); + + assert.deepEqual( + chainInterceptors([i1, i2])({}), + { id: 2 }, + ); + + assert.deepEqual( + chainInterceptors([i2])({}), + { id: NaN }, + ); + + assert.deepEqual( + chainInterceptors([i2, i1])({}), + { id: 1 }, + ); + }); + + await t.test("we can chain async interceptors", async () => { + const i1 = async (req, next) => await next({ ...req, i1: true }); + const i2 = async (req, next) => await next({ ...req, i2: true }); + + assert.deepEqual( + await chainInterceptors([i1, i2])({}), + { + i1: true, + i2: true, + }, + ); + }); +}; + +const test_wrapHandler = async t => { + t.start("wrapHandler()"); + + await t.test("noop when arr is empty", () => { + const fn = () => {}; + const wrappedFn1 = wrapHandler(fn, []); + assert.deepEqual(fn, wrappedFn1); + + const wrappedFn2 = wrapHandler(fn, [ interceptors.requestId ]); + assert.notDeepEqual(fn, wrappedFn2); + }); + + await t.test("a handler with chained interceptors change its behaviour", async () => { + let i = 0; + const uuidFn = () => `${i++}`; + + const contents = []; + const logger = { info: x => contents.push(x) }; + + const interceptors = interceptorsFn({uuidFn, logger}); + + const fn = async _ => await { status: 1, body: { a: 1 }}; + const wrappedFn = wrapHandler(fn, [ + interceptors.requestId, + interceptors.logged, + interceptors.contentType, + ]); + + const req = { + url: "URL", + method: "METHOD", + }; + + assert.deepEqual( + await fn(req), + { status: 1, body: { a: 1 }}, + ); + assert.deepEqual(contents, []); + + assert.deepEqual( + await wrappedFn(req), + { + status: 1, + body: `{"a":1}`, + headers: { + "Content-Type": "application/json", + "Content-Length": 7, + }, + }, + ); + assert.deepEqual( + contents, + [ + { + id: "0", + url: "URL", + method: "METHOD", + type: "in-request", + }, + { + id: "0", + status: 1, + type: "in-response", + }, + ], + ); + }); +}; + const test_normalizeSegments = async t => { t.start("normalizeSegments()"); @@ -661,481 +1136,6 @@ const test_makeRequestListener = async t => { }); }; -const test_configLogger = async t => { - t.start("configLogger()"); - - await t.test("globals starts empty", () => { - assert.deepEqual(loggerGlobals, {}); - }); - - await t.test("is gets becomes we assign it", () => { - const globals = { - app: "my-app", - color: "green", - version: "deadbeef", - }; - configLogger(globals); - assert.deepEqual(loggerGlobals, globals); - }); - - await t.test("we can reset it", () => { - configLogger({}); - assert.deepEqual(loggerGlobals, {}); - }); -}; - -const test_logit = async t => { - t.start("logit()"); - - await t.test("we can log data", () => { - configLogger({ app: "hero-based app" }); - const contents = []; - const writerFn = x => contents.push(x); - - logit(writerFn, "my level", { a: 1, type: "log-test" }); - assert.deepEqual(contents.map(JSON.parse), [{ - ...loggerDefaults, - app: "hero-based app", - level: "my level", - a: 1, - type: "log-test", - }]); - - /// reset the logger out of the values specific to this test - configLogger({}); - }); - - await t.test("the object can change previous fallback values", () => { - const contents = []; - const writerFn = x => contents.push(x); - - configLogger({ - level: "unseen", - a: "unseen", - }); - logit(writerFn, "overwritten by level", { a: "overwritten by o" }); - - configLogger({ - pid: "overwritten by loggerGlobals", - }); - logit(writerFn, "unseen", { level: "overwritten by o" }); - - /// reset the logger out of the values specific to this test - configLogger({}); - - assert.deepEqual(contents.map(JSON.parse), [ - { - ...loggerDefaults, - pid: process.pid, - level: "overwritten by level", - a: "overwritten by o", - }, - { - ...loggerDefaults, - pid: "overwritten by loggerGlobals", - level: "overwritten by o", - }, - ]); - - }); - - await t.test("we can't log unserializable things", () => { - const obj = { self: null }; - obj.self = obj; - assert.throws(() => logit(obj), TypeError); - }); -}; - -const test_makeLogger = async t => { - t.start("makeLogger()"); - - await t.test("various log levels", () => { - const contents = []; - const writerFn = x => contents.push(x); - const log = makeLogger(writerFn); - - log.info ({ type: "expected" }); - log.warn ({ type: "worrysome" }); - log.error({ type: "bad" }); - assert.deepEqual(contents.map(JSON.parse), [ - { - ...loggerDefaults, - level: "INFO", - type: "expected", - }, - { - ...loggerDefaults, - level: "WARN", - type: "worrysome", - }, - { - ...loggerDefaults, - level: "ERROR", - type: "bad", - }, - ]); - }); - - await t.test("debug only works when $DEBUG is set", () => { - const contents = []; - const writerFn = x => contents.push(x); - const log = makeLogger(writerFn); - - const previous = process.env.DEBUG; - delete process.env.DEBUG; - - log.debug({ x: "ignored" }); - process.env.DEBUG = "1"; - log.debug({ x: "seen" }); - delete process.env.DEBUG; - /// call the function that toggles - log.debug({ x: "ignored" }); - - assert.deepEqual(contents.map(JSON.parse), [{ - ...loggerDefaults, - level: "DEBUG", - x: "seen", - }]); - - process.env.DEBUG = previous; - }); -}; - -const test_interceptorsFn = async t => { - const next = x => x; - - { - t.start("interceptorsFn().requestId()"); - - let i = 0; - const uuidFn = () => `${i++}`; - - await t.test("we add an id to whatever we receive", () => { - assert.deepEqual( - interceptorsFn({uuidFn}).requestId({}, next), - { - id: "0", - }, - ); - assert.deepEqual( - interceptorsFn({uuidFn}).requestId({ a: "existing data" }, next), - { - a: "existing data", - id: "1", - }, - ); - }); - - await t.test(`we overwrite the "id" if it already exists`, async () => { - assert.deepEqual( - interceptorsFn({uuidFn}).requestId({ id: "before" }, next), - { - id: "2", - }, - ); - }); - }; - - { - t.start("interceptorsFn().logged()"); - - await t.test("we log before and after next()", async () => { - const contents = []; - const logger = { info: x => contents.push(x) }; - const status = 201; - const req = { - id: "an ID", - url: "a URL", - method: "a method", - }; - - assert.deepEqual( - await interceptorsFn({logger}).logged(req, _ => ({ status })), - { status }, - ); - assert.deepEqual( - contents, - [ - { ...req, type: "in-request" }, - { id: req.id, status, type: "in-response" }, - ], - ); - }); - }; - - { - t.start("interceptorsFn().contentType()"); - - await t.test("empty values", async () => { - assert.deepEqual( - await interceptorsFn().contentType({}, next), - { - body: "", - headers: { - "Content-Type": "application/json", - "Content-Length": 0, - }, - }, - ); - - assert.deepEqual( - await interceptorsFn().contentType({ body: "" }, next), - { - body: "", - headers: { - "Content-Type": "text/html", - "Content-Length": 0, - }, - }, - ); - }); - - await t.test("body values", async () => { - assert.deepEqual( - await interceptorsFn().contentType({ body: { a: 1 }}, next), - { - body: `{"a":1}`, - headers: { - "Content-Type": "application/json", - "Content-Length": 7, - }, - }, - ); - - assert.deepEqual( - await interceptorsFn().contentType({ body: "<br />" }, next), - { - body: "<br />", - headers: { - "Content-Type": "text/html", - "Content-Length": 6, - }, - }, - ); - }); - - await t.test("header values preference", async () => { - assert.deepEqual( - await interceptorsFn().contentType({ - body: "", - headers: { - "Content-Type": "we have preference", - "Content-Length": "and so do we", - }, - }, next), - { - body: "", - headers: { - "Content-Type": "we have preference", - "Content-Length": "and so do we", - }, - }, - ); - }); - - await t.test("headers get propagated", async () => { - assert.deepEqual( - await interceptorsFn().contentType({ - body: "", - headers: { - "My": "Header", - }, - }, next), - { - body: "", - headers: { - "My": "Header", - "Content-Type": "text/html", - "Content-Length": 0, - }, - }, - ); - }); - }; - - { - t.start("interceptorsFn().serverError()"); - - await t.test("no-op when no error occurs", async () => { - assert.deepEqual( - await interceptorsFn().serverError({ status: 1 }, next), - { status: 1 }, - ); - }); - - await t.test(`an error is thrown if "status" is missing`, async () => { - const contents = []; - const logger = { error: x => contents.push(x) }; - assert.deepEqual( - await interceptorsFn({ logger }).serverError({ id: 123 }, next), - { - status: 500, - body: "Internal Server Error\n", - }, - ); - assert.deepEqual( - contents, - [{ - id: 123, - type: "server-error-interceptor", - message: `Missing "status"`, - }], - ); - }); - - await t.test("we turn a handler error into a 500 response", async () => { - const contents = []; - const logger = { error: x => contents.push(x) }; - assert.deepEqual( - await interceptorsFn({ logger }).serverError( - { id: "some ID" }, - _ => { throw new Error("My test error message"); }, - ), - { - status: 500, - body: "Internal Server Error\n", - }, - ); - assert.deepEqual( - contents, - [{ - id: "some ID", - type: "server-error-interceptor", - message: "My test error message", - }], - ); - }); - }; -}; - -const test_chainInterceptors = async t => { - t.start("chainInterceptors()"); - - await t.test("empty values", () => { - assert.equal(chainInterceptors([])("req"), "req"); - }); - - await t.test("the order of interceptors matter", () => { - const a = []; - - const i1 = (req, next) => { - a.push("i1"); - return next(req); - }; - const i2 = (req, next) => { - a.push("i2"); - return next(req); - }; - - assert.equal(chainInterceptors([i1, i2])("req"), "req"); - assert.deepEqual(a, ["i1", "i2"]); - assert.equal(chainInterceptors([i2, i1])("req"), "req"); - assert.deepEqual(a, ["i1", "i2", "i2", "i1"]); - }); - - await t.test("with ordering, interceptors implicitly depend on each other", () => { - const i1 = (req, next) => next({ ...req, id: 1 }); - const i2 = (req, next) => next({ ...req, id: req.id + 1 }); - - assert.deepEqual( - chainInterceptors([i1, i2])({}), - { id: 2 }, - ); - - assert.deepEqual( - chainInterceptors([i2])({}), - { id: NaN }, - ); - - assert.deepEqual( - chainInterceptors([i2, i1])({}), - { id: 1 }, - ); - }); - - await t.test("we can chain async interceptors", async () => { - const i1 = async (req, next) => await next({ ...req, i1: true }); - const i2 = async (req, next) => await next({ ...req, i2: true }); - - assert.deepEqual( - await chainInterceptors([i1, i2])({}), - { - i1: true, - i2: true, - }, - ); - }); -}; - -const test_wrapHandler = async t => { - t.start("wrapHandler()"); - - await t.test("noop when arr is empty", () => { - const fn = () => {}; - const wrappedFn1 = wrapHandler(fn, []); - assert.deepEqual(fn, wrappedFn1); - - const wrappedFn2 = wrapHandler(fn, [ interceptors.requestId ]); - assert.notDeepEqual(fn, wrappedFn2); - }); - - await t.test("a handler with chained interceptors change its behaviour", async () => { - let i = 0; - const uuidFn = () => `${i++}`; - - const contents = []; - const logger = { info: x => contents.push(x) }; - - const interceptors = interceptorsFn({uuidFn, logger}); - - const fn = async _ => await { status: 1, body: { a: 1 }}; - const wrappedFn = wrapHandler(fn, [ - interceptors.requestId, - interceptors.logged, - interceptors.contentType, - ]); - - const req = { - url: "URL", - method: "METHOD", - }; - - assert.deepEqual( - await fn(req), - { status: 1, body: { a: 1 }}, - ); - assert.deepEqual(contents, []); - - assert.deepEqual( - await wrappedFn(req), - { - status: 1, - body: `{"a":1}`, - headers: { - "Content-Type": "application/json", - "Content-Length": 7, - }, - }, - ); - assert.deepEqual( - contents, - [ - { - id: "0", - url: "URL", - method: "METHOD", - type: "in-request", - }, - { - id: "0", - status: 1, - type: "in-response", - }, - ], - ); - }); -}; - const test_actionsFn = async t => { { t.start(`actionsFn()["toggle-debug-env"()]`); @@ -1734,6 +1734,12 @@ const test_buildServer = async t => { await runner.runTests([ + test_configLogger, + test_logit, + test_makeLogger, + test_interceptorsFn, + test_chainInterceptors, + test_wrapHandler, test_normalizeSegments, test_pathToSegments, test_hasPathParams, @@ -1746,12 +1752,6 @@ await runner.runTests([ test_make404Handler, test_handleRequest, test_makeRequestListener, - test_configLogger, - test_logit, - test_makeLogger, - test_interceptorsFn, - test_chainInterceptors, - test_wrapHandler, test_actionsFn, test_lineHandlerFn, test_buildRoutes, |