From ccaae1614c063e6def184948e852e510aea372ed Mon Sep 17 00:00:00 2001 From: Lloyd Brookes Date: Tue, 24 Nov 2015 11:46:47 +0000 Subject: [PATCH] mocks v2 --- bin/cli.js | 31 ++++++------------ example/mock/.local-web-server.json | 44 +++++++++++++++++++++++--- example/mock/mocks/five.js | 8 +++++ example/mock/mocks/four.js | 5 +++ example/mock/mocks/search.mock.js | 20 ------------ example/mock/mocks/tree.mock.js | 25 --------------- example/mock/mocks/trees.json | 7 ----- example/mock/mocks/trees.mock.js | 34 -------------------- lib/local-web-server.js | 21 +++++++++++-- lib/middleware.js | 63 +++++++++++++++++++------------------ 10 files changed, 114 insertions(+), 144 deletions(-) create mode 100644 example/mock/mocks/five.js create mode 100644 example/mock/mocks/four.js delete mode 100644 example/mock/mocks/search.mock.js delete mode 100644 example/mock/mocks/tree.mock.js delete mode 100644 example/mock/mocks/trees.json delete mode 100644 example/mock/mocks/trees.mock.js diff --git a/bin/cli.js b/bin/cli.js index 1716c69..dff27aa 100755 --- a/bin/cli.js +++ b/bin/cli.js @@ -8,22 +8,15 @@ const loadConfig = require('config-master') const path = require('path') const s = require('string-tools') const os = require('os') +const arrayify = require('array-back') const cli = commandLineArgs(cliOptions.definitions) const usage = cli.getUsage(cliOptions.usageData) const stored = loadConfig('local-web-server') const options = collectOptions() -// TODO summary line on server launch - -if (options.misc.help) { - console.log(usage) - process.exit(0) -} -if (options.misc.config) { - console.log(JSON.stringify(options.server, null, ' ')) - process.exit(0) -} +if (options.misc.help) stop(usage, 0) +if (options.misc.config) stop(JSON.stringify(options.server, null, ' '), 0) const app = localWebServer({ static: { @@ -48,19 +41,15 @@ const app = localWebServer({ spa: options.server.spa, 'no-cache': options.server['no-cache'], rewrite: options.server.rewrite, - verbose: options.server.verbose + verbose: options.server.verbose, + mocks: options.server.mocks }) -app - .on('verbose', (category, message) => { - console.error(ansi.format(s.padRight(category, 14), 'bold'), message) - }) - .listen(options.server.port, onServerUp) +app.listen(options.server.port, onServerUp) -function halt (err) { - console.log(ansi.format(`Error: ${err.message}`, 'red')) - console.log(usage) - process.exit(1) +function stop (msgs, exitCode) { + arrayify(msgs).forEach(msg => console.error(ansi.format(msg))) + process.exit(exitCode) } function onServerUp () { @@ -85,7 +74,7 @@ function collectOptions () { try { options = cli.parse() } catch (err) { - halt(err) + stop([ `[red]{Error}: ${err.message}`, usage ], 1) } const builtIn = { diff --git a/example/mock/.local-web-server.json b/example/mock/.local-web-server.json index 4259315..5b49c4f 100644 --- a/example/mock/.local-web-server.json +++ b/example/mock/.local-web-server.json @@ -1,7 +1,43 @@ { - "rewrite": [ - { "from": "/trees", "to": "/mocks/trees.mock.js" }, - { "from": "/trees/:id", "to": "/mocks/tree.mock.js" }, - { "from": "/search", "to": "/mocks/search.mock.js" } + "mocks": [ + { + "route": "/one", + "response": { + "body": { "id": 1, "name": "whatever" } + } + }, + { + "route": "/two", + "request": { "accepts": "xml" }, + "response": { + "body": "" + } + }, + { + "route": "/three", + "targets": [ + { + "request": { "method": "GET" }, + "response": { + "body": { "id": 1, "name": "whatever" } + } + }, + { + "request": { "method": "POST" }, + "response": { + "status": 400, + "body": { "message": "That method is not allowed." } + } + } + ] + }, + { + "route": "/four", + "module": "/mocks/four.js" + }, + { + "route": "/five/:id\\?name=:name", + "module": "/mocks/five.js" + } ] } diff --git a/example/mock/mocks/five.js b/example/mock/mocks/five.js new file mode 100644 index 0000000..168708d --- /dev/null +++ b/example/mock/mocks/five.js @@ -0,0 +1,8 @@ +module.exports = { + response: function (ctx, id, name) { + this.body = { + id: id, + name: name + } + } +} diff --git a/example/mock/mocks/four.js b/example/mock/mocks/four.js new file mode 100644 index 0000000..4beeb30 --- /dev/null +++ b/example/mock/mocks/four.js @@ -0,0 +1,5 @@ +module.exports = { + response: { + body: { id: 2, name: 'eucalyptus', maxHeight: 210 } + } +} diff --git a/example/mock/mocks/search.mock.js b/example/mock/mocks/search.mock.js deleted file mode 100644 index 2fc9a72..0000000 --- a/example/mock/mocks/search.mock.js +++ /dev/null @@ -1,20 +0,0 @@ -module.exports = [ - { - request: { - accepts: 'json' - }, - response: { - status: 200, - body: { id: 2, name: 'eucalyptus', maxHeight: 210 } - } - }, - { - request: { - accepts: 'xml' - }, - response: { - status: 200, - body: '' - } - } -] diff --git a/example/mock/mocks/tree.mock.js b/example/mock/mocks/tree.mock.js deleted file mode 100644 index f8ac1e2..0000000 --- a/example/mock/mocks/tree.mock.js +++ /dev/null @@ -1,25 +0,0 @@ -module.exports = [ - /* CREATE (409 CONFLICT - should be called on the collection) */ - { - request: { method: 'POST' }, - response: { status: 409 } - }, - /* READ */ - { - request: { method: 'GET' }, - response: { - status: 200, - body: { id: 2, name: 'eucalyptus', maxHeight: 210 } - } - }, - /* UPDATE */ - { - request: { method: 'PUT' }, - response: { status: 204 } - }, - /* DELETE */ - { - request: { method: 'DELETE' }, - response: { status: 204 } - } -] diff --git a/example/mock/mocks/trees.json b/example/mock/mocks/trees.json deleted file mode 100644 index 5d27582..0000000 --- a/example/mock/mocks/trees.json +++ /dev/null @@ -1,7 +0,0 @@ -[ - { "id": 1, "name": "Conifer", "maxHeight": 115 }, - { "id": 2, "name": "Eucalyptus", "maxHeight": 210 }, - { "id": 3, "name": "Ash", "maxHeight": 40 }, - { "id": 4, "name": "Elder", "maxHeight": 5 }, - { "id": 5, "name": "Holly", "maxHeight": 10 } -] diff --git a/example/mock/mocks/trees.mock.js b/example/mock/mocks/trees.mock.js deleted file mode 100644 index 70a1f98..0000000 --- a/example/mock/mocks/trees.mock.js +++ /dev/null @@ -1,34 +0,0 @@ -const data = require('./trees') - -module.exports = [ - /* CREATE */ - { - request: { method: 'POST' }, - response: function (ctx) { - data.push(ctx.request.body) - this.status = 201 - this.location = '/tree/1' - } - }, - /* READ */ - { - request: { method: 'GET' }, - response: function (ctx) { - this.status = 200 - this.body = data.filter(tree => tree.maxHeight > Number(ctx.query.tallerThan || 0)) - } - }, - /* UPDATE (forbidden on collection)*/ - { - request: { method: 'PUT' }, - response: { status: 404 } - }, - /* DELETE (forbidden on collection) */ - { - request: { method: 'DELETE' }, - response: { status: 404 } - } -] - -// curl -i http://localhost:8000/trees -H 'content-type: application/json' -d '{"id":6, "name":"Oak", "maxHeight": 100 }' -// curl -i http://localhost:8000/trees diff --git a/lib/local-web-server.js b/lib/local-web-server.js index 6f45413..7176aee 100644 --- a/lib/local-web-server.js +++ b/lib/local-web-server.js @@ -44,7 +44,8 @@ function localWebServer (options) { mime: {}, forbid: [], rewrite: [], - verbose: false + verbose: false, + mocks: [] }, options) if (options.verbose) { @@ -106,7 +107,7 @@ function localWebServer (options) { app.use(mw.blacklist(options.forbid)) } - /* Cache */ + /* cache */ if (!options['no-cache']) { const conditional = require('koa-conditional-get') const etag = require('koa-etag') @@ -144,7 +145,21 @@ function localWebServer (options) { } /* Mock Responses */ - app.use(mw.mockResponses({ root: options.static.root })) + options.mocks.forEach(mock => { + if (mock.module) { + mock.targets = require(path.join(options.static.root, mock.module)) + } + + if (mock.targets) { + app.use(mw.mockResponses(mock.route, mock.targets)) + } else if (mock.response) { + mock.target = { + request: mock.request, + response: mock.response + } + app.use(mw.mockResponses(mock.route, mock.target)) + } + }) /* serve static files */ if (options.static.root) { diff --git a/lib/middleware.js b/lib/middleware.js index 357b332..44eadbc 100644 --- a/lib/middleware.js +++ b/lib/middleware.js @@ -3,6 +3,7 @@ const path = require('path') const http = require('http') const url = require('url') const arrayify = require('array-back') +const t = require('typical') const pathToRegexp = require('path-to-regexp') const debug = require('debug')('local-web-server') @@ -63,17 +64,30 @@ function blacklist (forbid) { } } -function mockResponses (options) { - options = options || { root: process.cwd() } - return function mockResponses (ctx, next) { - if (/\.mock.js$/.test(ctx.path)) { - const mocks = arrayify(require(path.join(options.root, ctx.path))) +function mime (mimeTypes) { + return function mime (ctx, next) { + return next().then(() => { + const reqPathExtension = path.extname(ctx.path).slice(1) + Object.keys(mimeTypes).forEach(mimeType => { + const extsToOverride = mimeTypes[mimeType] + if (extsToOverride.indexOf(reqPathExtension) > -1) ctx.type = mimeType + }) + }) + } +} + +function mockResponses (route, targets) { + targets = arrayify(targets) + debug('mock route: %s, targets: %j', route, targets); + const pathRe = pathToRegexp(route) + + return function mockResponse (ctx, next) { + if (pathRe.test(ctx.url)) { const testValue = require('test-value') - const t = require('typical') /* find a mock with compatible method and accepts */ - let mock = mocks.find(mock => { - return testValue(mock, { + let target = targets.find(target => { + return testValue(target, { request: { method: [ ctx.method, undefined ], accepts: type => ctx.accepts(type) @@ -81,33 +95,22 @@ function mockResponses (options) { }) }) - /* else take the first mock without a request (no request means 'all requests') */ - if (!mock) { - mock = mocks.find(mock => !mock.request) + /* else take the first target without a request (no request means 'all requests') */ + if (!target) { + target = targets.find(target => !target.request) } - if (mock) { - let mockedResponse = mock.response - if (t.isFunction(mock.response)) { - mockedResponse = new mock.response(ctx) + if (target) { + let mockedResponse = target.response + if (t.isFunction(target.response)) { + const pathMatches = ctx.url.match(pathRe).slice(1) + const ctor = target.response.bind(null, ...[ctx].concat(pathMatches)) + mockedResponse = new ctor() } - debug('mock response: %j', mockedResponse) + debug('target response: %j', mockedResponse) Object.assign(ctx.response, mockedResponse) } - } else { - return next() } - } -} - -function mime (mimeTypes) { - return function mime (ctx, next) { - return next().then(() => { - const reqPathExtension = path.extname(ctx.path).slice(1) - Object.keys(mimeTypes).forEach(mimeType => { - const extsToOverride = mimeTypes[mimeType] - if (extsToOverride.indexOf(reqPathExtension) > -1) ctx.type = mimeType - }) - }) + return next() } }