Skip to main content
Module

x/oak/router_test.ts

A middleware framework for handling HTTP with Deno 🐿️ 🦕
Extremely Popular
Go to Latest
File
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983
// Copyright 2018-2021 the oak authors. All rights reserved. MIT license.
// deno-lint-ignore-file
import { assertEquals, assertStrictEquals, assertThrowsAsync,} from "./test_deps.ts";import type { Application } from "./application.ts";import type { Context } from "./context.ts";import { Status } from "./deps.ts";import { httpErrors } from "./httpError.ts";import { Router, RouterContext } from "./router.ts";
const { test } = Deno;
function createMockApp< S extends Record<string | number | symbol, any> = Record<string, any>,>( state = {} as S,): Application<S> { const app = { state, use() { return app; }, }; return app as any;}
function createMockContext< S extends Record<string | number | symbol, any> = Record<string, any>,>( app: Application<S>, path = "/", method = "GET",) { const headers = new Headers(); return ({ app, request: { headers: new Headers(), method, url: new URL(path, "https://localhost/"), }, response: { status: undefined, body: undefined, redirect(url: string | URL) { headers.set("Location", encodeURI(String(url))); }, headers, }, state: app.state, } as unknown) as Context<S>;}
function createMockNext() { return async function next() {};}
function setup< S extends Record<string | number | symbol, any> = Record<string, any>,>( path = "/", method = "GET",): { app: Application<S>; context: Context<S>; next: () => Promise<void>;} { const app = createMockApp<S>(); const context = createMockContext<S>(app, path, method); const next = createMockNext(); return { app, context, next };}
test({ name: "router empty routes", async fn() { const { context, next } = setup();
const router = new Router(); const mw = router.routes(); assertEquals(await mw(context, next), undefined); },});
test({ name: "router accepts non-void middleware", fn() { const router = new Router(); router.get("/", (ctx) => ctx.response.body = "hello oak"); },});
test({ name: "router get single match", async fn() { const { app, context, next } = setup("/", "GET");
const callStack: number[] = []; const router = new Router(); router.get("/", (context) => { assertStrictEquals(context.router, router); assertStrictEquals(context.app, app); callStack.push(1); }); const mw = router.routes(); await mw(context, next); assertEquals(callStack, [1]); },});
test({ name: "router match single param", async fn() { const { context, next } = setup("/foo/bar", "GET");
const callStack: number[] = []; const router = new Router(); router.get("/", (context) => { callStack.push(1); }); router.get("/foo", (context) => { callStack.push(2); }); router.get<{ id: string }>("/foo/:id", (context) => { callStack.push(3); assertEquals(context.params.id, "bar"); }); const mw = router.routes(); await mw(context, next); assertEquals(callStack, [3]); },});
test({ name: "router match with next", async fn() { const { context, next } = setup("/foo", "GET");
const callStack: number[] = []; const router = new Router(); router.get("/", (_context) => { callStack.push(1); }); router.get("/foo", async (_context, next) => { callStack.push(2); await next(); }); router.get("/foo", () => { callStack.push(3); }); router.get("/foo", () => { callStack.push(4); }); const mw = router.routes(); await mw(context, next); assertEquals(callStack, [2, 3]); },});
test({ name: "router match delete", async fn() { const { context, next } = setup("/", "DELETE");
const callStack: number[] = []; const router = new Router(); router.all("/", async (_context, next) => { callStack.push(0); await next(); }); router.delete("/", () => { callStack.push(1); }); router.get("/", () => { callStack.push(2); }); router.head("/", () => { callStack.push(3); }); router.options("/", () => { callStack.push(4); }); router.patch("/", () => { callStack.push(5); }); router.post("/", () => { callStack.push(6); }); router.put("/", () => { callStack.push(7); }); const mw = router.routes(); await mw(context, next); assertEquals(callStack, [0, 1]); },});
test({ name: "router match get", async fn() { const { context, next } = setup("/", "GET");
const callStack: number[] = []; const router = new Router(); router.all("/", async (_context, next) => { callStack.push(0); await next(); }); router.delete("/", () => { callStack.push(1); }); router.get("/", () => { callStack.push(2); }); router.head("/", () => { callStack.push(3); }); router.options("/", () => { callStack.push(4); }); router.patch("/", () => { callStack.push(5); }); router.post("/", () => { callStack.push(6); }); router.put("/", () => { callStack.push(7); }); const mw = router.routes(); await mw(context, next); assertEquals(callStack, [0, 2]); },});
test({ name: "router match head", async fn() { const { context, next } = setup("/", "HEAD");
const callStack: number[] = []; const router = new Router(); router.all("/", async (_context, next) => { callStack.push(0); await next(); }); router.delete("/", () => { callStack.push(1); }); router.head("/", () => { callStack.push(3); }); router.get("/", () => { callStack.push(2); }); router.options("/", () => { callStack.push(4); }); router.patch("/", () => { callStack.push(5); }); router.post("/", () => { callStack.push(6); }); router.put("/", () => { callStack.push(7); }); const mw = router.routes(); await mw(context, next); assertEquals(callStack, [0, 3]); },});
test({ name: "router match options", async fn() { const { context, next } = setup("/", "OPTIONS");
const callStack: number[] = []; const router = new Router(); router.all("/", async (_context, next) => { callStack.push(0); await next(); }); router.delete("/", () => { callStack.push(1); }); router.get("/", () => { callStack.push(2); }); router.head("/", () => { callStack.push(3); }); router.options("/", () => { callStack.push(4); }); router.patch("/", () => { callStack.push(5); }); router.post("/", () => { callStack.push(6); }); router.put("/", () => { callStack.push(7); }); const mw = router.routes(); await mw(context, next); assertEquals(callStack, [4]); },});
test({ name: "router match patch", async fn() { const { context, next } = setup("/", "PATCH");
const callStack: number[] = []; const router = new Router(); router.all("/", async (_context, next) => { callStack.push(0); await next(); }); router.delete("/", () => { callStack.push(1); }); router.get("/", () => { callStack.push(2); }); router.head("/", () => { callStack.push(3); }); router.options("/", () => { callStack.push(4); }); router.patch("/", () => { callStack.push(5); }); router.post("/", () => { callStack.push(6); }); router.put("/", () => { callStack.push(7); }); const mw = router.routes(); await mw(context, next); assertEquals(callStack, [5]); },});
test({ name: "router match post", async fn() { const { context, next } = setup("/", "POST");
const callStack: number[] = []; const router = new Router(); router.all("/", async (_context, next) => { callStack.push(0); await next(); }); router.delete("/", () => { callStack.push(1); }); router.get("/", () => { callStack.push(2); }); router.head("/", () => { callStack.push(3); }); router.options("/", () => { callStack.push(4); }); router.patch("/", () => { callStack.push(5); }); router.post("/", () => { callStack.push(6); }); router.put("/", () => { callStack.push(7); }); const mw = router.routes(); await mw(context, next); assertEquals(callStack, [0, 6]); },});
test({ name: "router match put", async fn() { const { context, next } = setup("/", "PUT");
const callStack: number[] = []; const router = new Router(); router.all("/", async (_context, next) => { callStack.push(0); await next(); }); router.delete("/", () => { callStack.push(1); }); router.get("/", () => { callStack.push(2); }); router.head("/", () => { callStack.push(3); }); router.options("/", () => { callStack.push(4); }); router.patch("/", () => { callStack.push(5); }); router.post("/", () => { callStack.push(6); }); router.put("/", () => { callStack.push(7); }); const mw = router.routes(); await mw(context, next); assertEquals(callStack, [0, 7]); },});
test({ name: "router patch prefix", async fn() { const { context, next } = setup("/route1/action1", "GET"); const callStack: number[] = []; const router = new Router({ prefix: "/route1" }); router.get("/action1", () => { callStack.push(0); }); const mw = router.routes(); await mw(context, next); assertEquals(callStack, [0]); },});
test({ name: "router match strict", async fn() { const { context, next } = setup("/route", "GET"); const callStack: number[] = []; const router = new Router({ strict: true }); router.get("/route", () => { callStack.push(0); }); router.get("/route/", () => { callStack.push(1); }); const mw = router.routes(); await mw(context, next); assertEquals(callStack, [0]); },});
test({ name: "router as iterator", fn() { const router = new Router(); router.all("/route", () => {}); router.delete("/route/:id", () => {}); router.patch("/route/:id", () => {}); const routes = [...router]; assertEquals(routes.length, 3); assertEquals(routes[0].path, "/route"); assertEquals(routes[0].methods, ["HEAD", "DELETE", "GET", "POST", "PUT"]); assertEquals(routes[0].middleware.length, 1); },});
test({ name: "route throws", async fn() { const { context, next } = setup(); const router = new Router(); router.all("/", (ctx) => { ctx.throw(404); }); const mw = router.routes(); await assertThrowsAsync(async () => { await mw(context, next); }); },});
test({ name: "router prefix, default route", async fn() { const { context, next } = setup("/foo"); let called = 0; const router = new Router({ prefix: "/foo", }); router.all("/", () => { called++; }); const mw = router.routes(); await mw(context, next); assertEquals(called, 1); },});
test({ name: "router redirect", async fn() { const { context, next } = setup("/foo"); const router = new Router(); router.redirect("/foo", "/bar"); const mw = router.routes(); await mw(context, next); assertEquals(context.response.status, Status.Found); assertEquals(context.response.headers.get("Location"), "/bar"); },});
test({ name: "router redirect, 301 Moved Permanently", async fn() { const { context, next } = setup("/foo"); const router = new Router(); router.redirect("/foo", "/bar", Status.MovedPermanently); const mw = router.routes(); await mw(context, next); assertEquals(context.response.status, Status.MovedPermanently); assertEquals(context.response.headers.get("Location"), "/bar"); },});
test({ name: "router redirect, arbitrary URL", async fn() { const { context, next } = setup("/foo"); const router = new Router(); router.redirect("/foo", "https://example.com/", Status.MovedPermanently); const mw = router.routes(); await mw(context, next); assertEquals(context.response.status, Status.MovedPermanently); assertEquals( context.response.headers.get("Location"), "https://example.com/", ); },});
test({ name: "router param middleware", async fn() { const { context, next } = setup("/book/1234/price"); const router = new Router<{ id: string }>(); const callStack: string[] = []; router.param("id", (param, ctx, next) => { callStack.push("param"); assertEquals(param, "1234"); assertEquals(ctx.params.id, "1234"); return next(); }); router.all("/book/:id/price", (ctx, next) => { callStack.push("all"); assertEquals(ctx.params.id, "1234"); return next(); }); const mw = router.routes(); await mw(context, next); assertEquals(callStack, ["param", "all"]); },});
test({ name: "router allowedMethods() OPTIONS", async fn() { const { context, next } = setup("/foo", "OPTIONS"); const router = new Router(); router.put("/foo", (_ctx, next) => { return next(); }); router.patch("/foo", (_ctx, next) => { return next(); }); const routes = router.routes(); const mw = router.allowedMethods(); await routes(context, next); await mw(context, next); assertEquals(context.response.status, Status.OK); assertEquals(context.response.headers.get("Allowed"), "PUT, PATCH"); },});
test({ name: "router allowedMethods() Not Implemented", async fn() { const { context, next } = setup("/foo", "PATCH"); const router = new Router({ methods: ["GET"] }); router.get("/foo", (_ctx, next) => { return next(); }); const routes = router.routes(); const mw = router.allowedMethods(); await routes(context, next); await mw(context, next); assertEquals(context.response.status, Status.NotImplemented); },});
test({ name: "router allowedMethods() Method Not Allowed", async fn() { const { context, next } = setup("/foo", "PUT"); const router = new Router(); router.get("/foo", (_ctx, next) => { return next(); }); const routes = router.routes(); const mw = router.allowedMethods(); await routes(context, next); await mw(context, next); assertEquals(context.response.status, Status.MethodNotAllowed); },});
test({ name: "router allowedMethods() throws Not Implemented", async fn() { const { context, next } = setup("/foo", "PATCH"); const router = new Router({ methods: ["GET"] }); router.get("/foo", (_ctx, next) => { return next(); }); const routes = router.routes(); const mw = router.allowedMethods({ throw: true }); await routes(context, next); await assertThrowsAsync(async () => { await mw(context, next); }, httpErrors.NotImplemented); },});
test({ name: "router allowedMethods() throws Method Not Allowed", async fn() { const { context, next } = setup("/foo", "PUT"); const router = new Router(); router.get("/foo", (_ctx, next) => { return next(); }); const routes = router.routes(); const mw = router.allowedMethods({ throw: true }); await routes(context, next); await assertThrowsAsync(async () => { await mw(context, next); }, httpErrors.MethodNotAllowed); },});
test({ name: "router named route - get URL", fn() { const router = new Router<{ id: string }>(); router.get("get_book", "/book/:id", (ctx, next) => next()); assertEquals(router.url("get_book", { id: "1234" }), "/book/1234"); assertEquals( router.url("get_book", { id: "1234" }, { query: { sort: "ASC" } }), "/book/1234?sort=ASC", ); },});
test({ name: "router types", fn() { const app = createMockApp<{ id: string }>(); const router = new Router();
app.use( router.get( "/:id", (ctx: RouterContext<{ id: string }, { session: number }>) => { ctx.params.id; ctx.state.session; }, ).get("/:id/names", (ctx) => { ctx.params.id; ctx.state.session; }).put("/:page", (ctx: RouterContext<{ page: string }>) => { ctx.params.page; }).put("/value", (ctx) => { ctx.params.id; ctx.params.page; ctx.state.session; // @ts-expect-error ctx.params.foo; }).routes(), ).use((ctx) => { ctx.state.id; }); },});
test({ name: "middleware returned from router.routes() passes next", async fn() { const { context } = setup("/foo", "GET");
const callStack: number[] = [];
async function next() { callStack.push(4); }
const router = new Router(); router.get("/", (_context) => { callStack.push(1); }); router.get("/foo", async (_context, next) => { callStack.push(2); await next(); }); router.get("/foo", async (_context, next) => { callStack.push(3); await next(); });
const mw = router.routes(); await mw(context, next); assertEquals(callStack, [2, 3, 4]); },});
test({ name: "router routes decode pathname before matching", async fn() { const path = encodeURIComponent("chêne"); const { context } = setup(`/${path}`, "GET");
const callStack: number[] = [];
async function next() { callStack.push(3); }
const router = new Router(); router.get("/chêne", () => { callStack.push(2); });
const mw = router.routes(); await mw(context, next); assertEquals(callStack, [2]); },});
test({ name: "router handling of bad request urls", async fn() { const headers = new Headers(); const app = createMockApp<{ id: string }>(); let context = ({ app, request: { headers: new Headers(), method: "GET", get url() { throw new TypeError("bad url"); }, }, response: { status: undefined, body: undefined, redirect(url: string | URL) { headers.set("Location", encodeURI(String(url))); }, headers, }, state: app.state, } as unknown) as Context<{ id: string }>;
const callStack: number[] = []; async function next() { callStack.push(1); }
const router = new Router(); router.get("/a", () => { callStack.push(2); });
const mw = router.routes(); assertThrowsAsync( async () => await mw(context, next), TypeError, "bad url", ); },});
test({ name: "sub router get single match", async fn() { const { app, context, next } = setup("/foo/bar", "GET");
const callStack: number[] = []; const router = new Router(); const subRouter = new Router(); const subSubRouter = new Router(); subSubRouter.get("/", (context) => { assertStrictEquals(context.router, router); assertStrictEquals(context.app, app); callStack.push(1); }); subRouter.use("/bar", subSubRouter.routes()); router.use("/foo", subRouter.routes()); const mw = router.routes(); await mw(context, next); assertEquals(callStack, [1]); assertStrictEquals((context as RouterContext).router, router); },});
test({ name: "sub router match with next", async fn() { const { context, next } = setup("/foo/bar/baz", "GET");
const callStack: number[] = []; const router = new Router(); const subRouter = new Router(); const subSubRouter = new Router(); subSubRouter.get("/", () => { callStack.push(3); }); subSubRouter.get("/baz", async (ctx, next) => { callStack.push(4); await next(); }); subSubRouter.get("/baz", () => { callStack.push(5); }); subSubRouter.get("/baz", () => { callStack.push(6); }); subRouter.get("/bar/(.*)", async (ctx, next) => { callStack.push(2); await next(); }); subRouter.use("/bar", subSubRouter.routes()); router.get("/foo/(.*)", async (ctx, next) => { callStack.push(1); await next(); }); router.use("/foo", subRouter.routes()); const mw = router.routes(); await mw(context, next); assertEquals(callStack, [1, 2, 4, 5]); },});
test({ name: "sub router match single param", async fn() { const { context, next } = setup("/foo/bar/baz/beep", "GET");
const callStack: number[] = []; const router = new Router(); const subRouter = new Router(); const subSubRouter = new Router(); subSubRouter.get<{ id: string; name: string }>("/", (context) => { assertEquals(context.params.id, "bar"); assertEquals(context.params.name, "beep"); callStack.push(1); }); subRouter.get("/baz", () => { callStack.push(2); }); subRouter.use<{ name: string }>("/baz/:name", subSubRouter.routes()); router.get("/foo", () => { callStack.push(3); }); router.use<{ id: string }>("/foo/:id", subRouter.routes()); const mw = router.routes(); await mw(context, next); assertEquals(callStack, [1]); assertStrictEquals((context as RouterContext).router, router); },});
test({ name: "sub router patch prefix with param", async fn() { const { context, next } = setup("/foo/bar/baz", "GET"); const callStack: number[] = []; const router = new Router(); const subRouter = new Router({ prefix: "/:bar" }); subRouter.get("/baz", (ctx) => { assertEquals(ctx.params.bar, "bar"); callStack.push(0); }); router.use("/foo", subRouter.routes()); const mw = router.routes(); await mw(context, next); assertEquals(callStack, [0]); },});
test({ name: "sub router match layer prefix", async fn() { let callStack: number[] = []; let matches: string[] = []; const router = new Router(); const subRouter = new Router(); const subSubRouter = new Router();
subSubRouter.get("/bar", async (ctx, next) => { callStack.push(1); matches.push(...(ctx.matched?.map((layer) => layer.path) ?? [])); await next(); }); subRouter.use(subSubRouter.routes()); subRouter.use("(.*)", subSubRouter.routes()); router.use("/foo", subRouter.routes()); const mw = router.routes();
const { context, next } = setup("/foo/bar", "GET"); await mw(context, next); assertEquals(callStack, [1, 1]); assertEquals(matches, [ "/foo/bar", "/foo(.*)/bar", "/foo/bar", "/foo(.*)/bar", ]); assertStrictEquals((context as RouterContext).router, router);
callStack = []; matches = [];
const { context: context2, next: next2 } = setup("/foo/123/bar", "GET"); await mw(context2, next2); assertEquals(callStack, [1]); assertEquals(matches, [ "/foo(.*)/bar", ]); assertStrictEquals((context2 as RouterContext).router, router); },});
test({ name: "router - type checking - ensure at least one middleware is passed", fn() { const router = new Router();
try { // @ts-expect-error router.all("/"); // @ts-expect-error router.delete("/"); // @ts-expect-error router.get("/"); // @ts-expect-error router.head("/"); // @ts-expect-error router.options("/"); // @ts-expect-error router.patch("/"); // @ts-expect-error router.post("/"); // @ts-expect-error router.put("/"); // @ts-expect-error router.use(); } catch { // } },});