diff options
author | EuAndreh <eu@euandre.org> | 2024-02-23 06:05:19 -0300 |
---|---|---|
committer | EuAndreh <eu@euandre.org> | 2024-02-23 06:05:21 -0300 |
commit | c36bf8e3577da31cf6d575879c7e92d3e9c7e4f1 (patch) | |
tree | 4fd5414e76490e297c4c770ff35e09149ef3658f /tests/js | |
parent | Remove C code and cleanup repository (diff) | |
download | papod-c36bf8e3577da31cf6d575879c7e92d3e9c7e4f1.tar.gz papod-c36bf8e3577da31cf6d575879c7e92d3e9c7e4f1.tar.xz |
Big cleanup
- delete all SQLite Node-API code: we'll use the C++ one instead;
- implement hero.mjs, with tests!
- use ESM all over.
Diffstat (limited to 'tests/js')
-rw-r--r-- | tests/js/db.js | 16 | ||||
-rw-r--r-- | tests/js/db.mjs | 15 | ||||
-rw-r--r-- | tests/js/escape.mjs | 62 | ||||
-rw-r--r-- | tests/js/hero.mjs | 1050 | ||||
-rw-r--r-- | tests/js/ircd.js | 7 | ||||
-rw-r--r-- | tests/js/ircd.mjs | 7 | ||||
-rw-r--r-- | tests/js/utils.js | 130 | ||||
-rw-r--r-- | tests/js/utils.mjs | 225 | ||||
-rw-r--r-- | tests/js/web.js | 7 | ||||
-rw-r--r-- | tests/js/web.mjs | 7 |
10 files changed, 1366 insertions, 160 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("<"), "<"); + 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"); + }); + + t.test("the combination of all special characters", () => { + assert.strictEqual( + escape(`foo, "bar", 'baz' & <quux>`), + "foo, "bar", 'baz' & <quux>", + ); + }); +}; + + +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([ +]); |