From c8290126fa8ddb7040edd1ccfd88bf97e3302e43 Mon Sep 17 00:00:00 2001 From: Jonatan Nilsson Date: Sat, 9 Oct 2021 00:12:56 +0000 Subject: [PATCH] pretty much fully finished version 1 --- LICENSE | 13 + benchmark/index.js | 234 ++++++++++++++++-- benchmark/router_flaska.js | 9 +- flaska.mjs | 483 +++++++++++++++++++++++++----------- test.mjs | 17 ++ test/client.mjs | 44 +--- test/flaska.api.test.mjs | 487 ++++++++++++++++++++++++++++++++++++ test/flaska.in.test.mjs | 495 +++++++++++++++++++++++++++++++++++++ test/flaska.out.test.mjs | 249 +++++++++++++++++++ test/flaska.test.mjs | 5 - test/helper.mjs | 118 +++++++++ test/http.test.mjs | 29 +++ test/router.test.mjs | 74 ++++++ 13 files changed, 2045 insertions(+), 212 deletions(-) create mode 100644 LICENSE create mode 100644 test.mjs create mode 100644 test/flaska.api.test.mjs create mode 100644 test/flaska.in.test.mjs create mode 100644 test/flaska.out.test.mjs delete mode 100644 test/flaska.test.mjs create mode 100644 test/helper.mjs create mode 100644 test/http.test.mjs diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..5c93f45 --- /dev/null +++ b/LICENSE @@ -0,0 +1,13 @@ + DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE + Version 2, December 2004 + + Copyright (C) 2004 Sam Hocevar + + Everyone is permitted to copy and distribute verbatim or modified + copies of this license document, and changing it is allowed as long + as the name is changed. + + DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. You just DO WHAT THE FUCK YOU WANT TO. diff --git a/benchmark/index.js b/benchmark/index.js index 7e6b50e..2b43e76 100644 --- a/benchmark/index.js +++ b/benchmark/index.js @@ -1,7 +1,7 @@ import assert from 'assert' import Benchmark from 'benchmarkjs-pretty' import { koaRouter1, koaRouter2 } from './router_koa.js' -import { flaskaRouter1, flaskaRouter2, flaskaClassRouter1, flaskaClassRouter2 } from './router_flaska.js' +import { flaskaRouter1, flaskaRouter2 } from './router_flaska.js' import { expressRouter1, expressRouter2 } from './router_express.js' import * as consts from './const.js' @@ -10,6 +10,216 @@ let testData = null consts.overrideDummy(function() { testData = assertOk }) +function TestPromiseCreators() { + let resolveTrue = Promise.resolve(true) + + let asyncFunc = async function() { + let check = await resolveTrue + if (check !== true) { + throw new Error('false') + } + } + return new Benchmark.default('test different promise creation methods') + .add('new Promise()', function() { + return new Promise((res, rej) => { + try { + resolveTrue.then(function(check) { + res(check) + }) + } catch (err) { + rej(err) + } + }) + }) + .add('new Promise() static', function() { + return new Promise((res, rej) => { + res(true) + }) + }) + .add('new Promise() static with try catch', function() { + return new Promise((res, rej) => { + try { + res(true) + } catch (err) { + rej(err) + } + }) + }) + .add('new Promise() static with one then()', function() { + return new Promise((res, rej) => { + res(true) + }).then(function(check) { + return check + }) + }) + .add('Promise.resolve()', function() { + return Promise.resolve().then(function() { + return resolveTrue + }).then(function(check) { + return check + }) + }) + .add('resolved promise one then()', function() { + return resolveTrue.then(function(check) { + return check + }) + }) + .add('resolved promise but four then()', function() { + return resolveTrue.then(function(check) { + return check + }) + .then(function(item) { return item }) + .then(function(item) { return item }) + .then(function(item) { return item }) + }) + .add('resolved promise but twenty then()', function() { + return resolveTrue.then(function(check) { + if (check !== true) { + throw new Error('false') + } + return true + }) + .then(function(item) { return item }) + .then(function(item) { return item }) + .then(function(item) { return item }) + .then(function(item) { return item }) + .then(function(item) { return item }) + .then(function(item) { return item }) + .then(function(item) { return item }) + .then(function(item) { return item }) + .then(function(item) { return item }) + .then(function(item) { return item }) + .then(function(item) { return item }) + .then(function(item) { return item }) + .then(function(item) { return item }) + .then(function(item) { return item }) + .then(function(item) { return item }) + .then(function(item) { return item }) + .then(function(item) { return item }) + .then(function(item) { return item }) + .then(function(item) { return item }) + }) + .add('async function()', async function() { + let check = await resolveTrue + if (check !== true) { + throw new Error('false') + } + }) + .run() + .then(function() {}, function(e) { + console.error('error:', e) + process.exit(1) + }) +} + +function TestObjectAssign() { + const base = { + state: {}, + log: { + info: function() { }, + warn: function() { }, + error: function() { }, + }, + router1: flaskaRouter1, + router2: flaskaRouter2, + } + const req = { + test: 1, + test2: 2, + koa1: koaRouter1, + koa2: koaRouter2, + test: function() {}, + } + const res = { + express1: expressRouter1, + express2: expressRouter2, + test: function() {}, + } + let propMaker = {} + let propMakerAlt = {} + Object.keys(base).forEach(function(key) { + propMaker[key] = { + enumerable: true, + value: base[key], + writable: true, + configurable: true, + } + propMakerAlt[key] = { + enumerable: true, + value: base[key], + writable: true, + configurable: true, + } + }) + propMakerAlt.req = { + enumerable: true, + get: function() { + return req + }, + configurable: true, + } + propMakerAlt.res = { + enumerable: true, + get: function() { + return res + }, + configurable: true, + } + + function register1(ctx) { + ctx.log = base.log + } + function register2(ctx) { + ctx.router1 = flaskaRouter1 + } + function register3(ctx) { + ctx.router2 = flaskaRouter2 + } + + return new Benchmark.default('test different method to initialize objects)') + .add('Object.assign()', function() { + let ctx = { + req: req, + res: res, + } + Object.assign(ctx, base) + ctx.log.info() + }) + .add('register functions with direct assignment', function() { + let ctx = { + state: {}, + req: req, + res: res, + } + register1(ctx) + register2(ctx) + register3(ctx) + ctx.log.info() + }) + /*.add('Object.create() all props', function() { + let ctx = Object.create({}, propMakerAlt) + ctx.log.info() + }) + .add('Object.defineProperties()', function() { + let ctx = { + req: req, + res: res, + } + Object.defineProperties(ctx, propMaker) + ctx.log.info() + }) + .add('Object.defineProperties() all props', function() { + let ctx = { } + Object.defineProperties(ctx, propMakerAlt) + ctx.log.info() + })*/ + .run() + .then(function() {}, function(e) { + console.error('error:', e) + process.exit(1) + }) +} + function TestSmallStaticRoute() { return new Benchmark.default('Small router static route benchmark: /api/staff (16 routes registered)') .add('expressjs', function() { @@ -28,10 +238,6 @@ function TestSmallStaticRoute() { testData = flaskaRouter1.match('/api/staff') assert.ok(testData.handler) }) - .add('bottle-router-alt', function() { - testData = flaskaClassRouter1.match('/api/staff') - assert.ok(testData.handler) - }) .run() .then(function() {}, function(e) { console.error('error:', e) @@ -57,10 +263,6 @@ function TestSmallParamRoute() { testData = flaskaRouter1.match('/api/staff/justatest') assert.ok(testData.handler) }) - .add('bottle-router-alt', function() { - testData = flaskaClassRouter1.match('/api/staff/justatest') - assert.ok(testData.handler) - }) .run() .then(function() {}, function(e) { console.error('error:', e) @@ -86,10 +288,6 @@ function TestLargeStaticRoute() { testData = flaskaRouter2.match('/api/staff') assert.ok(testData.handler) }) - .add('bottle-router-alt', function() { - testData = flaskaClassRouter2.match('/api/staff') - assert.ok(testData.handler) - }) .run() .then(function() {}, function(e) { console.error('error:', e) @@ -115,10 +313,6 @@ function TestLargeParamRoute() { testData = flaskaRouter2.match('/api/staff/justatest') assert.ok(testData.handler) }) - .add('bottle-router-alt', function() { - testData = flaskaClassRouter2.match('/api/staff/justatest') - assert.ok(testData.handler) - }) .run() .then(function() {}, function(e) { console.error('error:', e) @@ -144,10 +338,6 @@ function TestLargeParamLargeUrlRoute() { testData = flaskaRouter2.match('/api/products/justatest/sub_products/foobar') assert.ok(testData.handler) }) - .add('bottle-router-alt', function() { - testData = flaskaClassRouter2.match('/api/products/justatest/sub_products/foobar') - assert.ok(testData.handler) - }) .run() .then(function() {}, function(e) { console.error('error:', e) @@ -156,6 +346,8 @@ function TestLargeParamLargeUrlRoute() { } TestSmallStaticRoute() +// TestObjectAssign() +// TestPromiseCreators() .then(function() { return TestSmallParamRoute() }) diff --git a/benchmark/router_flaska.js b/benchmark/router_flaska.js index 03603ff..c114fd6 100644 --- a/benchmark/router_flaska.js +++ b/benchmark/router_flaska.js @@ -1,25 +1,18 @@ -import { FlaskaRouter, FlaskaRouterClass } from '../flaska.mjs' +import { FlaskaRouter } from '../flaska.mjs' import * as consts from './const.js' const router1 = new FlaskaRouter() const router2 = new FlaskaRouter() -const classRouter1 = new FlaskaRouterClass() -const classRouter2 = new FlaskaRouterClass() - for (let key in consts.allRoutes) { router1.addRoute(consts.allRoutes[key], consts.dummy) - classRouter1.addRoute(consts.allRoutes[key], consts.dummy) } for (let key in consts.allManyRoutes) { router2.addRoute(consts.allManyRoutes[key], consts.dummy) - classRouter2.addRoute(consts.allManyRoutes[key], consts.dummy) } export { router1 as flaskaRouter1, router2 as flaskaRouter2, - classRouter1 as flaskaClassRouter1, - classRouter2 as flaskaClassRouter2, } diff --git a/flaska.mjs b/flaska.mjs index 1d38a17..2201eb5 100644 --- a/flaska.mjs +++ b/flaska.mjs @@ -1,3 +1,6 @@ +import http from 'http' +import stream from 'stream' + /** * Router */ @@ -12,9 +15,41 @@ class Branch { } } +export const ErrorCodes = { + ERR_CONNECTION_ABORTED: 'ERR_CON_ABORTED' +} + +// Taken from https://github.com/nfp-projects/koa-lite/blob/master/lib/statuses.js +const statuses = { + 100: 'Continue', 101: 'Switching Protocols', 102: 'Processing', 103: 'Early Hints', + 200: 'OK', 201: 'Created', 202: 'Accepted', 203: 'Non-Authoritative Information', 204: 'No Content', 205: 'Reset Content', 206: 'Partial Content', 207: 'Multi-Status', 208: 'Already Reported', 226: 'IM Used', + 300: 'Multiple Choices', 301: 'Moved Permanently', 302: 'Found', 303: 'See Other', 304: 'Not Modified', 305: 'Use Proxy', 306: '(Unused)', 307: 'Temporary Redirect', 308: 'Permanent Redirect', + 400: 'Bad Request', 401: 'Unauthorized', 402: 'Payment Required', 403: 'Forbidden', 404: 'Not Found', 405: 'Method Not Allowed', 406: 'Not Acceptable', 407: 'Proxy Authentication Required', 408: 'Request Timeout', 409: 'Conflict', 410: 'Gone', 411: 'Length Required', 412: 'Precondition Failed', 413: 'Payload Too Large', 414: 'URI Too Long', 415: 'Unsupported Media Type', 416: 'Range Not Satisfiable', 417: 'Expectation Failed', 418: 'I\'m a teapot', 421: 'Misdirected Request', 422: 'Unprocessable Entity', 423: 'Locked', 424: 'Failed Dependency', 425: 'Too Early', 426: 'Upgrade Required', 428: 'Precondition Required', 429: 'Too Many Requests', 431: 'Request Header Fields Too Large', 451: 'Unavailable For Legal Reasons', + 500: 'Internal Server Error', 501: 'Not Implemented', 502: 'Bad Gateway', 503: 'Service Unavailable', 504: 'Gateway Timeout', 505: 'HTTP Version Not Supported', 506: 'Variant Also Negotiates', 507: 'Insufficient Storage', 508: 'Loop Detected', 509: 'Bandwidth Limit Exceeded', 510: 'Not Extended', 511: 'Network Authentication Required', + redirect: { + 300: true, + 301: true, + 302: true, + 303: true, + 305: true, + 307: true, + 308: true + }, + empty: { + 204: true, + 205: true, + 304: true + } +} const __paramMapName = '__param' const __fullParamMapName = '__fullparam' +function assertIsHandler(handler, name) { + if (typeof(handler) !== 'function') { + throw new Error(`${name} was called with a handler that was not a function`) + } +} + export class FlaskaRouter { constructor() { this.root = new Branch() @@ -33,9 +68,7 @@ export class FlaskaRouter { if (middlewares && typeof(middlewares) === 'function') { middlewares = [middlewares] } - if (typeof(handler) !== 'function') { - throw new Error(`route "${route}" was missing a handler`) - } + assertIsHandler(handler, 'addRoute()') let start = 1 let end = 1 @@ -150,14 +183,34 @@ export class FlaskaRouter { else if (output = branch.children.get(__paramMapName)) { branch = output params[branch.paramName] = name + } + else if (output = branch.children.get(__fullParamMapName)) { + params[output.fullparamName] = url.slice(start) + return { + handler: output.handler, + middlewares: output.middlewares, + params: params, + } } else { + if (output = this.root.children.get(__fullParamMapName)) { + params = { + [output.fullparamName]: url.slice(1) + } + return { + handler: output.handler, + middlewares: output.middlewares, + params: params, + } + } return null } i++ end = start = i char = url[i] } - if (i >= url.length) { + // Check branch.handler. This can happen if route /::path is added + // and request is '/' it will attempt to match root which will fail + if (i >= url.length && branch.handler) { return { handler: branch.handler, middlewares: branch.middlewares, @@ -170,167 +223,313 @@ export class FlaskaRouter { end++ } } + if (output = this.root.children.get(__fullParamMapName)) { + params = { + [output.fullparamName]: url.slice(1) + } + return { + handler: output.handler, + middlewares: output.middlewares, + params: params, + } + } return null } } -export function FlaskaRouter() { - this.root = new Branch() -} +/** + * Flaska + */ +export class Flaska { + constructor(opts, orgHttp = http, orgStream = stream) { + this._before = [] + this._beforeCompiled = null + this._beforeAsync = [] + this._beforeAsyncCompiled = null + this._after = [] + this._afterCompiled = null + this._afterAsync = [] + this._afterAsyncCompiled = null + this._on404 = function(ctx) { + ctx.status = 404 + ctx.body = { + status: 404, + message: statuses[404], + } + } + this._backuperror = this._onerror = function(err, ctx) { + ctx.log.error(err) + ctx.status = 500 + ctx.body = { + status: 500, + message: statuses[500], + } + } + this._onreqerror = function(err, ctx) { + ctx.log.error(err) + ctx.res.statusCode = ctx.statusCode = 400 + ctx.res.end() + ctx.finished = true + } + this._onreserror = function(err, ctx) { + ctx.log.error(err) + } -FlaskaRouter.prototype.addRoute = function(route, orgMiddlewares, orgHandler) { - if (route[0] !== '/') - throw new Error(`route "${route}" must start with forward slash`) + let options = opts || {} - let middlewares = orgMiddlewares - let handler = orgHandler - if (!orgHandler) { - handler = orgMiddlewares - middlewares = [] - } - if (middlewares && typeof(middlewares) === 'function') { - middlewares = [middlewares] - } - if (typeof(handler) !== 'function') { - throw new Error(`route "${route}" was missing a handler`) + this.log = options.log || console + this.http = orgHttp + this.stream = orgStream + this.server = null + this.routers = { + 'GET': new FlaskaRouter(), + 'POST': new FlaskaRouter(), + 'PUT': new FlaskaRouter(), + 'DELETE': new FlaskaRouter(), + 'OPTIONS': new FlaskaRouter(), + 'PATCH': new FlaskaRouter(), + } + this.get = this.routers.GET.addRoute.bind(this.routers.GET) + this.post = this.routers.POST.addRoute.bind(this.routers.POST) + this.put = this.routers.PUT.addRoute.bind(this.routers.PUT) + this.delete = this.routers.DELETE.addRoute.bind(this.routers.DELETE) + this.options = this.routers.OPTIONS.addRoute.bind(this.routers.OPTIONS) + this.patch = this.routers.PATCH.addRoute.bind(this.routers.PATCH) } - let start = 1 - let end = 1 - let name = '' - let param = '' - let isParam = false - let isFullParam = false - let branch = this.root - - if (route.indexOf(':') < 0) { - let name = route - if (name.length > 1 && name[name.length - 1] === '/') { - name = name.slice(0, -1) + _assertIsHandler(handler, name) { + if (typeof(handler) !== 'function') { + throw new Error(`${name} was called with a handler that was not a function`) } - let child = new Branch() - branch.children.set(name, child) - child.handler = handler - child.middlewares = middlewares } - for (let i = 1; i <= route.length; i++) { - if ((i === route.length || route[i] === '/') && end > start) { - if (branch.fullparamName) { - throw new Error(`route "${route}" conflicts with a sub-branch that has a full param child`) + devMode() { + this._backuperror = this._onerror = function(err, ctx) { + ctx.log.error(err) + ctx.status = 500 + ctx.body = { + status: 500, + message: `${statuses[500]}: ${err.message}`, + stack: err.stack || '', } - let child - name = route.substring(start, end) - if (isFullParam) { - param = name - name = __fullParamMapName - } else if (isParam) { - param = name - name = __paramMapName - } - if (branch.children.has(name)) { - child = branch.children.get(name) - } - else if (isParam && !isFullParam && branch.children.has(__fullParamMapName)) { - throw new Error(`route "${route}" conflicts with a sub-branch that has a full param child`) - } - else if (isFullParam && branch.children.has(__paramMapName)) { - throw new Error(`route "${route}" conflicts with a sub-branch that has a partial param child`) - } - else { - child = new Branch() - branch.children.set(name, child) - } - branch = child - end = i - start = i - if (isParam) { - if (branch.paramName && branch.paramName !== param) { - throw new Error(`route "${route}" conflicts with pre-existing param name of ${branch.paramName} instead of ${param}`) - } - if (isFullParam) { - branch.fullparamName = param - } else { - branch.paramName = param - } - isParam = false - } - } else if (route[i] === '/' && end === start) { - throw new Error(`route "${route}" has missing path name inbetween slashes`) - } - if (i === route.length) { - branch.handler = handler - branch.middlewares = middlewares - continue - } - if (route[i] === ':') { - if (isParam) { - isFullParam = true - } - isParam = true - end = start = i + 1 - } - else if (route[i] === '/') { - end = start = i + 1 - } - else { - end++ } } -} -FlaskaRouter.prototype.match = function(orgUrl) { - let url = orgUrl - if (url.length > 1 && url[url.length - 1] === '/') { - url = url.slice(0, -1) + on404(handler) { + assertIsHandler(handler, 'on404()') + this._on404 = handler } - let branch = this.root - let start = 1 - let end = 1 - let output - let name - let char - let params = {} - if (output = branch.children.get(url)) { - return { - handler: output.handler, - middlewares: output.middlewares, - params: params, + + onerror(handler) { + assertIsHandler(handler, 'onerror()') + this._onerror = handler + } + + onreqerror(handler) { + assertIsHandler(handler, 'onreqerror()') + this._onreqerror = handler + } + + onreserror(handler) { + assertIsHandler(handler, 'onreserror()') + this._onreserror = handler + } + + onerror(handler) { + assertIsHandler(handler, 'onerror()') + this._onerror = handler + } + + before(handler) { + assertIsHandler(handler, 'before()') + this._before.push(handler) + } + + beforeAsync(handler) { + assertIsHandler(handler, 'beforeAsync()') + this._beforeAsync.push(handler) + } + + after(handler) { + assertIsHandler(handler, 'after()') + this._after.push(handler) + } + + afterAsync(handler) { + assertIsHandler(handler, 'afterAsync()') + this._afterAsync.push(handler) + } + + requestStart(req, res) { + let url = req.url + let search = '' + let hasSearch = url.indexOf('?') + + if (hasSearch > 0) { + search = url.slice(hasSearch) + url = url.slice(0, hasSearch) + } + + let ctx = { + req: req, + res: res, + method: req.method, + url: url, + search: search, + state: {}, + status: 200, + body: null, + type: null, + length: null, + } + + req.on('error', (err) => { + this._onreqerror(err, ctx) + }) + res.on('error', (err) => { + this._onreserror(err, ctx) + }) + + req.on('aborted', function() { + ctx.aborted = true + }) + req.on('close', () => { + this.requestEnded() + }) + + try { + this._beforeCompiled(ctx) + if (this._beforeAsyncCompiled) { + return this._beforeAsyncCompiled(ctx) + .then(() => { + this.requestStartInternal(ctx) + }).catch(err => { + this.requestEnd(err, ctx) + }) + } + this.requestStartInternal(ctx) + } + catch (err) { + this.requestEnd(err, ctx) } } - for (let i = 1; i <= url.length; i++) { - char = url[i] - if ((i === url.length || char === '/') && end > start) { - name = url.slice(start, end) - if (output = branch.children.get(name)) { - branch = output - } - else if (output = branch.children.get(__paramMapName)) { - branch = output - params[branch.paramName] = name - } else { - return null - } - i++ - end = start = i - char = url[i] - } - if (i >= url.length) { - return { - handler: branch.handler, - middlewares: branch.middlewares, - params: params, + + requestStartInternal(ctx) { + let route = this.routers[ctx.method].match(ctx.url) + ctx.params = route.params + + if (route.middlewares.length) { + let middle = this.handleMiddleware(ctx, route.middlewares, 0) + + if (middle && middle.then) { + return middle.then(() => { + return route.handler(ctx) + }) + .then(() => { + this.requestEnd(null, ctx) + }, err => { + this.requestEnd(err, ctx) + }) } } - if (char === '/') { - end = start = i + 1 + let handler = route.handler(ctx) + if (handler && handler.then) { + return handler.then(() => { + this.requestEnd(null, ctx) + }, err => { + this.requestEnd(err, ctx) + }) + } + this.requestEnd(null, ctx) + } + + handleMiddleware(ctx, middles, index) { + for (let i = index; i < middles.length; i++) { + let res = middles[i](ctx) + if (res && res.then) { + return res.then(() => { + return this.handleMiddleware(ctx, middles, i + 1) + }) + } + } + } + + requestEnd(err, ctx) { + if (err) { + this._onerror(err, ctx) + return ctx.res.end() + } + if (ctx.req.complete) { + return + } + + ctx.res.statusCode = ctx.status + if (statuses.empty[ctx.status]) { + return ctx.res.end() + } + + let body = ctx.body + let length = 0 + if (body && typeof(body.pipe) === 'function') { + ctx.res.setHeader('Content-Type', ctx.type || 'application/octet-stream') + return this.stream.pipeline(body, ctx.res, function() {}) + } + if (typeof(body) === 'object') { + body = JSON.stringify(body) + length = Buffer.byteLength(body) + ctx.res.setHeader('Content-Type', 'application/json; charset=utf-8') } else { - end++ + body = body.toString() + length = Buffer.byteLength(body) + ctx.res.setHeader('Content-Type', ctx.type || 'text/plain; charset=utf-8') + } + ctx.res.setHeader('Content-Length', length) + ctx.res.end(body) + } + + requestEnded(ctx) { + this._afterCompiled(ctx) + if (this._afterAsyncCompiled) { + return this._afterAsyncCompiled(ctx).then() } } - return null -} -export function Flaska() { + compile() { + let types = ['before', 'after'] + for (let i = 0; i < types.length; i++) { + let type = types[i] + let args = '' + let body = '' + for (let i = 0; i < this['_' + type].length; i++) { + args += `a${i}, ` + body += `a${i}(ctx);` + } + args += 'ctx' + let func = new Function(args, body) + this[`_${type}Compiled`] = func.bind(this, ...this['_' + type]) + + if (this[`_${type}Async`].length) { + args = '' + body = 'return Promise.all([' + for (let i = 0; i < this[`_${type}Async`].length; i++) { + args += `a${i}, ` + body += `a${i}(ctx),` + } + args += 'ctx' + body += '])' + func = new Function(args, body) + this[`_${type}AsyncCompiled`] = func.bind(this, ...this[`_${type}Async`]) + } + } + } + + listen(port, cb) { + if (typeof(port) !== 'number') { + throw new Error('Flaska.listen() called with non-number in port') + } + this.compile() + this.server = this.http.createServer(this.requestStart.bind(this)) + this.server.listen(port, cb) + } } diff --git a/test.mjs b/test.mjs new file mode 100644 index 0000000..5d76f28 --- /dev/null +++ b/test.mjs @@ -0,0 +1,17 @@ +import { Flaska } from './flaska.mjs' + +const port = 51024 +const flaska = new Flaska({}, ) + +flaska.get('/', function(ctx) { + return new Promise(function(res, rej) { + ctx.body = { status: true } + setTimeout(res, 1000) + }).then(function() { + process.stdout.write(".") + }) +}) + +flaska.listen(port, function() { + console.log('listening on port', port) +}) diff --git a/test/client.mjs b/test/client.mjs index 839d35e..63b7395 100644 --- a/test/client.mjs +++ b/test/client.mjs @@ -1,39 +1,6 @@ import http from 'http' import { URL } from 'url' - -// taken from isobject npm library -function isObject(val) { - return val != null && typeof val === 'object' && Array.isArray(val) === false -} - -function defaults(options, def) { - let out = { } - - if (options) { - Object.keys(options || {}).forEach(key => { - out[key] = options[key] - - if (Array.isArray(out[key])) { - out[key] = out[key].map(item => { - if (isObject(item)) return defaults(item) - return item - }) - } else if (out[key] && typeof out[key] === 'object') { - out[key] = defaults(options[key], def && def[key]) - } - }) - } - - if (def) { - Object.keys(def).forEach(function(key) { - if (typeof out[key] === 'undefined') { - out[key] = def[key] - } - }) - } - - return out -} +import { defaults } from './helper.mjs' export default function Client(port, opts) { this.options = defaults(opts, {}) @@ -48,6 +15,7 @@ Client.prototype.customRequest = function(method = 'GET', path, body, options) { return new Promise((resolve, reject) => { const opts = defaults(defaults(options, { + agent: false, method: method, timeout: 500, protocol: urlObj.protocol, @@ -64,7 +32,10 @@ Client.prototype.customRequest = function(method = 'GET', path, body, options) { } req.on('error', reject) - req.on('timeout', function() { reject(new Error(`Request ${method} ${path} timed out`)) }) + req.on('timeout', function() { + console.log(req.destroy()) + reject(new Error(`Request ${method} ${path} timed out`)) + }) req.on('response', res => { res.setEncoding('utf8') let output = '' @@ -79,7 +50,7 @@ Client.prototype.customRequest = function(method = 'GET', path, body, options) { } catch (e) { return reject(new Error(`${e.message} while decoding: ${output}`)) } - if (output.status) { + if (output.status && typeof(output.status) === 'number') { let err = new Error(`Request failed [${output.status}]: ${output.message}`) err.body = output return reject(err) @@ -88,6 +59,7 @@ Client.prototype.customRequest = function(method = 'GET', path, body, options) { }) }) req.end() + return req }) } diff --git a/test/flaska.api.test.mjs b/test/flaska.api.test.mjs new file mode 100644 index 0000000..f7d2bda --- /dev/null +++ b/test/flaska.api.test.mjs @@ -0,0 +1,487 @@ +import { Eltro as t, assert} from 'eltro' +import { Flaska } from '../flaska.mjs' +import { spy, createCtx, fakeHttp } from './helper.mjs' + +const faker = fakeHttp() + +t.test('should be able to override the http', function() { + let flaska = new Flaska({}, faker) + assert.strictEqual(flaska.http, faker) +}) + +t.test('it should have all the common verbs', function() { + let flaska = new Flaska({}, faker) + assert.ok(flaska.get) + assert.strictEqual(typeof(flaska.get), 'function') + assert.ok(flaska.post) + assert.strictEqual(typeof(flaska.post), 'function') + assert.ok(flaska.put) + assert.strictEqual(typeof(flaska.put), 'function') + assert.ok(flaska.delete) + assert.strictEqual(typeof(flaska.delete), 'function') + assert.ok(flaska.options) + assert.strictEqual(typeof(flaska.options), 'function') + assert.ok(flaska.patch) + assert.strictEqual(typeof(flaska.patch), 'function') +}) + +t.describe('#log', function() { + t.test('default have a logger valid', function() { + let flaska = new Flaska({}, faker) + assert.strictEqual(typeof(flaska.log.error), 'function') + assert.strictEqual(typeof(flaska.log.info), 'function') + assert.strictEqual(typeof(flaska.log.warn), 'function') + }) + + t.test('allow overwriting in options', function() { + const assertFunction = function() { return 1 } + let flaska = new Flaska({ log: { + error: assertFunction, + info: assertFunction, + warn: assertFunction, + debug: assertFunction, + } }, faker) + assert.strictEqual(flaska.log.error, assertFunction) + assert.strictEqual(flaska.log.info, assertFunction) + assert.strictEqual(flaska.log.warn, assertFunction) + assert.strictEqual(flaska.log.debug, assertFunction) + }) +}) + +let specialHandlers = ['on404', 'onerror', 'onreqerror', 'onreserror'] + +specialHandlers.forEach(function(type) { + t.describe(`#${type}()`, function() { + t.test('exist', function() { + let flaska = new Flaska({}, faker) + assert.strictEqual(typeof(flaska[type]), 'function') + }) + + t.test('validate handler', function() { + let flaska = new Flaska({}, faker) + assert.throws(function() { flaska[type]() }, /[Ff]unction/) + assert.throws(function() { flaska[type]('asdf') }, /[Ff]unction/) + assert.throws(function() { flaska[type]('123') }, /[Ff]unction/) + assert.throws(function() { flaska[type]([]) }, /[Ff]unction/) + assert.throws(function() { flaska[type]({}) }, /[Ff]unction/) + assert.throws(function() { flaska[type](1234) }, /[Ff]unction/) + }) + + if (type !== 'on404') { + t.test('should call ctx.log.error correctly', function() { + const assertError = new Error('Samuraism') + let flaska = new Flaska({}, faker) + let ctx = createCtx() + flaska['_' + type](assertError, ctx) + assert.strictEqual(ctx.log.error.callCount, 1) + assert.strictEqual(ctx.log.error.firstCall[0], assertError) + }) + } + + if (type === 'onreqerror') { + t.test('default sends 400 immediately', function() { + let flaska = new Flaska({}, faker) + let ctx = createCtx() + flaska['_' + type](new Error(), ctx) + assert.strictEqual(ctx.res.statusCode, 400) + assert.strictEqual(ctx.finished, true) + assert.strictEqual(ctx.res.end.callCount, 1) + assert.strictEqual(ctx.res.end.firstCall.length, 0) + }) + } + + t.test('register it into flaska', function() { + const assertFunction = function() { return true } + let flaska = new Flaska({}, faker) + flaska[type](assertFunction) + assert.strictEqual(flaska['_' + type], assertFunction) + }) + }) +}) + +t.describe('_on404', function() { + t.test('a valid function', function() { + let flaska = new Flaska({}, faker) + assert.strictEqual(typeof(flaska._on404), 'function') + }) + + t.test('default valid handling of context', function() { + let ctx = createCtx() + let flaska = new Flaska({}, faker) + flaska._on404(ctx) + assert.strictEqual(ctx.status, 404) + assert.deepStrictEqual(ctx.body, { + status: 404, + message: 'Not Found', + }) + }) +}) + +t.describe('_onerror', function() { + t.test('a valid function', function() { + let flaska = new Flaska({}, faker) + assert.strictEqual(typeof(flaska._onerror), 'function') + }) + + t.test('default valid handling of context', function() { + const assertError = new Error('should not be seen') + let ctx = createCtx() + let flaska = new Flaska({}, faker) + flaska._onerror(assertError, ctx) + assert.strictEqual(ctx.log.error.callCount, 1) + assert.strictEqual(ctx.log.error.firstCall[0], assertError) + assert.strictEqual(ctx.status, 500) + assert.deepStrictEqual(ctx.body, { + status: 500, + message: 'Internal Server Error', + }) + }) +}) + +t.describe('#devMode()', function() { + t.test('turns on debug mode', function() { + const assertErrorMessage = 'Fascination' + const assertError = new Error(assertErrorMessage) + let ctx = createCtx() + let flaska = new Flaska({}, faker) + flaska._onerror(assertError, ctx) + assert.strictEqual(ctx.log.error.callCount, 1) + assert.strictEqual(ctx.log.error.firstCall[0], assertError) + assert.strictEqual(ctx.status, 500) + assert.deepStrictEqual(ctx.body, { + status: 500, + message: 'Internal Server Error', + }) + ctx = createCtx() + flaska._backuperror(assertError, ctx) + assert.strictEqual(ctx.log.error.callCount, 1) + assert.strictEqual(ctx.log.error.firstCall[0], assertError) + assert.strictEqual(ctx.status, 500) + assert.deepStrictEqual(ctx.body, { + status: 500, + message: 'Internal Server Error', + }) + flaska.devMode() + ctx = createCtx() + flaska._onerror(assertError, ctx) + assert.strictEqual(ctx.log.error.callCount, 1) + assert.strictEqual(ctx.log.error.firstCall[0], assertError) + assert.strictEqual(ctx.status, 500) + assert.strictEqual(ctx.body.status, 500) + assert.match(ctx.body.message, /Internal Server Error/) + assert.match(ctx.body.message, new RegExp(assertErrorMessage)) + assert.ok(ctx.body.stack) + ctx = createCtx() + flaska._backuperror(assertError, ctx) + assert.strictEqual(ctx.log.error.callCount, 1) + assert.strictEqual(ctx.log.error.firstCall[0], assertError) + assert.strictEqual(ctx.status, 500) + assert.strictEqual(ctx.body.status, 500) + assert.match(ctx.body.message, /Internal Server Error/) + assert.match(ctx.body.message, new RegExp(assertErrorMessage)) + assert.ok(ctx.body.stack) + }) +}) + +t.describe('#before()', function() { + t.test('should throw if not a function', function() { + let flaska = new Flaska({}, faker) + assert.throws(function() { flaska.before() }, /[Ff]unction/) + assert.throws(function() { flaska.before('asdf') }, /[Ff]unction/) + assert.throws(function() { flaska.before('123') }, /[Ff]unction/) + assert.throws(function() { flaska.before([]) }, /[Ff]unction/) + assert.throws(function() { flaska.before({}) }, /[Ff]unction/) + assert.throws(function() { flaska.before(1234) }, /[Ff]unction/) + }) + + t.test('add handler to preflight list', function() { + const assertFunction = function() {} + let flaska = new Flaska({}, faker) + assert.ok(flaska._before) + flaska.before(assertFunction) + assert.strictEqual(flaska._before.length, 1) + assert.strictEqual(flaska._before[0], assertFunction) + }) +}) + +t.describe('#beforeAsync()', function() { + t.test('should throw if not a function', function() { + let flaska = new Flaska({}, faker) + assert.throws(function() { flaska.beforeAsync() }, /[Ff]unction/) + assert.throws(function() { flaska.beforeAsync('asdf') }, /[Ff]unction/) + assert.throws(function() { flaska.beforeAsync('123') }, /[Ff]unction/) + assert.throws(function() { flaska.beforeAsync([]) }, /[Ff]unction/) + assert.throws(function() { flaska.beforeAsync({}) }, /[Ff]unction/) + assert.throws(function() { flaska.beforeAsync(1234) }, /[Ff]unction/) + }) + + t.test('add handler to preflight list', function() { + const assertFunction = function() {} + let flaska = new Flaska({}, faker) + assert.ok(flaska._beforeAsync) + flaska.beforeAsync(assertFunction) + assert.strictEqual(flaska._beforeAsync.length, 1) + assert.strictEqual(flaska._beforeAsync[0], assertFunction) + }) +}) + +t.describe('#after()', function() { + t.test('should throw if not a function', function() { + let flaska = new Flaska({}, faker) + assert.throws(function() { flaska.after() }, /[Ff]unction/) + assert.throws(function() { flaska.after('asdf') }, /[Ff]unction/) + assert.throws(function() { flaska.after('123') }, /[Ff]unction/) + assert.throws(function() { flaska.after([]) }, /[Ff]unction/) + assert.throws(function() { flaska.after({}) }, /[Ff]unction/) + assert.throws(function() { flaska.after(1234) }, /[Ff]unction/) + }) + + t.test('add handler to preflight list', function() { + const assertFunction = function() {} + let flaska = new Flaska({}, faker) + assert.ok(flaska._after) + flaska.after(assertFunction) + assert.strictEqual(flaska._after.length, 1) + assert.strictEqual(flaska._after[0], assertFunction) + }) +}) + +t.describe('#afterAsync()', function() { + t.test('should throw if not a function', function() { + let flaska = new Flaska({}, faker) + assert.throws(function() { flaska.afterAsync() }, /[Ff]unction/) + assert.throws(function() { flaska.afterAsync('asdf') }, /[Ff]unction/) + assert.throws(function() { flaska.afterAsync('123') }, /[Ff]unction/) + assert.throws(function() { flaska.afterAsync([]) }, /[Ff]unction/) + assert.throws(function() { flaska.afterAsync({}) }, /[Ff]unction/) + assert.throws(function() { flaska.afterAsync(1234) }, /[Ff]unction/) + }) + + t.test('add handler to preflight list', function() { + const assertFunction = function() {} + let flaska = new Flaska({}, faker) + assert.ok(flaska._afterAsync) + flaska.afterAsync(assertFunction) + assert.strictEqual(flaska._afterAsync.length, 1) + assert.strictEqual(flaska._afterAsync[0], assertFunction) + }) +}) + +t.describe('#compile()', function() { + t.test('join all before together in one function', function() { + let flaska = new Flaska({}, faker) + flaska.before(function(ctx) { ctx.a = 1 }) + flaska.before(function(ctx) { ctx.b = 2 }) + flaska.before(function(ctx) { ctx.c = 3 }) + flaska.before(function(ctx) { ctx.d = 4 }) + assert.notOk(flaska._beforeCompiled) + flaska.compile() + assert.ok(flaska._beforeCompiled) + assert.notOk(flaska._beforeAsyncCompiled) + assert.strictEqual(typeof(flaska._beforeCompiled), 'function') + let ctx = createCtx() + flaska._beforeCompiled(ctx) + assert.strictEqual(ctx.a, 1) + assert.strictEqual(ctx.b, 2) + assert.strictEqual(ctx.c, 3) + assert.strictEqual(ctx.d, 4) + }) + t.test('join all beforeAsync together in one function', function() { + let flaska = new Flaska({}, faker) + flaska.beforeAsync(function(ctx) { return Promise.resolve().then(function() { ctx.a = 1 }) }) + flaska.beforeAsync(function(ctx) { ctx.b = 2 }) + flaska.beforeAsync(function(ctx) { return new Promise(function(res) { ctx.c = 3; res() }) }) + flaska.beforeAsync(function(ctx) { ctx.d = 4 }) + assert.notOk(flaska._beforeAsyncCompiled) + flaska.compile() + assert.ok(flaska._beforeAsyncCompiled) + assert.strictEqual(typeof(flaska._beforeAsyncCompiled), 'function') + let ctx = createCtx() + return flaska._beforeAsyncCompiled(ctx).then(function() { + assert.strictEqual(ctx.a, 1) + assert.strictEqual(ctx.b, 2) + assert.strictEqual(ctx.c, 3) + assert.strictEqual(ctx.d, 4) + }) + }) + t.test('join all after together in one function', function() { + let flaska = new Flaska({}, faker) + flaska.after(function(ctx) { ctx.a = 1 }) + flaska.after(function(ctx) { ctx.b = 2 }) + flaska.after(function(ctx) { ctx.c = 3 }) + flaska.after(function(ctx) { ctx.d = 4 }) + assert.notOk(flaska._afterCompiled) + flaska.compile() + assert.ok(flaska._afterCompiled) + assert.notOk(flaska._afterAsyncCompiled) + assert.strictEqual(typeof(flaska._afterCompiled), 'function') + let ctx = createCtx() + flaska._afterCompiled(ctx) + assert.strictEqual(ctx.a, 1) + assert.strictEqual(ctx.b, 2) + assert.strictEqual(ctx.c, 3) + assert.strictEqual(ctx.d, 4) + }) + t.test('join all afterAsync together in one function', function() { + let flaska = new Flaska({}, faker) + flaska.afterAsync(function(ctx) { return Promise.resolve().then(function() { ctx.a = 1 }) }) + flaska.afterAsync(function(ctx) { ctx.b = 2 }) + flaska.afterAsync(function(ctx) { return new Promise(function(res) { ctx.c = 3; res() }) }) + flaska.afterAsync(function(ctx) { ctx.d = 4 }) + assert.notOk(flaska._afterAsyncCompiled) + flaska.compile() + assert.ok(flaska._afterAsyncCompiled) + assert.strictEqual(typeof(flaska._afterAsyncCompiled), 'function') + let ctx = createCtx() + return flaska._afterAsyncCompiled(ctx).then(function() { + assert.strictEqual(ctx.a, 1) + assert.strictEqual(ctx.b, 2) + assert.strictEqual(ctx.c, 3) + assert.strictEqual(ctx.d, 4) + }) + }) +}) + +t.describe('#handleMiddleware()', function() { + t.test('should work with empty array', function() { + let flaska = new Flaska({}, faker) + flaska.handleMiddleware({}, [], 0) + }) + + t.test('should work with correct index', function() { + let checkIsTrue = false + let flaska = new Flaska({}, faker) + flaska.handleMiddleware({}, [ + function() { throw new Error('should not be thrown') }, + function() { throw new Error('should not be thrown') }, + function() { throw new Error('should not be thrown') }, + function() { checkIsTrue = true }, + ], 3) + assert.strictEqual(checkIsTrue, true) + }) + + t.test('should work with static functions', function() { + const assertCtx = createCtx({ a: 1 }) + let checkCounter = 0 + let flaska = new Flaska({}, faker) + flaska.handleMiddleware(assertCtx, [ + function(ctx) { assert.strictEqual(ctx, assertCtx); checkCounter++ }, + function(ctx) { assert.strictEqual(ctx, assertCtx); checkCounter++ }, + function(ctx) { assert.strictEqual(ctx, assertCtx); checkCounter++ }, + function(ctx) { assert.strictEqual(ctx, assertCtx); checkCounter++ }, + function(ctx) { assert.strictEqual(ctx, assertCtx); checkCounter++ }, + ], 0) + assert.strictEqual(checkCounter, 5) + }) + + t.test('should work with random promises inbetween', function() { + const assertCtx = createCtx({ a: 1 }) + let checkCounter = 0 + let flaska = new Flaska({}, faker) + let result = flaska.handleMiddleware(assertCtx, [ + function(ctx) { assert.strictEqual(ctx, assertCtx); assert.strictEqual(checkCounter, 0); checkCounter++ }, + function(ctx) { assert.strictEqual(ctx, assertCtx); assert.strictEqual(checkCounter, 1); checkCounter++ }, + function(ctx) { return new Promise(function(res) { assert.strictEqual(ctx, assertCtx); assert.strictEqual(checkCounter, 2); checkCounter++; res() }) }, + function(ctx) { return Promise.resolve().then(function() { assert.strictEqual(ctx, assertCtx); assert.strictEqual(checkCounter, 3); checkCounter++ }) }, + function(ctx) { assert.strictEqual(ctx, assertCtx); assert.strictEqual(checkCounter, 4); checkCounter++ }, + function(ctx) { return Promise.resolve().then(function() { assert.strictEqual(ctx, assertCtx); assert.strictEqual(checkCounter, 5); checkCounter++ }) }, + function(ctx) { return Promise.resolve().then(function() { assert.strictEqual(ctx, assertCtx); assert.strictEqual(checkCounter, 6); checkCounter++ }) }, + function(ctx) { assert.strictEqual(ctx, assertCtx); assert.strictEqual(checkCounter, 7); checkCounter++ }, + function(ctx) { assert.strictEqual(ctx, assertCtx); assert.strictEqual(checkCounter, 8); checkCounter++ }, + ], 0) + assert.ok(result) + assert.strictEqual(typeof(result.then), 'function') + return result.then(function() { + assert.strictEqual(checkCounter, 9) + }) + }) + + t.test('should work with rejected promises inbetween', async function() { + const assertError = { a: 1 } + let checkCounter = 0 + let flaska = new Flaska({}, faker) + let err = await assert.isRejected(flaska.handleMiddleware({}, [ + function() { }, + function() { return new Promise(function(res, rej) { rej(assertError) }) }, + function() { throw new Error('should not be seen') }, + ], 0)) + assert.strictEqual(err, assertError) + err = await assert.isRejected(flaska.handleMiddleware({}, [ + function() { }, + function() { return Promise.reject(assertError) }, + function() { throw new Error('should not be seen') }, + ], 0)) + assert.strictEqual(err, assertError) + err = await assert.isRejected(flaska.handleMiddleware({}, [ + function() { }, + function() { return Promise.resolve() }, + function() { throw assertError }, + ], 0)) + assert.strictEqual(err, assertError) + }) +}) + +t.describe('#listen()', function() { + t.test('it should throw if missing port', function() { + let flaska = new Flaska({}, faker) + assert.throws(function() { flaska.listen() }, /[Pp]ort/) + assert.throws(function() { flaska.listen('asdf') }, /[Pp]ort/) + assert.throws(function() { flaska.listen('123') }, /[Pp]ort/) + assert.throws(function() { flaska.listen([]) }, /[Pp]ort/) + assert.throws(function() { flaska.listen({}) }, /[Pp]ort/) + assert.throws(function() { flaska.listen(function() {}) }, /[Pp]ort/) + }) + + t.test('it should automatically call compile', function() { + let assertCalled = false + let flaska = new Flaska({}, faker) + flaska.compile = function() { assertCalled = true } + flaska.listen(404) + assert.strictEqual(assertCalled, true) + }) + + t.test('call http correctly', function() { + const assertPort = 325897235 + const assertCb = function() { } + let checkPort = null + let checkListenCb = null + + let testFaker = fakeHttp(null, function(port, cb) { + checkPort = port + checkListenCb = cb + }) + let flaska = new Flaska({}, testFaker) + assert.ok(flaska.requestStart) + flaska.requestStart = function() { + checkInternalThis = this + checkIsTrue = true + } + flaska.listen(assertPort, assertCb) + assert.strictEqual(checkPort, assertPort) + assert.strictEqual(checkListenCb, assertCb) + }) + + t.test('register requestStart if no async', function() { + let checkIsTrue = false + let checkInternalThis = null + let checkHandler = null + + let testFaker = fakeHttp(function(cb) { + checkHandler = cb + }) + let flaska = new Flaska({}, testFaker) + assert.ok(flaska.requestStart) + flaska.requestStart = function() { + checkInternalThis = this + checkIsTrue = true + } + flaska.listen(404) + assert.strictEqual(typeof(checkHandler), 'function') + assert.notStrictEqual(checkHandler, flaska.requestStart) + assert.notStrictEqual(checkIsTrue, true) + assert.notStrictEqual(checkInternalThis, flaska) + checkHandler() + assert.strictEqual(checkIsTrue, true) + assert.strictEqual(checkInternalThis, flaska) + }) +}) diff --git a/test/flaska.in.test.mjs b/test/flaska.in.test.mjs new file mode 100644 index 0000000..c275658 --- /dev/null +++ b/test/flaska.in.test.mjs @@ -0,0 +1,495 @@ +import { Eltro as t, assert} from 'eltro' +import { Flaska } from '../flaska.mjs' +import { fakeHttp, createReq, spy, createRes } from './helper.mjs' + +const faker = fakeHttp() + +t.describe('#requestStart()', function() { + t.test('calls on correct events on both req and res', function(cb) { + const assertErrorOne = new Error('Erics Birth') + const assertErrorTwo = new Error('Contactic Eric') + const onReqError = spy() + const onResError = spy() + const onEnded = spy() + const assertReq = createReq({ url: '', a: 1 }) + const assertRes = createRes({ b: 2 }) + let flaska = new Flaska({}, faker) + + flaska.onreqerror(onReqError) + flaska.onreserror(onResError) + flaska.requestEnded = onEnded + + flaska.requestEnd = function(err, ctx) { + try { + assert.ok(err) + assert.strictEqual(assertReq.on.callCount, 3) + assert.strictEqual(assertRes.on.callCount, 1) + + + assert.strictEqual(assertRes.on.firstCall[0], 'error') + assert.strictEqual(typeof(assertRes.on.firstCall[1]), 'function') + assertRes.on.firstCall[1](assertErrorTwo, ctx) + assert.strictEqual(onResError.callCount, 1) + assert.strictEqual(onResError.firstCall[0], assertErrorTwo) + assert.strictEqual(onResError.firstCall[1], ctx) + + assert.strictEqual(assertReq.on.firstCall[0], 'error') + assert.strictEqual(typeof(assertReq.on.firstCall[1]), 'function') + assertReq.on.firstCall[1](assertErrorOne, ctx) + assert.strictEqual(onReqError.callCount, 1) + assert.strictEqual(onReqError.firstCall[0], assertErrorOne) + assert.strictEqual(onReqError.firstCall[1], ctx) + + assert.strictEqual(assertReq.on.secondCall[0], 'aborted') + assert.strictEqual(typeof(assertReq.on.secondCall[1]), 'function') + assert.notStrictEqual(ctx.aborted, false) + assertReq.on.secondCall[1]() + assert.strictEqual(ctx.aborted, true) + + assert.strictEqual(assertReq.on.thirdCall[0], 'close') + assert.strictEqual(typeof(assertReq.on.thirdCall[1]), 'function') + assert.strictEqual(onEnded.called, false) + assertReq.on.thirdCall[1]() + assert.strictEqual(onEnded.called, true) + + // Test abort and close + + cb() + } catch (err) { cb(err) } + } + flaska._beforeCompiled = function(ctx) { + throw new Error() + } + + flaska.requestStart(assertReq, assertRes) + }) + + t.test('calls beforeCompiled correctly', function(cb) { + const assertError = new Error('test') + const assertReq = createReq({ url: '', a: 1 }) + const assertRes = createRes({ b: 2 }) + let flaska = new Flaska({}, faker) + + flaska.requestEnd = function(err, ctx) { + if (err && err !== assertError) return cb(err) + + try { + assert.ok(err) + assert.strictEqual(err, assertError) + assert.deepStrictEqual(ctx.state, {}) + assert.strictEqual(ctx.req, assertReq) + assert.strictEqual(ctx.res, assertRes) + cb() + } catch (err) { cb(err) } + } + flaska._beforeCompiled = function(ctx) { + assert.strictEqual(ctx.req, assertReq) + assert.strictEqual(ctx.res, assertRes) + throw assertError + } + + flaska.requestStart(assertReq, assertRes) + }) + + t.test('calls beforeAsyncCompiled correctly if defined', function(cb) { + const assertError = new Error('test') + const assertReq = createReq({ url: '', a: 1 }) + const assertRes = createRes({ b: 2 }) + let flaska = new Flaska({}, faker) + flaska.compile() + + flaska._beforeAsyncCompiled = function(ctx) { + assert.strictEqual(ctx.req, assertReq) + assert.strictEqual(ctx.res, assertRes) + return Promise.resolve().then(function() { return Promise.reject(assertError) }) + } + + flaska.requestEnd = function(err, ctx) { + if (err && err !== assertError) return cb(err) + + try { + assert.ok(err) + assert.strictEqual(err, assertError) + assert.deepStrictEqual(ctx.state, {}) + assert.strictEqual(ctx.req, assertReq) + assert.strictEqual(ctx.res, assertRes) + cb() + } catch (err) { cb(err) } + } + + flaska.requestStart(assertReq, assertRes) + }) + + t.test('calls correct router with correct url and context', function(cb) { + const assertError = new Error('test') + const assertMethod = 'test' + const assertPath = '/test/me' + const assertSearch = '?asdf=test' + let flaska = new Flaska({}, faker) + flaska.compile() + + flaska.routers.test = { + match: function(path) { + assert.strictEqual(path, assertPath) + throw assertError + } + } + + flaska.requestEnd = function(err, ctx) { + if (err && err !== assertError) return cb(err) + + try { + assert.ok(err) + assert.strictEqual(err, assertError) + assert.strictEqual(ctx.url, assertPath) + assert.strictEqual(ctx.search, assertSearch) + assert.strictEqual(ctx.method, assertMethod) + assert.strictEqual(ctx.status, 200) + assert.strictEqual(ctx.body, null) + assert.strictEqual(ctx.type, null) + assert.strictEqual(ctx.length, null) + cb() + } catch (err) { cb(err) } + } + + flaska.requestStart(createReq({ + url: assertPath + assertSearch, + method: assertMethod, + }), createRes()) + }) + + t.test('calls correct router with correct url and context if beforeAsync', function(cb) { + const assertError = new Error('test') + const assertMethod = 'test' + const assertPath = '/test/me' + const assertSearch = '?asdf=test' + let flaska = new Flaska({}, faker) + flaska.compile() + flaska._beforeAsyncCompiled = function() { return Promise.resolve() } + + flaska.routers.test = { + match: function(path) { + assert.strictEqual(path, assertPath) + throw assertError + } + } + + flaska.requestEnd = function(err, ctx) { + if (err && err !== assertError) return cb(err) + + try { + assert.ok(err) + assert.strictEqual(err, assertError) + assert.strictEqual(ctx.url, assertPath) + assert.strictEqual(ctx.search, assertSearch) + assert.strictEqual(ctx.method, assertMethod) + cb() + } catch (err) { cb(err) } + } + + flaska.requestStart(createReq({ + url: assertPath + assertSearch, + method: assertMethod, + }), createRes()) + }) + + t.test('calls handleMiddleware correctly', function(cb) { + const assertError = new Error('test') + const assertMiddles = [1, 2] + const assertParams = { a: 1, b: 2 } + let checkMiddleCtx = null + + let flaska = new Flaska({}, faker) + flaska.compile() + + flaska.routers.GET.match = function() { + return { + handler: function() {}, + middlewares: assertMiddles, + params: assertParams, + } + } + flaska.handleMiddleware = function(ctx, middles, index) { + assert.strictEqual(index, 0) + assert.strictEqual(middles, assertMiddles) + checkMiddleCtx = ctx + throw assertError + } + + flaska.requestEnd = function(err, ctx) { + if (err && err !== assertError) return cb(err) + + try { + assert.ok(err) + assert.ok(ctx) + assert.strictEqual(err, assertError) + assert.strictEqual(ctx, checkMiddleCtx) + assert.strictEqual(ctx.params, assertParams) + cb() + } catch (err) { cb(err) } + } + + flaska.requestStart(createReq({ + url: '', + method: 'GET', + }), createRes({})) + }) + + t.test('calls route handler correctly', function(cb) { + const assertError = new Error('test') + let checkHandlerCtx = null + let handler = function(ctx) { + assert.strictEqual(ctx.params.id, 'test') + checkHandlerCtx = ctx + throw assertError + } + + let flaska = new Flaska({}, faker) + flaska.get('/:id', handler) + flaska.compile() + + flaska.requestEnd = function(err, ctx) { + if (err && err !== assertError) return cb(err) + + try { + assert.ok(err) + assert.ok(ctx) + assert.strictEqual(err, assertError) + assert.strictEqual(ctx, checkHandlerCtx) + cb() + } catch (err) { cb(err) } + } + + flaska.requestStart(createReq({ + url: '/test', + method: 'GET', + }), createRes()) + }) + + t.test('should work with synchronous handler', function(cb) { + const assertBody = { a: 1 } + let handler = function(ctx) { + assert.strictEqual(ctx.params.path, 'test/something/here') + ctx.body = assertBody + } + + let flaska = new Flaska({}, faker) + flaska.get('/::path', handler) + flaska.compile() + flaska.handleMiddleware = function() { + throw new Error('should not be called') + } + + flaska.requestEnd = function(err, ctx) { + try { + assert.notOk(err) + assert.ok(ctx) + assert.strictEqual(ctx.body, assertBody) + cb() + } catch (err) { cb(err) } + } + + flaska.requestStart(createReq({ + url: '/test/something/here', + method: 'GET', + }), createRes()) + }) + + t.test('calls handleMiddleware correctly if is promise', function(cb) { + const assertError = new Error('test') + const assertMiddles = [1] + + let flaska = new Flaska({}, faker) + flaska.compile() + + flaska.routers.GET.match = function() { + return { + handler: function() {}, + middlewares: assertMiddles, + } + } + flaska.handleMiddleware = function() { + return Promise.resolve().then(function() { return Promise.reject(assertError) }) + } + + flaska.requestEnd = function(err, ctx) { + if (err && err !== assertError) return cb(err) + + try { + assert.ok(err) + assert.ok(ctx) + assert.strictEqual(err, assertError) + cb() + } catch (err) { cb(err) } + } + + flaska.requestStart(createReq({ + url: '', + method: 'GET', + }), createRes()) + }) + + t.test('calls route handler correctly with promise middle', function(cb) { + const assertError = new Error('test') + let checkHandlerCtx = null + let handler = function(ctx) { + assert.strictEqual(ctx.params.id, 'test') + checkHandlerCtx = ctx + throw assertError + } + + let flaska = new Flaska({}, faker) + flaska.get('/:id', [function() { return Promise.resolve() }], handler) + flaska.compile() + + flaska.requestEnd = function(err, ctx) { + if (err && err !== assertError) return cb(err) + + try { + assert.ok(err) + assert.ok(ctx) + assert.strictEqual(err, assertError) + assert.strictEqual(ctx, checkHandlerCtx) + cb() + } catch (err) { cb(err) } + } + + flaska.requestStart(createReq({ + url: '/test', + method: 'GET', + }), createRes()) + }) + + t.test('calls promise route handler correctly with promise middle', function(cb) { + const assertError = new Error('test') + let checkHandlerCtx = null + let handler = function(ctx) { + assert.strictEqual(ctx.params.id, 'test') + checkHandlerCtx = ctx + return Promise.resolve().then(function() { return Promise.reject(assertError) }) + } + + let flaska = new Flaska({}, faker) + flaska.get('/:id', [function() { return Promise.resolve() }], handler) + flaska.compile() + + flaska.requestEnd = function(err, ctx) { + if (err && err !== assertError) return cb(err) + + try { + assert.ok(err) + assert.ok(ctx) + assert.strictEqual(err, assertError) + assert.strictEqual(ctx, checkHandlerCtx) + cb() + } catch (err) { cb(err) } + } + + flaska.requestStart(createReq({ + url: '/test', + method: 'GET', + }), createRes()) + }) + + t.test('should work with promise middle and promise handler correctly', function(cb) { + const assertBody = { a: 1 } + const assertState = { b: 2 } + let middle = function(ctx) { + return Promise.resolve().then(function() { + ctx.state = assertState + }) + } + let handler = function(ctx) { + assert.strictEqual(ctx.params.path, 'test/something/here') + assert.strictEqual(ctx.state, assertState) + + return Promise.resolve().then(function() { + ctx.body = assertBody + }) + } + + let flaska = new Flaska({}, faker) + flaska.get('/::path', [middle], handler) + flaska.compile() + + flaska.requestEnd = function(err, ctx) { + try { + assert.notOk(err) + assert.ok(ctx) + assert.strictEqual(ctx.body, assertBody) + assert.strictEqual(ctx.state, assertState) + cb() + } catch (err) { cb(err) } + } + + flaska.requestStart(createReq({ + url: '/test/something/here', + method: 'GET', + }), createRes()) + }) + + t.test('calls route handler correctly if promise', function(cb) { + const assertError = new Error('test') + let checkHandlerCtx = null + let handler = function(ctx) { + assert.strictEqual(ctx.params.id, 'test') + checkHandlerCtx = ctx + return Promise.resolve().then(function() { + return Promise.reject(assertError) + }) + } + + let flaska = new Flaska({}, faker) + flaska.get('/:id', handler) + flaska.compile() + + flaska.requestEnd = function(err, ctx) { + if (err && err !== assertError) return cb(err) + + try { + assert.ok(err) + assert.ok(ctx) + assert.strictEqual(err, assertError) + assert.strictEqual(ctx, checkHandlerCtx) + cb() + } catch (err) { cb(err) } + } + + flaska.requestStart(createReq({ + url: '/test', + method: 'GET', + }), createRes()) + }) + + t.test('should work with promise handler', function(cb) { + const assertBody = { a: 1 } + let handler = function(ctx) { + return Promise.resolve().then(function() { + assert.strictEqual(ctx.params.path, 'test/something/here') + ctx.body = assertBody + }) + } + + let flaska = new Flaska({}, faker) + flaska.get('/::path', [], handler) + flaska.compile() + + flaska.handleMiddleware = function() { + throw new Error('should not be called') + } + + flaska.requestEnd = function(err, ctx) { + try { + assert.notOk(err) + assert.ok(ctx) + assert.strictEqual(ctx.body, assertBody) + cb() + } catch (err) { cb(err) } + } + + flaska.requestStart(createReq({ + url: '/test/something/here', + method: 'GET', + }), createRes()) + }) +}) diff --git a/test/flaska.out.test.mjs b/test/flaska.out.test.mjs new file mode 100644 index 0000000..96f25c2 --- /dev/null +++ b/test/flaska.out.test.mjs @@ -0,0 +1,249 @@ +import { Eltro as t, assert} from 'eltro' +import { Flaska, FlaskaRouter } from '../flaska.mjs' +import { fakeHttp, createCtx, spy } from './helper.mjs' + +const fakerHttp = fakeHttp() +const fakeStream = { pipeline: spy() } + +t.describe('#requestEnd()', function() { + t.test('calls onerror correctly on error', function(cb) { + const assertError = new Error('test') + const assertBody = { a: 1 } + let onFinish = function() { + try { + assert.strictEqual(ctx.status, 501) + assert.strictEqual(ctx.body, assertBody) + cb() + } catch (err) { cb(err) } + } + const ctx = createCtx({}, onFinish) + + let flaska = new Flaska({}, fakerHttp, fakeStream) + flaska.onerror(function(err, inctx) { + assert.strictEqual(err, assertError) + assert.strictEqual(inctx, ctx) + inctx.status = 501 + inctx.body = assertBody + }) + + flaska.requestEnd(assertError, ctx) + }) + + t.test('call res and end correctly when dealing with objects', function(cb) { + const assertStatus = 202 + // Calculated manually just in case + const assertBodyLength = 7 + const assertBody = { a: 1 } + let onFinish = function(body) { + try { + assert.strictEqual(ctx.status, assertStatus) + assert.strictEqual(ctx.body, assertBody) + assert.strictEqual(ctx.res.statusCode, assertStatus) + assert.strictEqual(ctx.res.setHeader.callCount, 2) + assert.strictEqual(ctx.res.setHeader.firstCall[0], 'Content-Type') + assert.strictEqual(ctx.res.setHeader.firstCall[1], 'application/json; charset=utf-8') + assert.strictEqual(ctx.res.setHeader.secondCall[0], 'Content-Length') + assert.strictEqual(ctx.res.setHeader.secondCall[1], assertBodyLength) + assert.ok(body) + assert.strictEqual(body, '{"a":1}') + cb() + } catch (err) { cb(err) } + } + const ctx = createCtx({ + status: assertStatus, + }, onFinish) + ctx.body = assertBody + + let flaska = new Flaska({}, fakerHttp, fakeStream) + flaska.requestEnd(null, ctx) + }) + + t.test('call res and end correctly when dealing with strings', function(cb) { + const assertStatus = 206 + // Calculated manually just in case + const assertBodyLength = 4 + const assertBody = 'eða' + let onFinish = function(body) { + try { + assert.strictEqual(ctx.status, assertStatus) + assert.strictEqual(ctx.body, assertBody) + assert.strictEqual(ctx.res.statusCode, assertStatus) + assert.strictEqual(ctx.res.setHeader.callCount, 2) + assert.strictEqual(ctx.res.setHeader.firstCall[0], 'Content-Type') + assert.strictEqual(ctx.res.setHeader.firstCall[1], 'text/plain; charset=utf-8') + assert.strictEqual(ctx.res.setHeader.secondCall[0], 'Content-Length') + assert.strictEqual(ctx.res.setHeader.secondCall[1], assertBodyLength) + assert.ok(body) + assert.strictEqual(body, assertBody) + cb() + } catch (err) { cb(err) } + } + const ctx = createCtx({ + status: assertStatus, + }, onFinish) + ctx.body = assertBody + + let flaska = new Flaska({}, fakerHttp, fakeStream) + flaska.requestEnd(null, ctx) + }) + + t.test('call res and end correctly when dealing with numbers', function(cb) { + const assertStatus = 211 + // Calculated manually just in case + const assertBodyLength = 7 + const assertBody = 4214124 + let onFinish = function(body) { + try { + assert.strictEqual(ctx.status, assertStatus) + assert.strictEqual(ctx.body, assertBody) + assert.strictEqual(ctx.res.statusCode, assertStatus) + assert.strictEqual(ctx.res.setHeader.callCount, 2) + assert.strictEqual(ctx.res.setHeader.firstCall[0], 'Content-Type') + assert.strictEqual(ctx.res.setHeader.firstCall[1], 'text/plain; charset=utf-8') + assert.strictEqual(ctx.res.setHeader.secondCall[0], 'Content-Length') + assert.strictEqual(ctx.res.setHeader.secondCall[1], assertBodyLength) + assert.ok(body) + assert.strictEqual(body, assertBody.toString()) + cb() + } catch (err) { cb(err) } + } + const ctx = createCtx({ + status: assertStatus, + }, onFinish) + ctx.body = assertBody + + let flaska = new Flaska({}, fakerHttp, fakeStream) + flaska.requestEnd(null, ctx) + }) + + t.test('call res and end correctly when dealing with custom type', function(cb) { + const assertStatus = 209 + const assertBody = 'test' + const assertType = 'something/else' + let onFinish = function(body) { + try { + assert.strictEqual(ctx.status, assertStatus) + assert.strictEqual(ctx.res.statusCode, assertStatus) + assert.strictEqual(ctx.res.setHeader.firstCall[0], 'Content-Type') + assert.strictEqual(ctx.res.setHeader.firstCall[1], assertType) + assert.strictEqual(body, assertBody) + cb() + } catch (err) { cb(err) } + } + const ctx = createCtx({ + status: assertStatus, + body: assertBody, + }, onFinish) + ctx.type = assertType + + let flaska = new Flaska({}, fakerHttp, fakeStream) + flaska.requestEnd(null, ctx) + }) + + t.test('call pipeline correctly when dealing with pipe', function(cb) { + const assertStatus = 211 + const assertType = 'herp/derp' + const assertBody = { pipe: function() {} } + + let onFinish = function(source, target, callback) { + try { + assert.strictEqual(ctx.status, assertStatus) + assert.strictEqual(ctx.res.statusCode, assertStatus) + assert.strictEqual(ctx.res.setHeader.firstCall[0], 'Content-Type') + assert.strictEqual(ctx.res.setHeader.firstCall[1], assertType) + assert.strictEqual(source, assertBody) + assert.strictEqual(target, ctx.res) + assert.strictEqual(typeof(callback), 'function') + cb() + } catch (err) { cb(err) } + } + const ctx = createCtx({ + status: assertStatus, + }) + fakeStream.pipeline = onFinish + + ctx.body = assertBody + ctx.type = assertType + let flaska = new Flaska({}, fakerHttp, fakeStream) + flaska.requestEnd(null, ctx) + }) + + t.test('call pipe should have default type', function(cb) { + let onFinish = function(source, target) { + try { + assert.strictEqual(ctx.res.statusCode, 200) + assert.strictEqual(ctx.res.setHeader.firstCall[0], 'Content-Type') + assert.strictEqual(ctx.res.setHeader.firstCall[1], 'application/octet-stream') + assert.strictEqual(source, ctx.body) + assert.strictEqual(target, ctx.res) + cb() + } catch (err) { cb(err) } + } + const ctx = createCtx({}) + ctx.body = { pipe: function() {} } + fakeStream.pipeline = onFinish + + let flaska = new Flaska({}, fakerHttp, fakeStream) + flaska.requestEnd(null, ctx) + }) + + const emptyStatuses = [204, 205, 304] + + emptyStatuses.forEach(function(status) { + t.test('call res and end correctly when dealing with status ' + status, function(cb) { + const assertStatus = status + const assertNotBody = 'test' + const assertNotType = 'something/else' + let onFinish = function(body) { + try { + assert.strictEqual(ctx.status, assertStatus) + assert.strictEqual(ctx.res.statusCode, assertStatus) + assert.strictEqual(ctx.res.setHeader.callCount, 0) + assert.notOk(body) + cb() + } catch (err) { cb(err) } + } + const ctx = createCtx({ + status: assertStatus, + body: assertNotBody, + }, onFinish) + ctx.type = assertNotType + + let flaska = new Flaska({}, fakerHttp, fakeStream) + flaska.requestEnd(null, ctx) + }) + }) +}) + +t.describe('#requestEnded()', function() { + t.test('calls afterCompiled correctly', function() { + const assertError = new Error('test') + const assertCtx = createCtx() + let flaska = new Flaska({}, fakerHttp) + + flaska._afterCompiled = function(ctx) { + assert.strictEqual(ctx, assertCtx) + throw assertError + } + + assert.throws(function() { + flaska.requestEnded(assertCtx) + }, /test/) + }) + + t.test('calls afterAsyncCompiled correctly if defined', async function() { + const assertError = new Error('test') + const assertCtx = createCtx() + let flaska = new Flaska({}, fakerHttp) + flaska.compile() + + flaska._afterAsyncCompiled = function(ctx) { + assert.strictEqual(ctx, assertCtx) + return Promise.resolve().then(function() { return Promise.reject(assertError) }) + } + + let err = await assert.isRejected(flaska.requestEnded(assertCtx)) + + assert.strictEqual(err, assertError) + }) +}) diff --git a/test/flaska.test.mjs b/test/flaska.test.mjs deleted file mode 100644 index cfd9ee0..0000000 --- a/test/flaska.test.mjs +++ /dev/null @@ -1,5 +0,0 @@ -import { Eltro as t, assert} from 'eltro' -import { Flaska } from '../flaska.mjs' - -t.describe('Flaska', function() { -}) diff --git a/test/helper.mjs b/test/helper.mjs new file mode 100644 index 0000000..55c8fc1 --- /dev/null +++ b/test/helper.mjs @@ -0,0 +1,118 @@ +const indexMap = [ + 'firstCall', + 'secondCall', + 'thirdCall', +] + +export function spy() { + let calls = [] + let called = 0 + let func = function(...args) { + func.called = true + calls.push(args) + if (called < indexMap.length) { + func[indexMap[called]] = args + } + called++ + func.callCount = called + } + func.called = false + func.callCount = called + func.onCall = function(i) { + return calls[i] + } + for (let i = 0; i < indexMap.length; i++) { + func[indexMap] = null + } + return func +} + +export function fakeHttp(inj1, inj2) { + let intermediate = { + createServer: function(cb) { + if (inj1) inj1(cb) + intermediate.fakeRequest = cb + return { + listen: function(port, cb) { + if (inj2) inj2(port, cb) + else if (cb) cb() + } + } + } + } + return intermediate +} + +export function createReq(def) { + return defaults(def, { + on: spy(), + }) +} + +export function createRes(def) { + return defaults(def, { + statusCode: 0, + end: spy(), + setHeader: spy(), + write: spy(), + on: spy(), + writeHead: spy(), + pipe: spy(), + }) +} + +export function createCtx(def, endHandler) { + return defaults(def, { + req: createReq(), + res: createRes({ end: endHandler || spy() }), + finished: false, + method: 'GET', + url: '/test', + search: '', + state: {}, + status: 200, + body: null, + type: null, + length: null, + log: { + error: spy(), + info: spy(), + warn: spy(), + debug: spy(), + } + }) +} + +// taken from isobject npm library +function isObject(val) { + return val != null && typeof val === 'object' && Array.isArray(val) === false +} + +export function defaults(options, def) { + let out = { } + + if (options) { + Object.keys(options || {}).forEach(key => { + out[key] = options[key] + + if (Array.isArray(out[key])) { + out[key] = out[key].map(item => { + if (isObject(item)) return defaults(item) + return item + }) + } else if (out[key] && typeof out[key] === 'object') { + out[key] = defaults(options[key], def && def[key]) + } + }) + } + + if (def) { + Object.keys(def).forEach(function(key) { + if (typeof out[key] === 'undefined') { + out[key] = def[key] + } + }) + } + + return out +} diff --git a/test/http.test.mjs b/test/http.test.mjs new file mode 100644 index 0000000..abb30e6 --- /dev/null +++ b/test/http.test.mjs @@ -0,0 +1,29 @@ +import { Eltro as t, assert} from 'eltro' +import { Flaska } from '../flaska.mjs' +import Client from './client.mjs' + +const port = 51024 +const flaska = new Flaska({}) +const client = new Client(port) + +flaska.get('/', function(ctx) { + ctx.body = { status: true } +}) + +t.before(function(cb) { + flaska.listen(port, cb) +}) + +t.describe('', function() { + t.test('/ should return status true', function() { + return client.get().then(function(body) { + assert.deepEqual(body, { status: true }) + }) + }) +}) + +t.after(function(cb) { + setTimeout(function() { + flaska.server.close(cb) + }, 1000) +}) diff --git a/test/router.test.mjs b/test/router.test.mjs index 2a4fab5..a4ea4d0 100644 --- a/test/router.test.mjs +++ b/test/router.test.mjs @@ -199,6 +199,50 @@ t.describe('#match()', function() { assert.strictEqual(assertMatched, true) assert.strictEqual(result.params.id, assertParameter) }) + + t.test('match full path variable paths', function() { + const assertParameter = 'bla/bla/bla' + let assertMatched = false + let router = new FlaskaRouter() + router.addRoute('/test/::id', function() { assertMatched = true }) + let result = router.match('/test/' + assertParameter) + assert.ok(result.handler) + assert.ok(result.middlewares) + assert.strictEqual(result.middlewares.length, 0) + result.handler() + assert.strictEqual(assertMatched, true) + assert.strictEqual(result.params.id, assertParameter) + + // Test with extra slash at the end + assertMatched = false + result = router.match('/test/' + assertParameter + '/') + assert.ok(result.handler) + assert.ok(result.middlewares) + assert.strictEqual(result.middlewares.length, 0) + result.handler() + assert.strictEqual(assertMatched, true) + assert.strictEqual(result.params.id, assertParameter) + }) + + t.test('match full path root path properly', function() { + const assertParamFunc = function() { } + const assertFullFunc = function() { } + let router = new FlaskaRouter() + router.addRoute('/test/:bla', assertParamFunc) + router.addRoute('/::id', assertFullFunc) + let result = router.match('/test/123') + assert.strictEqual(result.handler, assertParamFunc) + assert.ok(result.middlewares) + assert.strictEqual(result.middlewares.length, 0) + assert.strictEqual(result.params.bla, '123') + + result = router.match('/test/123/asdf') + assert.strictEqual(result.handler, assertFullFunc) + assert.ok(result.middlewares) + assert.strictEqual(result.middlewares.length, 0) + assert.strictEqual(result.params.id, 'test/123/asdf') + assert.notOk(result.params.bla) + }) t.test('match paths properly', function() { let assertMatched = true @@ -223,6 +267,36 @@ t.describe('#match()', function() { assert.strictEqual(result.params.id, 'asdf') }) + t.test('more comprehensive testing', function() { + const assertFunction = function() { } + const assertRootFunction = function() { } + const assertFailFunction = function() { } + let router = new FlaskaRouter() + router.addRoute('/test/:id', assertFunction) + router.addRoute('/test/test/::path', assertFunction) + router.addRoute('/foo/::path', assertFunction) + router.addRoute('/::path', assertFailFunction) + + assert.strictEqual(router.match('/test/123').handler, assertFunction) + assert.strictEqual(router.match('/test/asdfasdg').handler, assertFunction) + assert.strictEqual(router.match('/test/test/sdafsda').handler, assertFunction) + assert.strictEqual(router.match('/test/test/sdafsda/gdfshe4/43y34/wtaw').handler, assertFunction) + assert.strictEqual(router.match('/foo/123').handler, assertFunction) + assert.strictEqual(router.match('/foo/bar/baz/test').handler, assertFunction) + assert.ok(router.match('/test/123/yweherher/reher/h34h34/')) + assert.strictEqual(router.match('/test/123/yweherher/reher/h34h34/').handler, assertFailFunction) + assert.ok(router.match('/test/foo/bar')) + assert.strictEqual(router.match('/test/foo/bar').handler, assertFailFunction) + assert.ok(router.match('/')) + assert.strictEqual(router.match('/').handler, assertFailFunction) + assert.ok(router.match('/something/else/goes/here')) + assert.strictEqual(router.match('/something/else/goes/here').handler, assertFailFunction) + + router.addRoute('/', assertRootFunction) + assert.ok(router.match('/')) + assert.strictEqual(router.match('/').handler, assertRootFunction) + }) + t.test('return null when no match is found', function() { let router = new FlaskaRouter() router.addRoute('/test/:id', function() { })