summaryrefslogtreecommitdiff
path: root/tests
diff options
context:
space:
mode:
Diffstat (limited to 'tests')
-rw-r--r--tests/js/db.js16
-rw-r--r--tests/js/db.mjs15
-rw-r--r--tests/js/escape.mjs62
-rw-r--r--tests/js/hero.mjs1050
-rw-r--r--tests/js/ircd.js7
-rw-r--r--tests/js/ircd.mjs7
-rw-r--r--tests/js/utils.js130
-rw-r--r--tests/js/utils.mjs225
-rw-r--r--tests/js/web.js7
-rw-r--r--tests/js/web.mjs7
-rw-r--r--tests/runner.mjs (renamed from tests/runner.js)8
11 files changed, 1368 insertions, 166 deletions
diff --git a/tests/js/db.js b/tests/js/db.js
deleted file mode 100644
index c99dd1b..0000000
--- a/tests/js/db.js
+++ /dev/null
@@ -1,16 +0,0 @@
-const { runTests } = require("../runner.js");
-const { init } = require("../../src/db.js");
-
-
-const test_init = t => {
- t.start("init()");
- t.test("FIXME", () => {
- // init();
- });
-};
-
-const tests = [
- test_init,
-];
-
-runTests(tests);
diff --git a/tests/js/db.mjs b/tests/js/db.mjs
new file mode 100644
index 0000000..e4a8a51
--- /dev/null
+++ b/tests/js/db.mjs
@@ -0,0 +1,15 @@
+import { runTests } from "../runner.mjs";
+import { init } from "../../src/db.mjs";
+
+
+const test_init = t => {
+ t.start("init()");
+ t.test("FIXME", () => {
+ // init();
+ });
+};
+
+
+await runTests([
+ test_init,
+]);
diff --git a/tests/js/escape.mjs b/tests/js/escape.mjs
new file mode 100644
index 0000000..2cad16a
--- /dev/null
+++ b/tests/js/escape.mjs
@@ -0,0 +1,62 @@
+import assert from "node:assert/strict";
+import { runTests } from "../runner.mjs";
+import { escape } from "../../src/escape.mjs";
+
+const test_escape = t => {
+ t.start("escape()");
+
+ t.test("numbers", () => {
+ assert.equal(escape(0), "0");
+ assert.equal(escape(42), "42");
+ assert.equal(escape(-1), "-1");
+ });
+
+ t.test("object", () => {
+ assert.equal(escape({}), "[object Object]");
+ assert.equal(escape({ k: "v" }), "[object Object]");
+ });
+
+ t.test("string with special chars", () => {
+ assert.strictEqual(escape(`"`), """);
+ assert.strictEqual(escape(`"bar`), ""bar");
+ assert.strictEqual(escape(`foo"`), "foo"");
+ assert.strictEqual(escape(`foo"bar`), "foo"bar");
+ assert.strictEqual(escape(`foo""bar`), "foo""bar");
+
+ assert.strictEqual(escape("&"), "&");
+ assert.strictEqual(escape("&bar"), "&bar");
+ assert.strictEqual(escape("foo&"), "foo&");
+ assert.strictEqual(escape("foo&bar"), "foo&bar");
+ assert.strictEqual(escape("foo&&bar"), "foo&&bar");
+
+ assert.strictEqual(escape("'"), "'");
+ assert.strictEqual(escape("'bar"), "'bar");
+ assert.strictEqual(escape("foo'"), "foo'");
+ assert.strictEqual(escape("foo'bar"), "foo'bar");
+ assert.strictEqual(escape("foo''bar"), "foo''bar");
+
+ assert.strictEqual(escape("<"), "&lt;");
+ assert.strictEqual(escape("<bar"), "&lt;bar");
+ assert.strictEqual(escape("foo<"), "foo&lt;");
+ assert.strictEqual(escape("foo<bar"), "foo&lt;bar");
+ assert.strictEqual(escape("foo<<bar"), "foo&lt;&lt;bar");
+
+ assert.strictEqual(escape(">"), "&gt;");
+ assert.strictEqual(escape(">bar"), "&gt;bar");
+ assert.strictEqual(escape("foo>"), "foo&gt;");
+ assert.strictEqual(escape("foo>bar"), "foo&gt;bar");
+ assert.strictEqual(escape("foo>>bar"), "foo&gt;&gt;bar");
+ });
+
+ t.test("the combination of all special characters", () => {
+ assert.strictEqual(
+ escape(`foo, "bar", 'baz' & <quux>`),
+ "foo, &quot;bar&quot;, &#39;baz&#39; &amp; &lt;quux&gt;",
+ );
+ });
+};
+
+
+await runTests([
+ test_escape,
+]);
diff --git a/tests/js/hero.mjs b/tests/js/hero.mjs
new file mode 100644
index 0000000..09e6e54
--- /dev/null
+++ b/tests/js/hero.mjs
@@ -0,0 +1,1050 @@
+import assert from "node:assert/strict";
+import { runTests } from "../runner.mjs";
+import { assocIn } from "../../src/utils.mjs";
+import {
+ normalizeSegments,
+ pathToSegments,
+ hasPathParams,
+ addRoute,
+ buildRoutes,
+ findStaticHandler,
+ firstParamMatch,
+ findDynamicHandler,
+ findHandler,
+ extractQueryParams,
+ handleRequest,
+ makeRequestListener,
+ interceptorsFn,
+ chainInterceptors,
+ wrapHandler,
+} from "../../src/hero.mjs";
+
+
+const test_normalizeSegments = t => {
+ t.start("normalizeSegments()");
+
+ t.test("unchanged when already normalized", () => {
+ assert.deepEqual(normalizeSegments([""]), [""]);
+ });
+
+ 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 = t => {
+ t.start("pathToSegments()");
+
+ 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", ""]);
+ });
+
+ 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 = t => {
+ t.start("hasPathParam()");
+
+ t.test("has it", () => {
+ assert(hasPathParams(["some", ":path", ""]));
+ assert(hasPathParams(["path", ":params", ""]));
+ assert(hasPathParams(["api", "user", ":id", "info", ""]));
+ });
+
+ 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_addRoute = t => {
+ t.start("addRoute()");
+
+ const fn1 = () => {};
+
+ 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({}, ["PUT", "PATCH"], "/api/org/:orgid/member/:memberid", fn1),
+ {
+ dynamic: {
+ PUT: { api: { org: { ":orgid": { member: { ":memberid": { "": fn1 }}}}}},
+ PATCH: { api: { org: { ":orgid": { member: { ":memberid": { "": fn1 }}}}}},
+ },
+ },
+ );
+ });
+
+ t.test("bad method", () => {
+ assert.throws(
+ () => addRoute({}, "VERB", "/path", fn1),
+ assert.AssertionError,
+ );
+ });
+
+ t.test("empty methods array", () => {
+ assert.deepEqual(addRoute({}, [], "", fn1), {});
+ });
+
+ 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_buildRoutes = t => {
+ t.start("buildRoutes()");
+
+ t.test("empty values", () => {
+ assert.deepEqual(buildRoutes([]), {});
+ });
+
+ 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),
+ );
+ });
+
+ 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_findStaticHandler = t => {
+ t.start("findStaticHandler()");
+
+ t.test("multiple accesses to the same table", () => {
+ const fn1 = () => {};
+ const fn2 = () => {};
+ const fn3 = () => {};
+
+ const table = {
+ static: {
+ GET: {
+ api: {
+ home: { "": fn1 },
+ settings: { "": fn2 },
+ },
+ },
+ POST: {
+ api: {
+ settings: { "": fn3 },
+ },
+ },
+ },
+ };
+
+ assert.deepEqual(
+ findStaticHandler(table, "GET", [ "api", "home", "" ]),
+ { handlerFn: fn1, params: {} },
+ );
+ assert.deepEqual(
+ findStaticHandler(table, "PUT", [ "api", "home", "" ]),
+ null,
+ );
+
+ assert.deepEqual(
+ findStaticHandler(table, "GET", [ "api", "settings", "" ]),
+ { handlerFn: fn2, params: {} },
+ );
+ assert.deepEqual(
+ findStaticHandler(table, "POST", [ "api", "settings", "" ]),
+ { handlerFn: fn3, params: {} },
+ );
+ assert.deepEqual(
+ findStaticHandler(table, "PUT", [ "api", "settings", "" ]),
+ null,
+ );
+
+ assert.deepEqual(
+ findStaticHandler(table, "GET", [ "api", "profile", "" ]),
+ null,
+ );
+
+ assert.deepEqual(
+ findStaticHandler({}, "GET", [ "api", "profile", "" ]),
+ null,
+ );
+ });
+};
+
+const test_firstParamMatch = t => {
+ t.start("firstParamMatch()");
+
+ const params = {};
+ const fn1 = () => {};
+ const fn2 = () => {};
+
+ 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" }},
+ );
+ });
+
+ t.test("ambiguous route prefers params at the end", () => {
+ const segments = [ "path", "param1", "param2", "" ];
+
+ const tree1 = {
+ path: {
+ ":shallower": {
+ param2: {
+ "": fn2,
+ },
+ },
+ },
+ };
+
+ const tree2 = 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" }},
+ );
+ });
+
+ t.test("when 2 params are possible, we pick the first alphabetically", () => {
+ const segments = [ "user", "someId", "" ];
+
+ const tree1 = {
+ user: {
+ ":bbb": {
+ "": fn2,
+ },
+ },
+ };
+
+ const tree2 = 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" }},
+ );
+ });
+};
+
+const test_findDynamicHandler = t => {
+ t.start("findDynamicHandler()");
+
+ 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 = t => {
+ t.start("findHandler()");
+
+ t.test("mix of static and dynamic routes", () => {
+ const static1 = () => {};
+ const static2 = () => {};
+ const static3 = () => {};
+ const dynamic1 = () => {};
+ const dynamic2 = () => {};
+ const dynamic3 = () => {};
+ const dynamic4 = () => {};
+
+ const table = {
+ static: {
+ GET: {
+ user: {
+ "": static1,
+ },
+ pages: {
+ "": static2,
+ home: {
+ "": static3,
+ },
+ },
+ },
+ },
+ dynamic: {
+ GET: {
+ user: {
+ ":id": {
+ "": dynamic1,
+ },
+ },
+ },
+ PUT: {
+ user: {
+ ":id": {
+ "": dynamic2,
+ "info": {
+ "": dynamic3,
+ },
+ "preferences": {
+ "": dynamic4,
+ },
+ },
+ },
+ },
+ },
+ };
+
+ assert.deepEqual(
+ findHandler(table, "GET", "/"),
+ null,
+ );
+
+ assert.deepEqual(
+ findHandler(table, "GET", "/user/"),
+ { handlerFn: static1, params: {} },
+ );
+
+ assert.deepEqual(
+ findHandler(table, "GET", "/pages"),
+ { handlerFn: static2, params: {} },
+ );
+ assert.deepEqual(
+ findHandler(table, "GET", "/pages/home/"),
+ { handlerFn: static3, params: {} },
+ );
+
+ assert.deepEqual(
+ findHandler(table, "GET", "/user/some-id"),
+ { handlerFn: dynamic1, params: { id: "some-id" }},
+ );
+ assert.deepEqual(
+ findHandler(table, "GET", "/user/other-id/info"),
+ null,
+ );
+
+ assert.deepEqual(
+ findHandler(table, "PUT", "/user/other-id/info"),
+ { handlerFn: dynamic3, params: { id: "other-id" }},
+ );
+ assert.deepEqual(
+ findHandler(table, "PUT", "/user/another-id/preferences"),
+ { handlerFn: dynamic4, params: { id: "another-id" }},
+ );
+ assert.deepEqual(
+ findHandler(table, "POST", "/user/another-id/preferences"),
+ null,
+ );
+ });
+};
+
+const test_extractQueryParams = t => {
+ t.start("extractQueryParams()");
+
+ t.test("empty values", () => {
+ assert.deepEqual(extractQueryParams(), {});
+ assert.deepEqual(extractQueryParams(null), {});
+ assert.deepEqual(extractQueryParams(undefined), {});
+ });
+
+ t.test("we get a flat key-value strings", () => {
+ assert.deepEqual(
+ extractQueryParams("a[]=1&b=text&c=___"),
+ {
+ "a[]": "1",
+ b: "text",
+ c: "___",
+ },
+ );
+ });
+
+ 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_handleRequest = t => {
+ t.start("handleRequest()");
+
+ const fn = req => req;
+
+ t.test("request without params", async () => {
+ const table = {
+ static: {
+ GET: {
+ "": fn,
+ },
+ },
+ };
+
+ assert.deepEqual(
+ await handleRequest(table, "GET", "/?q=1"),
+ {
+ params: {
+ path: {},
+ query: {
+ q: "1",
+ },
+ },
+ method: "GET",
+ path: "/",
+ handler: fn,
+ },
+ );
+ });
+
+ t.test("request with params", async () => {
+ const table = {
+ dynamic: {
+ PUT: {
+ api: {
+ user: {
+ ":userId": {
+ "": fn,
+ },
+ },
+ },
+ },
+ },
+ };
+
+ assert.deepEqual(
+ await handleRequest(table, "PUT", "/api/user/2222"),
+ {
+ params: {
+ path: {
+ userId: "2222",
+ },
+ query: {},
+ },
+ method: "PUT",
+ path: "/api/user/2222",
+ handler: fn,
+ },
+ );
+ });
+
+ t.test("missing route", async () => {
+ assert.deepEqual(
+ await handleRequest({}, "GET", "/"),
+ {
+ status: 404,
+ body: "Not Found",
+ },
+ );
+ });
+};
+
+const test_makeRequestListener = t => {
+ t.start("makeRequestListener()");
+
+ 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" ],
+ );
+ });
+
+ 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_interceptorsFn = t => {
+ const next = x => x;
+
+ {
+ t.start("interceptorsFn().requestId()");
+
+ let i = 0;
+ const uuidFn = () => `${i++}`;
+
+ 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",
+ },
+ );
+ });
+
+ 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()");
+
+ t.test("we log before and after next()", async () => {
+ const contents = [];
+ const logger = 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()");
+
+ 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,
+ },
+ },
+ );
+ });
+
+ 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,
+ },
+ },
+ );
+ });
+
+ 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",
+ },
+ }
+ );
+ });
+
+ 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()");
+
+ t.test("no-op when no error occurs", async () => {
+ assert.deepEqual(
+ await interceptorsFn().serverError({ status: 1 }, next),
+ { status: 1 },
+ );
+ });
+
+ t.test(`an error is thrown if "status" is missing`, async () => {
+ const contents = [];
+ const logger = x => contents.push(x);
+ assert.deepEqual(
+ await interceptorsFn({ logger }).serverError({ id: 123 }, next),
+ {
+ status: 500,
+ body: "Internal Server Error",
+ },
+ );
+ assert.deepEqual(
+ contents,
+ [{
+ id: 123,
+ type: "server-error-interceptor",
+ message: `Missing "status"`,
+ }],
+ );
+ });
+
+ t.test("we turn a handler error into a 500 response", async () => {
+ const contents = [];
+ const logger = 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",
+ },
+ );
+ assert.deepEqual(
+ contents,
+ [{
+ id: "some ID",
+ type: "server-error-interceptor",
+ message: "My test error message",
+ }],
+ );
+ });
+ }
+};
+
+const test_chainInterceptors = t => {
+ t.start("chainInterceptors()");
+
+ t.test("empty values", () => {
+ assert.equal(chainInterceptors([])("req"), "req");
+ });
+
+ 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"]);
+ });
+
+ 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 },
+ );
+ });
+
+ 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 = t => {
+ t.start("wrapHandler()");
+
+ t.test("a handler with chained interceptors change its behaviour", async () => {
+ let i = 0;
+ const uuidFn = () => `${i++}`;
+
+ const contents = [];
+ const logger = 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",
+ },
+ ],
+ );
+ });
+};
+
+
+await runTests([
+ test_normalizeSegments,
+ test_pathToSegments,
+ test_hasPathParams,
+ test_addRoute,
+ test_buildRoutes,
+ test_findStaticHandler,
+ test_firstParamMatch,
+ test_findDynamicHandler,
+ test_findHandler,
+ test_extractQueryParams,
+ test_handleRequest,
+ test_makeRequestListener,
+ test_interceptorsFn,
+ test_chainInterceptors,
+ test_wrapHandler,
+]);
diff --git a/tests/js/ircd.js b/tests/js/ircd.js
deleted file mode 100644
index 08bb6dc..0000000
--- a/tests/js/ircd.js
+++ /dev/null
@@ -1,7 +0,0 @@
-const { runTests } = require("../runner.js");
-const { } = require("../../src/ircd.js");
-
-const tests = [
-];
-
-runTests(tests);
diff --git a/tests/js/ircd.mjs b/tests/js/ircd.mjs
new file mode 100644
index 0000000..e5eda66
--- /dev/null
+++ b/tests/js/ircd.mjs
@@ -0,0 +1,7 @@
+import { runTests } from "../runner.mjs";
+import { } from "../../src/ircd.mjs";
+
+
+
+await runTests([
+]);
diff --git a/tests/js/utils.js b/tests/js/utils.js
deleted file mode 100644
index 670d89a..0000000
--- a/tests/js/utils.js
+++ /dev/null
@@ -1,130 +0,0 @@
-const assert = require("node:assert");
-
-const { runTests } = require("../runner.js");
-const { eq, keys } = require("../../src/utils.js");
-
-const test_eq = t => {
- t.start("eq()");
- t.test("scalar values equality", () => {
- assert(eq(0, 0));
- assert(eq(100, 100));
- assert(eq(1.5, 1.5));
- assert(eq(-9, -9));
-
- assert(!eq(0, 1));
- assert(!eq(100, 99));
- assert(!eq(1.5, 1.4));
- assert(!eq(-9, 9));
-
-
- assert(eq(null, null));
- assert(eq(undefined, undefined));
- assert(eq("", ""));
- assert(eq("a string", "a string"));
-
- assert(!eq(null, undefined));
- assert(!eq(undefined, 0));
- assert(!eq("", "0"));
- assert(!eq("a string", "another string"));
-
-
- assert(eq(true, true));
- assert(eq(false, false));
-
- assert(!eq(true, false));
-
-
- assert(eq(1n, 1n));
- assert(eq(99999999999999n, 99999999999999n));
- });
-
- t.test("array equality", () => {
- assert(eq([], []));
-
-
- assert(eq([0, 1, 2], [0, 1, 2]));
- assert(eq([0, 1, 2], new Array(0, 1, 2)));
-
- assert(!eq([0, 1, 2], [0, 1]));
- assert(!eq([0, 1], new Array(0, 1, 2)));
-
-
- assert(eq([undefined], [undefined]));
- assert(eq([null, 0, "", true], [null, 0, "", true]));
-
-
- assert(eq([[[[0]]]], [[[[0]]]]));
-
- assert(!eq([[[[0]]]], [0]));
- });
-
- t.test("object equality", () => {
- assert(eq({}, {}));
- assert(eq({ a: 1 }, { a: 1 }));
-
- assert(!eq({ a: 1, b: undefined }, { a: 1 }));
-
-
- assert(eq(
- { a: 1, b: { c: { d: "e" } } },
- { a: 1, b: { c: { d: "e" } } },
- ));
-
- class C {}
- // ... FIXME: finish
- });
-
- t.test("mixed values", () => {
- assert(eq(
- {a: ["", 1, 2, 3, [{ b: { c: [ "d", "e" ]}}]]},
- {a: ["", 1, 2, 3, [{ b: { c: [ "d", "e" ]}}]]},
- ));
-
- assert(!eq(null, {}));
-
- assert(!eq([], {}));
- });
-};
-
-const test_keys = t => {
- t.start("keys()");
- t.test("happy paths", () => {
- assert.deepEqual(
- { a: 1, b: 2 },
- keys(["a", "b"], { a: 1, b: 2, c: 3 }),
- );
- });
-
- t.test("stress scenarios", () => {
- assert.deepEqual(
- {},
- keys([], {}),
- "empty selection of empty object",
- );
-
- assert.deepEqual(
- {},
- keys([], {a: 1}),
- "empty selection of non-empty object",
- );
-
- assert.deepEqual(
- {},
- keys(["a"], {}),
- "non-empty selection of empty object",
- );
-
- assert.deepEqual(
- { a: undefined, b: null },
- keys(["a", "b", "c"], { a: undefined, b: null }),
- "falsy values",
- );
- });
-};
-
-const tests = [
- test_eq,
- test_keys,
-];
-
-runTests(tests);
diff --git a/tests/js/utils.mjs b/tests/js/utils.mjs
new file mode 100644
index 0000000..1875ce5
--- /dev/null
+++ b/tests/js/utils.mjs
@@ -0,0 +1,225 @@
+import assert from "node:assert/strict";
+
+import { runTests } from "../runner.mjs";
+import {
+ eq,
+ keys,
+ assocIn,
+ getIn,
+ first,
+ log,
+} from "../../src/utils.mjs";
+
+const test_eq = t => {
+ t.start("eq()");
+ t.test("scalar values equality", () => {
+ assert(eq(0, 0));
+ assert(eq(100, 100));
+ assert(eq(1.5, 1.5));
+ assert(eq(-9, -9));
+
+ assert(!eq(0, 1));
+ assert(!eq(100, 99));
+ assert(!eq(1.5, 1.4));
+ assert(!eq(-9, 9));
+
+
+ assert(eq(null, null));
+ assert(eq(undefined, undefined));
+ assert(eq("", ""));
+ assert(eq("a string", "a string"));
+
+ assert(!eq(null, undefined));
+ assert(!eq(undefined, 0));
+ assert(!eq("", "0"));
+ assert(!eq("a string", "another string"));
+
+
+ assert(eq(true, true));
+ assert(eq(false, false));
+
+ assert(!eq(true, false));
+
+
+ assert(eq(1n, 1n));
+ assert(eq(99999999999999n, 99999999999999n));
+ });
+
+ t.test("array equality", () => {
+ assert(eq([], []));
+
+
+ assert(eq([0, 1, 2], [0, 1, 2]));
+ assert(eq([0, 1, 2], new Array(0, 1, 2)));
+
+ assert(!eq([0, 1, 2], [0, 1]));
+ assert(!eq([0, 1], new Array(0, 1, 2)));
+
+
+ assert(eq([undefined], [undefined]));
+ assert(eq([null, 0, "", true], [null, 0, "", true]));
+
+
+ assert(eq([[[[0]]]], [[[[0]]]]));
+
+ assert(!eq([[[[0]]]], [0]));
+ });
+
+ t.test("object equality", () => {
+ assert(eq({}, {}));
+ assert(eq({ a: 1 }, { a: 1 }));
+
+ assert(!eq({ a: 1, b: undefined }, { a: 1 }));
+
+
+ assert(eq(
+ { a: 1, b: { c: { d: "e" } } },
+ { a: 1, b: { c: { d: "e" } } },
+ ));
+ });
+
+ t.test("mixed values", () => {
+ assert(eq(
+ {a: ["", 1, 2, 3, [{ b: { c: [ "d", "e" ]}}]]},
+ {a: ["", 1, 2, 3, [{ b: { c: [ "d", "e" ]}}]]},
+ ));
+
+ assert(!eq(null, {}));
+
+ assert(!eq([], {}));
+ });
+};
+
+const test_keys = t => {
+ t.start("keys()");
+ t.test("happy paths", () => {
+ assert.deepEqual(
+ { a: 1, b: 2 },
+ keys(["a", "b"], { a: 1, b: 2, c: 3 }),
+ );
+ });
+
+ t.test("stress scenarios", () => {
+ assert.deepEqual(
+ {},
+ keys([], {}),
+ "empty selection of empty object",
+ );
+
+ assert.deepEqual(
+ {},
+ keys([], {a: 1}),
+ "empty selection of non-empty object",
+ );
+
+ assert.deepEqual(
+ {},
+ keys(["a"], {}),
+ "non-empty selection of empty object",
+ );
+
+ assert.deepEqual(
+ { a: undefined, b: null },
+ keys(["a", "b", "c"], { a: undefined, b: null }),
+ "falsy values",
+ );
+ });
+};
+
+const test_assocIn = t => {
+ t.start("assocIn()");
+
+ t.test("empty values", () => {
+ assert.deepEqual(assocIn({}, [], null), {});
+ assert.deepEqual(assocIn({ k: "v" }, [], null), { k: "v" });
+ });
+
+ t.test("adding values", () => {
+ assert.deepEqual(assocIn({}, ["k"], "v"), { k: "v" });
+ assert.deepEqual(assocIn({}, ["k1", "k2"], "v"), { k1: { k2: "v" }});
+ assert.deepEqual(assocIn({}, ["k1", "k2", "k3"], "v"), { k1: { k2: { k3: "v" }}});
+ assert.deepEqual(assocIn({ k: "v" }, ["k1", "k2"], "v"), { k: "v", k1: { k2: "v" }});
+ });
+
+ t.test("replacing values", () => {
+ assert.deepEqual(
+ assocIn(
+ { k1: { k2: { k3: "before" }}},
+ ["k1", "k2", "k3"],
+ "after"
+ ),
+ { k1: { k2: { k3: "after" }}}
+ );
+ });
+};
+
+const test_getIn = t => {
+ t.start("getIn()");
+
+ t.test("empty values", () => {
+ assert.deepEqual(getIn({}, []), {});
+ assert.deepEqual(getIn({ k: "v" }, []), { k: "v" });
+ });
+
+ t.test("missing values", () => {
+ assert.deepEqual(getIn({}, ["a", "b", "c"]), undefined);
+ assert.deepEqual(getIn({ a: {}}, ["a", "b", "c"]), undefined);
+ assert.deepEqual(getIn({ a: { b: {}}}, ["a", "b", "c"]), undefined);
+ assert.deepEqual(getIn({ a: { b: {}, c: {}}}, ["a", "b", "c"]), undefined);
+ });
+
+ t.test("nested valeues", () => {
+ assert.deepEqual(getIn({ a: { b: { c: { d: "e" }}}}, ["a", "b", "c", "d"]), "e");
+ });
+};
+
+const test_first = t => {
+ t.start("first()");
+
+ t.test("empty values", () => {
+ assert.equal(first([], () => {}), null);
+ });
+
+ t.test("when function doesn't transform, it behaves similarly to [].find()", () => {
+ const arr1 = [ 0, null, undefined, "", 1, 2 ];
+ assert.equal(first(arr1, x => x), 1);
+ assert.equal(arr1.find(x => x), 1);
+
+ const arr2 = [ 0, null, undefined, "", false ];
+ assert.equal(first(arr2, x => x), null);
+ assert.equal(arr2.find(x => x), undefined);
+ });
+
+ t.test("when it does transform, we return the transformed value", () => {
+ const arr = [ 1, 3, 5, 6 ];
+
+ assert.equal(
+ first(arr, x => x % 2 === 0 && "a brand new value"),
+ "a brand new value",
+ );
+ });
+};
+
+const test_log = t => {
+ t.start("log()");
+
+ t.test("we can log data", () => {
+ log({ a: 1, type: "log-test" });
+ });
+
+ t.test("we can't log unserializable things", () => {
+ const obj = { self: null };
+ obj.self = obj;
+ assert.throws(() => log(obj), TypeError);
+ });
+};
+
+
+await runTests([
+ test_eq,
+ test_keys,
+ test_assocIn,
+ test_getIn,
+ test_first,
+ test_log,
+]);
diff --git a/tests/js/web.js b/tests/js/web.js
deleted file mode 100644
index 6562433..0000000
--- a/tests/js/web.js
+++ /dev/null
@@ -1,7 +0,0 @@
-const { runTests } = require("../runner.js");
-const { } = require("../../src/web.js");
-
-const tests = [
-];
-
-runTests(tests);
diff --git a/tests/js/web.mjs b/tests/js/web.mjs
new file mode 100644
index 0000000..00cce70
--- /dev/null
+++ b/tests/js/web.mjs
@@ -0,0 +1,7 @@
+import { runTests } from "../runner.mjs";
+import { } from "../../src/web.mjs";
+
+
+
+await runTests([
+]);
diff --git a/tests/runner.js b/tests/runner.mjs
index 4772a28..79bda8c 100644
--- a/tests/runner.js
+++ b/tests/runner.mjs
@@ -1,4 +1,4 @@
-const { eq } = require("../src/utils.js");
+import { eq } from "../src/utils.mjs";
class AssertionError extends Error {}
@@ -25,12 +25,8 @@ const t = {
},
};
-const runTests = async tests => {
+export const runTests = async tests => {
for (const testFn of tests) {
await testFn(t);
}
};
-
-module.exports = {
- runTests,
-};