polonez
Version:
micro web server (faster express alternative with dynamic routing - you can add/remove routes while your app is running)
669 lines (580 loc) • 18.8 kB
JavaScript
const http = require("http");
const axios = require("axios");
const polonez = require("../index.js");
const test = require("tape");
const sleep = ms => new Promise(r => setTimeout(r, ms));
const listen = function(app, host) {
app.listen(); // boots
let { port } = app.server.address();
return `http://${host || "localhost"}:${port}`;
};
const METHODS = ["GET", "POST", "PUT", "DELETE"];
const a = {
isEmpty(t, val, msg) {
t.ok(!Object.keys(val).length, msg);
},
isArray(t, val, msg) {
t.ok(Array.isArray(val), msg);
},
isObject(t, val, msg) {
t.is(Object.prototype.toString.call(val), "[object Object]", msg);
},
isFunction(t, val, msg) {
t.is(typeof val, "function", msg);
}
};
test("polonez", t => {
t.is(typeof polonez, "function", "exports a function");
t.end();
});
test("polonez::internals", t => {
let app = polonez();
let proto = app.__proto__;
a.isObject(t, app.opts, "app.opts is an object");
a.isEmpty(t, app.opts, "app.opts is empty");
t.is(
app.server,
undefined,
"app.server is `undefined` initially (pre-listen)"
);
app.listen();
t.ok(
app.server instanceof http.Server,
"~> app.server becomes HTTP server (post-listen)"
);
app.server.close();
a.isFunction(t, app.onError, "app.onError is a function");
a.isFunction(t, app.onNoMatch, "app.onNoMatch is a function");
["parse", "handler"].forEach(k => {
a.isFunction(t, app[k], `app.${k} is a function`);
});
["use", "listen", "handler"].forEach(k => {
a.isFunction(t, proto[k], `app.${k} is a prototype method`);
});
a.isArray(t, app.routes, "app.routes is an array");
METHODS.forEach(k => {
a.isFunction(
t,
app[k.toLowerCase()],
`app.${k.toLowerCase()} is a function`
);
t.is(app.routes[k], undefined, `~> routes.${k} is empty`);
});
t.end();
});
test("polonez::usage::basic", t => {
t.plan(6);
let app = polonez();
let arr = [
["GET", "/"],
["POST", "/users"],
["PUT", "/users/:id"]
];
arr.forEach(([m, p], i) => {
app.add(m, p, _ => t.pass(`~> matched ${m}(${p}) route`));
t.is(app.routes.length, i + 1, "added a new `app.route` definition");
});
arr.forEach(([m, p]) => {
app.find(m, p).handlers.forEach(fn => {
fn();
});
});
});
test("polonez::usage::variadic", async t => {
t.plan(20);
function foo(req, res, next) {
req.foo = req.foo || 0;
req.foo += 250;
next();
}
function bar(req, res, next) {
req.bar = req.bar || "";
req.bar += "bar";
next();
}
function onError(err, req, res, next) {
t.pass('2nd "/err" handler threw error!');
t.is(err, "error", '~> receives the "error" message');
t.is(req.foo, 500, "~> foo() ran twice");
t.is(req.bar, "bar", "~> bar() ran once");
res.statusCode = 400;
res.end("error");
}
let app = polonez({ onError })
.use(foo, bar)
.get("/one", foo, bar, (req, res) => {
t.pass('3rd "/one" handler');
t.is(req.foo, 500, "~> foo() ran twice");
t.is(req.bar, "barbar", "~> bar() ran twice");
res.end("one");
})
.get(
"/two",
foo,
(req, res, next) => {
t.pass('2nd "/two" handler');
t.is(req.foo, 500, "~> foo() ran twice");
t.is(req.bar, "bar", "~> bar() ran once");
req.hello = "world";
next();
},
(req, res) => {
t.pass('3rd "/two" handler');
t.is(req.hello, "world", "~> preserves route handler order");
t.is(req.foo, 500, "~> original `req` object all the way");
res.end("two");
}
)
.get(
"/err",
foo,
(req, res, next) => {
if (true) next("error");
},
(req, res) => {
t.pass("SHOULD NOT RUN");
res.end("wut");
}
);
t.is(
app.find("GET", "/one").handlers.length,
5,
"adds all handlers to GET /one"
);
let uri = listen(app);
let r = await axios.get(uri + "/one");
t.is(r.status, 200, "~> received 200 status");
t.is(r.data, "one", '~> received "one" response');
r = await axios.get(uri + "/two");
t.is(r.status, 200, "~> received 200 status");
t.is(r.data, "two", '~> received "two" response');
await axios.get(uri + "/err").catch(err => {
let r = err.response;
t.is(r.status, 400, "~> received 400 status");
t.is(r.data, "error", '~> received "error" response');
});
app.server.close();
});
test("polonez::usage::middleware", async t => {
t.plan(20);
let app = polonez()
.use((req, res, next) => {
(req.one = "hello") && next();
})
.use("/", (req, res, next) => {
(req.two = "world") && next();
})
.use("/about", (req, res, next) => {
t.is(req.one, "hello", "~> sub-mware runs after first global middleware");
t.is(
req.two,
"world",
"~> sub-mware runs after second global middleware"
);
res.end("About");
})
.use("/subgroup", (req, res, next) => {
req.subgroup = true;
t.is(req.one, "hello", "~> sub-mware runs after first global middleware");
t.is(
req.two,
"world",
"~> sub-mware runs after second global middleware"
);
next();
})
.post("/subgroup", (req, res) => {
t.is(
req.subgroup,
true,
"~~> POST /subgroup ran after its shared middleware"
);
res.end("POST /subgroup");
})
.get("/subgroup/foo", (req, res) => {
t.is(
req.subgroup,
true,
"~~> GET /subgroup/foo ran after its shared middleware"
);
res.end("GET /subgroup/foo");
})
.get("/", (req, res) => {
t.pass("~> matches the GET(/) route");
t.is(req.one, "hello", "~> route handler runs after first middleware");
t.is(req.two, "world", "~> route handler runs after both middlewares!");
res.setHeader("x-type", "text/foo");
res.end("Hello");
});
let uri = listen(app);
let r = await axios.get(uri);
t.is(r.status, 200, "~> received 200 status");
t.is(r.data, "Hello", '~> received "Hello" response');
t.is(r.headers["x-type"], "text/foo", "~> received custom header");
r = await axios.get(uri + "/about");
t.is(r.status, 200, "~> received 200 status");
t.is(r.data, "About", '~> received "About" response');
r = await axios.post(uri + "/subgroup");
t.is(r.status, 200, "~> received 200 status");
t.is(r.data, "POST /subgroup", '~> received "POST /subgroup" response');
r = await axios.get(uri + "/subgroup/foo");
t.is(r.status, 200, "~> received 200 status");
t.is(r.data, "GET /subgroup/foo", '~> received "GET /subgroup/foo" response');
app.server.close();
});
test("polonez::usage::middleware (async)", async t => {
t.plan(6);
let app = polonez()
.use((req, res, next) => {
sleep(10)
.then(_ => {
req.foo = 123;
})
.then(next);
})
.use((req, res, next) => {
sleep(1)
.then(_ => {
req.bar = 456;
})
.then(next);
})
.get("/", (req, res) => {
t.pass("~> matches the GET(/) route");
t.is(req.foo, 123, "~> route handler runs after first middleware");
t.is(req.bar, 456, "~> route handler runs after both middlewares!");
res.setHeader("x-type", "text/foo");
res.end("Hello");
});
let uri = listen(app);
let r = await axios.get(uri);
t.is(r.status, 200, "~> received 200 status");
t.is(r.data, "Hello", '~> received "Hello" response');
t.is(r.headers["x-type"], "text/foo", "~> received custom header");
app.server.close();
});
test("polonez::usage::middleware (basenames)", async t => {
t.plan(37);
let chk = false;
let aaa = (req, res, next) => ((req.aaa = "aaa"), next());
let bbb = (req, res, next) => ((req.bbb = "bbb"), next());
let bar = (req, res, next) => ((req.bar = "bar"), next());
let ccc = (req, res, next) => {
if (chk) {
// runs 2x
t.true(
req.url.includes("/foo"),
"defers `bware` URL mutation until after global"
);
t.true(
req.path.includes("/foo"),
"defers `bware` PATH mutation until after global"
);
chk = false;
}
next();
};
let app = polonez()
.use(aaa, bbb, ccc) // globals
.use("foo", (req, res) => {
// all runs 2 times
t.pass("runs the base middleware for: foo");
t.is(req.aaa, "aaa", "~> runs after `aaa` global middleware");
t.is(req.bbb, "bbb", "~> runs after `bbb` global middleware");
t.false(req.url.includes("foo"), '~> strips "foo" base from `req.url`');
t.false(req.path.includes("foo"), '~> strips "foo" base from `req.path`');
t.ok(
req.originalUrl.includes("foo"),
'~> keeps "foo" base within `req.originalUrl`'
);
res.end("hello from foo");
})
.use("bar", bar, (req, res) => {
t.pass("runs the base middleware for: bar");
t.is(req.aaa, "aaa", "~> runs after `aaa` global middleware");
t.is(req.bbb, "bbb", "~> runs after `bbb` global middleware");
t.is(req.bar, "bar", "~> runs after `bar` SELF-GROUPED middleware");
t.false(req.url.includes("bar"), '~> strips "bar" base from `req.url`');
t.false(req.path.includes("bar"), '~> strips "bar" base from `req.path`');
t.ok(
req.originalUrl.includes("bar"),
'~> keeps "bar" base within `req.originalUrl`'
);
t.is(req.path, "/hello", "~> matches expected `req.path` value");
res.end("hello from bar");
})
.get("/", (req, res) => {
t.pass("runs the MAIN app handler for GET /");
t.is(req.aaa, "aaa", "~> runs after `aaa` global middleware");
t.is(req.bbb, "bbb", "~> runs after `bbb` global middleware");
res.end("hello from main");
});
let uri = listen(app);
let r1 = await axios.get(uri);
t.is(r1.status, 200, "~> received 200 status");
t.is(r1.data, "hello from main", '~> received "hello from main" response');
// Test (GET /foo)
chk = true;
let r2 = await axios.get(`${uri}/foo`);
t.is(r2.status, 200, "~> received 200 status");
t.is(r2.data, "hello from foo", '~> received "hello from foo" response');
// Test (POST /foo/items?this=123)
chk = true;
let r3 = await axios.post(`${uri}/foo/items?this=123`);
t.is(r3.status, 200, "~> received 200 status");
t.is(r3.data, "hello from foo", '~> received "hello from foo" response');
// Test (GET /bar/hello)
let r4 = await axios.get(`${uri}/bar/hello`);
t.is(r4.status, 200, "~> received 200 status");
t.is(r4.data, "hello from bar", '~> received "hello from bar" response');
// Test (GET 404)
await axios.get(`${uri}/foobar`).catch(err => {
let r = err.response;
t.is(r.status, 404, "~> received 404 status");
t.is(r.data, "Not Found", '~> received "Not Found" response');
});
app.server.close();
});
test("polonez::usage::middleware (wildcard)", async t => {
t.plan(29);
let expect;
let foo = (req, res, next) => ((req.foo = "foo"), next());
let app = polonez()
.use(foo) // global
.use("bar", (req, res) => {
// runs 2x
t.pass("runs the base middleware for: bar");
t.is(req.foo, "foo", "~> runs after `foo` global middleware");
t.false(req.url.includes("bar"), '~> strips "bar" base from `req.url`');
t.false(req.path.includes("bar"), '~> strips "bar" base from `req.path`');
t.ok(
req.originalUrl.includes("bar"),
'~> keeps "bar" base within `req.originalUrl`'
);
res.end("hello from bar");
})
.get("*", (req, res) => {
// runs 3x
t.pass("runs the MAIN app handler for GET /*");
t.is(req.foo, "foo", "~> runs after `foo` global middleware");
t.is(req.url, expect, "~> receives the full, expected URL");
res.end("hello from wildcard");
});
let uri = listen(app);
expect = "/";
let r1 = await axios.get(uri);
t.is(r1.status, 200, "~> received 200 status");
t.is(
r1.data,
"hello from wildcard",
'~> received "hello from wildcard" response'
);
expect = "/hello";
let r2 = await axios.get(`${uri}${expect}`);
t.is(r2.status, 200, "~> received 200 status");
t.is(
r2.data,
"hello from wildcard",
'~> received "hello from wildcard" response'
);
expect = "/a/b/c";
let r3 = await axios.get(`${uri}${expect}`);
t.is(r3.status, 200, "~> received 200 status");
t.is(
r3.data,
"hello from wildcard",
'~> received "hello from wildcard" response'
);
let r4 = await axios.get(`${uri}/bar`);
t.is(r4.status, 200, "~> received 200 status");
t.is(r4.data, "hello from bar", '~> received "hello from bar" response');
let r5 = await axios.get(`${uri}/bar/extra`);
t.is(r5.status, 200, "~> received 200 status");
t.is(r5.data, "hello from bar", '~> received "hello from bar" response');
app.server.close();
});
test("polonez::usage::errors", async t => {
t.plan(9);
let a = 41;
// next(Error)
let foo = polonez()
.use((req, res, next) => {
(a += 1) && next(new Error("boo"));
})
.get("/", (req, res) => {
a = 0; // wont run
res.end("OK");
});
let u1 = listen(foo);
await axios.get(u1).catch(err => {
let r = err.response;
t.is(a, 42, "exits before route handler if middleware error");
t.is(r.data, "boo", '~> received "boo" text');
t.is(r.status, 500, "~> received 500 status");
foo.server.close();
});
// next(string)
let bar = polonez()
.use((_, r, next) => {
next("boo~!");
})
.get("/", (_, res) => {
a = 123; // wont run
res.end("OK");
});
let u2 = listen(bar);
await axios.get(u2).catch(err => {
let r = err.response;
t.is(a, 42, "exits without running route handler");
t.is(r.data, "boo~!", '~> received "boo~!" text');
t.is(r.status, 500, "~> received 500 status");
bar.server.close();
});
// early res.end()
let baz = polonez()
.use((_, res) => {
res.statusCode = 501;
res.end("oh dear");
})
.get("/", (_, res) => {
a = 42; // wont run
res.end("OK");
});
let u3 = listen(baz);
await axios.get(u3).catch(err => {
let r = err.response;
t.is(a, 42, "exits without running route handler");
t.is(r.data, "oh dear", '~> received "oh dear" (custom) text');
t.is(r.status, 501, "~> received 501 (custom) status");
baz.server.close();
});
});
test("polonez::usage::middleware w/ sub-app", async t => {
t.plan(19);
function verify(req, res, next) {
t.is(req.main, true, "~> VERIFY middleware ran after MAIN");
req.verify = true;
next();
}
// Construct the "API" sub-application
const api = polonez().use((req, res, next) => {
t.is(req.main, true, "~> API middleware ran after MAIN");
t.is(req.verify, true, "~> API middleware ran after VERIFY");
req.api = true;
next();
});
api.use("/users", (req, res, next) => {
t.is(req.main, true, "~> API/users middleware ran after MAIN");
t.is(req.verify, true, "~> API middleware ran after VERIFY");
t.is(req.api, true, "~> API/users middleware ran after API");
req.users = true;
next();
});
api.get(
"/users/:id",
(req, res, next) => {
t.is(req.main, true, "~> GET API/users/:id #1 ran after MAIN");
t.is(req.verify, true, "~> API middleware ran after VERIFY");
t.is(req.api, true, "~> GET API/users/:id #1 ran after API");
t.is(req.users, true, "~> GET API/users/:id #1 ran after API/users");
t.is(
req.params.id,
"BOB",
"~> GET /API/users/:id #1 received the `params.id` value"
);
req.userid = true;
next();
},
(req, res) => {
t.is(req.main, true, "~> GET API/users/:id #2 ran after MAIN");
t.is(req.verify, true, "~> API middleware ran after VERIFY");
t.is(req.api, true, "~> GET API/users/:id #2 ran after API");
t.is(req.users, true, "~> GET API/users/:id #2 ran after API/users");
t.is(
req.userid,
true,
"~> GET API/users/:id #2 ran after GET API/users/:id #1"
);
t.is(
req.params.id,
"BOB",
"~> GET /API/users/:id #2 received the `params.id` value"
);
res.end(`Hello, ${req.params.id}`);
}
);
// Construct the main application
const main = polonez()
.use((req, res, next) => {
req.main = true;
next();
})
.use("/api", verify, api);
let uri = listen(main);
let r = await axios.get(uri + "/api/users/BOB");
t.is(r.status, 200, "~> received 200 status");
t.is(r.data, "Hello, BOB", '~> received "Hello, BOB" response');
main.server.close();
});
test("polonez::options::server", t => {
let server = http.createServer();
let app = polonez({ server });
t.same(app.server, server, "~> store custom server internally as is");
app.listen();
t.same(
server._events.request,
app.handler,
"~> attach `polonez.handler` to custom server"
);
app.server.close();
t.end();
});
test("polonez::options::onError", async t => {
t.plan(7);
let abc = new Error("boo~!");
abc.code = 418; // teapot lol
let foo = (err, req, res, next) => {
t.is(err, abc, "~> receives the `err` object directly as 1st param");
t.ok(req.url, "~> receives the `req` object as 2nd param");
a.isFunction(t, res.end, "~> receives the `res` object as 3rd param");
a.isFunction(t, next, "~> receives the `next` function 4th param"); // in case want to skip?
res.statusCode = err.code;
res.end("error: " + err.message);
};
let app = polonez({ onError: foo }).use((req, res, next) => next(abc));
t.is(app.onError, foo, "replaces `app.onError` with the option value");
let uri = listen(app);
await axios.get(uri).catch(err => {
let r = err.response;
t.is(r.status, 418, "~> response gets the error code");
t.is(
r.data,
"error: boo~!",
"~> response gets the formatted error message"
);
app.server.close();
});
});
test("polonez::options::onNoMatch", async t => {
t.plan(6);
let foo = (req, res) => {
t.ok(req.url, "~> receives the `req` object as 1st param");
a.isFunction(t, res.end, "~> receives the `res` object as 2nd param");
res.statusCode = 405;
res.end("prefer: Method Not Found");
};
let app = polonez({ onNoMatch: foo }).get("/", _ => {});
t.is(app.onNoMatch, foo, "replaces `app.onNoMatch` with the option value");
t.not(app.onError, foo, "does not affect the `app.onError` handler");
let uri = listen(app);
await axios.post(uri).catch(err => {
let r = err.response;
t.is(r.status, 405, "~> response gets the error code");
t.is(
r.data,
"prefer: Method Not Found",
"~> response gets the formatted error message"
);
app.server.close();
});
});