import { describe, expect, test } from "bun:test"; import Router, { createParamsObj, reconstructRegisteredURL } from "./router"; import type { Request, Response } from "./types"; import { Method } from "./types"; describe("router tree", () => { test("static overlapping routes", async () => { const router = createRouter([ [Method.GET, "/foo/bar"], [Method.POST, "/food"], [Method.GET, "/"], [Method.GET, "/foo"], [Method.GET, "/bar"], [Method.DELETE, "/food"], [Method.GET, "/bar/food"], [Method.DELETE, "/foo/bar"], [Method.GET, "/foo/bad"], [Method.GET, "/foo/baz"], [Method.GET, "/fab/lets/go"], ]); await expectRoute(router, Method.GET, "/"); await expectRoute(router, Method.GET, "/foo"); await expectNotFound(router, Method.DELETE, "/foo"); await expectRoute(router, Method.DELETE, "/foo/bar"); await expectNotFound(router, Method.DELETE, "/foo/baz"); }); test("dynamic overlapping routes", async () => { const router = createRouter([ [Method.GET, "/api/users/:user_id"], [Method.GET, "/api/users/list"], [Method.GET, "/api/users/:userid/comments"], [Method.GET, "/api/users/:id/email"], [Method.POST, "/api/users/"], [Method.GET, "/api/thread/:id/comments"], [Method.GET, "/api/thread/:id/*stuff"], ]); await expectRoute(router, Method.POST, "/api/users/"); await expectRoute(router, Method.GET, "/api/users/list"); await expectRoute(router, Method.GET, "/api/users/1", { user_id: "1" }); await expectRoute(router, Method.GET, "/api/thread/abcdef/comments", { id: "abcdef", }); await expectRoute( router, Method.GET, "/api/thread/abcdef/other/url/thing", { id: "abcdef", stuff: "other/url/thing" }, ); }); describe("trailing slash redirects", () => { test("no trailing slash with no children", async () => { const router = createRouter([ [Method.GET, "/a"], [Method.GET, "/a/:b"], // we're testing against this route ]); await expectRoute(router, Method.GET, "/a/foo", { b: "foo" }, undefined); await expectRoute(router, Method.GET, "/a/foo/", { b: "foo" }, "/a/foo"); }); test("trailing slash as empty node next in tree", async () => { const router = createRouter([ [Method.GET, "/a"], [Method.GET, "/a/:b"], // we're testing against this route [Method.GET, "/a/:b/:x"], [Method.GET, "/a/:b/y"], ]); await expectRoute(router, Method.GET, "/a/foo", { b: "foo" }, undefined); await expectRoute(router, Method.GET, "/a/foo/", { b: "foo" }, "/a/foo"); }); test("trailing slash at end of target node with no children", async () => { const router = createRouter([ [Method.GET, "/a"], [Method.GET, "/a/b/"], // we're testing against this route ]); await expectRoute(router, Method.GET, "/a/b", {}, "/a/b/"); await expectRoute(router, Method.GET, "/a/b/", {}, undefined); }); test("trailing slash at end of target node with children", async () => { const router = createRouter([ [Method.GET, "/a"], [Method.GET, "/a/b/"], // we're testing against this route [Method.GET, "/a/b/:x"], [Method.GET, "/a/b/y"], ]); await expectRoute(router, Method.GET, "/a/b", {}, "/a/b/"); await expectRoute(router, Method.GET, "/a/b/", {}, undefined); }); test("trailing slash literal priority", async () => { const router = createRouter([ [Method.GET, "/cmd/:tool/"], [Method.GET, "/cmd/whoami"], // we're testing against this route [Method.GET, "/cmd/whoami/foo"], ]); await expectRoute(router, Method.GET, "/cmd/whoami", {}, undefined); await expectRoute(router, Method.GET, "/cmd/whoami/", {}, "/cmd/whoami"); }); test("trailing slash at end of target node with children with wildcard", async () => { const router = createRouter([ [Method.GET, "/aa/*xx"], [Method.GET, "/ab"], [Method.GET, "/:cc"], ]); await expectRoute(router, Method.GET, "/aa", { cc: "aa" }, undefined); await expectRoute(router, Method.GET, "/aa/", { xx: "" }, undefined); }); }); }); function createRouter(routes: [Method, string][]): Router { const router = new Router(); for (const [method, path] of routes) { router.register(method, path, testHandler(method, path)); } expect(router.debug()).toMatchSnapshot("createRouter"); return router; } async function expectNotFound( router: Router, method: Method, path: string, ): Promise { const res = router.find(method, path); expect(res).toBeNull(); } async function expectRoute( router: Router, method: Method, path: string, expectedParams: Record = {}, tsrURL?: string, ): Promise { const res = router.find(method, path); expect(res).not.toBeNull(); expect(createParamsObj(res)).toEqual(expectedParams); expect(res.tsfSuggestion).toEqual(tsrURL); await res.handler({} as Request, {} as Response); const reconstructedPath = reconstructRegisteredURL(res); expect(lastTestHandlerCalled).toEqual([method, reconstructedPath]); } let lastTestHandlerCalled: [Method, string] = [Method.Any, ""]; function testHandler(method: Method, name: string): () => Promise { return async () => { lastTestHandlerCalled = [method, name]; }; }