summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/hero.mjs208
-rw-r--r--tests/js/hero.mjs982
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,