Very Popular
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708// Copyright 2018-2020 the oak authors. All rights reserved. MIT license.
import { assertEquals, assertStrictEquals, assertThrowsAsync, test,} from "./test_deps.ts";import { Application } from "./application.ts";import { Context } from "./context.ts";import { Status } from "./deps.ts";import { httpErrors } from "./httpError.ts";import { Router, RouterContext } from "./router.ts";
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 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 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]); },});