UNPKG

koa-detour

Version:

Resource-oriented routing for Koa 2+

433 lines (375 loc) 11.9 kB
const expect = require("expect"); const verity = require("verity"); const Koa = require("koa"); const Detour = require("../lib"); const Route = require("../lib/route"); const PORT = 9999; let app, server, v; process.on("unhandledRejection", err => { throw err; }); function worked (ctx) { ctx.status = 200; ctx.body = "success"; } function failed (ctx) { ctx.status = 200; ctx.body = "failed"; } function createApp (router) { app = new Koa(); app.use(router.middleware()); server = app.listen(PORT); v = verity(`http://localhost:${PORT}`, "GET"); v.expectBody("success"); v.expectStatus(200); } function closeApp (done) { if (server == null) return done(); server.close(done); server = null; app = null; } describe("koa-detour", function () { afterEach(closeApp); describe("#route", function () { it("accepts an array path", function () { new Detour().route([], { GET: worked }); }); it("accepts a regexp path", function () { new Detour().route(new RegExp(), { GET: worked }); }); it("throws if path argument is invalid", function () { expect(() => { new Detour().route({}, { GET: worked }); }).toThrow(/Invalid path/); }); it("throws if resource argument is invalid", function () { expect(() => { new Detour().route("/", { GIBBERISH: worked }); }).toThrow(/valid HTTP verb/); }); it("properly routes a basic GET request", function (done) { createApp(new Detour().route("/", { GET: worked })); v.test(done); }); it ("can route multiple times", function (done){ const router = new Detour() .route("/", { GET: failed }) .route("/test", { GET: worked }) .route("/test/:id", { GET: failed }); expect(router._routes.length).toEqual(3); createApp(router); v.path("test"); v.test(done); }); it("200s for plain POST", function (done) { createApp(new Detour().route("/", { POST: worked })); v.method("POST"); v.test(done); }); it("routes are terminal -- no `next` is provided", function (done) { createApp(new Detour().route("/", { GET (ctx, next) { expect(next).toBe(undefined); worked(ctx); }, })); v.test(done); }); it("404s when no route is matched", function (done) { createApp(new Detour().route("/", { GET: worked })); v.method("GET"); v.path("test"); v.expectStatus(404); v.expectBody("Not Found"); v.test(done); }); }); describe("context additions", function () { it("adds `params`", function (done) { createApp(new Detour() .route("/a/:a/b/:b", { GET (ctx) { expect(ctx.params).toEqual({ a: "x", b: "y" }); worked(ctx); }, }) ); v.path("/a/x/b/y"); v.test(done); }); it("adds `resource`", function (done) { const resource = { GET (ctx) { expect(ctx.resource).toBe(resource); worked(ctx); }, }; createApp(new Detour().route("/", resource)); v.test(done); }); it("adds `route`", function (done) { let route; const resource = { GET (ctx) { ({route} = ctx); worked(ctx); } }; createApp(new Detour().route("/some-silly-path", resource)); v.path("/some-silly-path"); v.test(err => { expect(route.path).toBe("/some-silly-path"); expect(route.resource).toBe(resource); done(err); }); }); }); describe("#use and middleware behavior", function () { it("adds a middleware", function (done) { createApp(new Detour() .use(async function (ctx) { ctx.middlewareAdded = "success"; }) .route("/", { GET (ctx) { ctx.status = 200; ctx.body = ctx.middlewareAdded; }, }) ); v.test(done); }); it("throws if passed non-function", function () { expect(() => { new Detour().use({}); }).toThrow("`use` requires a function"); }); it("middleware isn't invoked when no route is matched", function (done) { let called = false; createApp(new Detour() .use(() => called = true) .route("/test", { GET: worked })); // this will get back the default koa "not found" response, since nothing // in the router matched the request v.expectStatus(404); v.expectBody("Not Found"); v.test(err => { expect(called).toBe(false); done(err); }); }); it("middleware isn't invoked when fallback handlers are used", function (done) { let called = false; createApp(new Detour() .use(() => called = true) .route("/", { POST: worked })); // the default 'methodNotAllowed' handler is responding here v.expectStatus(405); v.expectBody("Method Not Allowed"); v.test(err => { expect(called).toBe(false); done(err); }); }); it("middleware is executed in the order added", function (done) { createApp(new Detour() .use(ctx => ctx.prop = "a") .use(ctx => ctx.prop += "b") .use(ctx => ctx.prop += "c") .route("/", { GET (ctx) { ctx.status = 200; ctx.body = ctx.prop; }, }) ); v.expectBody("abc"); v.test(done); }); }); describe("fallback handlers", function () { it("405's by default when method not supported by resource", function (done) { createApp(new Detour().route("/", { GET: worked })); v.expectStatus(405); v.expectBody("Method Not Allowed"); v.method("POST"); v.test(done); }); it("provides a sane OPTIONS response by default", function (done) { createApp(new Detour().route("/", { GET: worked, POST: worked })); v.expectStatus(200); v.expectBody("Allow: GET,POST"); v.method("OPTIONS"); // TODO test Allow header v.test(done); }); it("provides a sane HEAD response by default", function (done) { createApp(new Detour().route("/", { GET: worked })); v.expectBody(""); // body is automatically stripped v.method("HEAD"); v.test(done); }); it("405 on HEAD if no GET", function (done) { createApp(new Detour().route("/", { POST: worked })); v.method("HEAD"); v.expectStatus(405); v.expectBody(""); v.test(done); }); }); describe("#handle", function () { it("throws if provided an unknown type", function () { expect(() => { new Detour().handle("BAD_KEY", () => {}); }).toThrow("`type` argument must be one of 'OPTIONS', 'HEAD', 'methodNotAllowed', found: BAD_KEY"); }); it("throws if handler argument isn't a function", function () { ["HEAD", "OPTIONS", "methodNotAllowed"].forEach(method => { expect(() => { new Detour().handle(method, {}); }).toThrow("Handler must be a function"); }); }); it("allows overriding `methodNotAllowed`", function (done) { createApp(new Detour() .route("/", { GET: worked }) .handle("methodNotAllowed", function (ctx) { ctx.status = 405; ctx.body = "No way, pal!"; }) ); v.expectStatus(405); v.expectBody("No way, pal!"); v.method("POST"); v.test(done); }); // without this override, OPTIONS would 200 it("allows overriding `OPTIONS`", function (done) { createApp(new Detour() .route("/", { GET: worked }) .handle("OPTIONS", function (ctx) { ctx.status = 404; ctx.body = "Not found"; }) ); v.expectStatus(404); v.expectBody("Not found"); v.method("OPTIONS"); v.test(done); }); // NOTE: Node's HTTP layer strips the body of a HEAD response it("allows overriding `HEAD`", function (done) { createApp(new Detour() .route("/", { GET: worked }) .handle("HEAD", function (ctx) { ctx.status = 404; ctx.body = "head request failed"; }) ); v.expectStatus(404); v.expectBody(""); // body is empty string even though we set it to "head request failed" v.method("HEAD"); v.test(done); }); }); describe("constructor options", function () { it("normally routes case-insensitive", function (done) { createApp(new Detour().route("/LOUD", { GET: worked })); v.path("loud"); v.test(done); }); it("accepts a `caseSensitive` option", function (done) { createApp(new Detour({caseSensitive: true}).route("/LOUD", { GET: worked })); v.path("loud"); v.expectStatus(404); v.expectBody("Not Found"); v.test(err => { if (err) return done(err); v = verity(`http://localhost:${PORT}`, "GET"); v.expectStatus(200); v.expectBody("success"); v.path("LOUD"); v.test(done); }); }); it("normally is loose about trailing slashes", function (done) { createApp(new Detour().route("/test", { GET: worked })); v.path("test/"); v.test(done); }); // can't use v.uri.path because UrlGrey trims trailing slashes it("accepts a `strict` option", function (done) { createApp(new Detour({strict: true}).route("/test" /* NOTE no trailing slash */, { GET: worked })); v.uri = `http://localhost:${PORT}/test/`; // <-- NOTE trailing slash v.expectStatus(404); v.expectBody("Not Found"); v.test(err => { if (err) return done(err); v = verity(`http://localhost:${PORT}`, "GET"); v.path("test"); // <-- NOTE no trailing slash v.expectStatus(200); v.expectBody("success"); v.test(done); }); }); }); describe("#collection", function () { it("throws if `.collection` not present", function () { expect(() => { new Detour().collection("/test/:id", { member: { GET: { worked} } }); }).toThrow(/requires an object/); }); it("does just collection routing", function (done) { createApp(new Detour() .collection("/test/:id/", { collection: { GET: worked } })); v.path("test"); v.test(done); }); it("does member and collection routing", function (done) { createApp(new Detour() .collection("/test/:id", { collection: { GET: worked }, member: { GET (ctx) { ctx.body = ctx.params.id; ctx.status = 200; }, }, }) ); v.path("test"); v.test(err => { if (err) return done(err); v = verity(`http://localhost:${PORT}`, "GET"); v.path("test/abcd"); v.expectStatus(200); v.expectBody("abcd"); v.test(done); }); }); }); describe("#apply", function () { it("takes a function that gets called with the router, and returns the router", function () { let arg; const router = new Detour(); const plugin = _arg => arg = _arg; const result = router.apply(plugin); expect(result).toBe(router); expect(arg).toBe(router); }); it("throws if passed non-function", function () { expect(() => { new Detour().apply({}); }).toThrow("`apply` requires a function"); }); }); }); describe("Route", function () { describe("#params", function () { it("gives an informative error if decoding fails", function () { expect(() => { new Route("/user/:id").params("/user/%E0%A4%A"); }).toThrow(/Failed to decode param '%E0%A4%A'/); }); it("returns null if no match", function () { expect( new Route("/user/:id").params("/document/123") ).toEqual(null); }); }); });