Browse Source

mocks v2

master
Lloyd Brookes 9 years ago
parent
commit
ccaae1614c
  1. 31
      bin/cli.js
  2. 44
      example/mock/.local-web-server.json
  3. 8
      example/mock/mocks/five.js
  4. 5
      example/mock/mocks/four.js
  5. 20
      example/mock/mocks/search.mock.js
  6. 25
      example/mock/mocks/tree.mock.js
  7. 7
      example/mock/mocks/trees.json
  8. 34
      example/mock/mocks/trees.mock.js
  9. 21
      lib/local-web-server.js
  10. 63
      lib/middleware.js

31
bin/cli.js

@ -8,22 +8,15 @@ const loadConfig = require('config-master')
const path = require('path') const path = require('path')
const s = require('string-tools') const s = require('string-tools')
const os = require('os') const os = require('os')
const arrayify = require('array-back')
const cli = commandLineArgs(cliOptions.definitions) const cli = commandLineArgs(cliOptions.definitions)
const usage = cli.getUsage(cliOptions.usageData) const usage = cli.getUsage(cliOptions.usageData)
const stored = loadConfig('local-web-server') const stored = loadConfig('local-web-server')
const options = collectOptions() 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({ const app = localWebServer({
static: { static: {
@ -48,19 +41,15 @@ const app = localWebServer({
spa: options.server.spa, spa: options.server.spa,
'no-cache': options.server['no-cache'], 'no-cache': options.server['no-cache'],
rewrite: options.server.rewrite, 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 () { function onServerUp () {
@ -85,7 +74,7 @@ function collectOptions () {
try { try {
options = cli.parse() options = cli.parse()
} catch (err) { } catch (err) {
halt(err)
stop([ `[red]{Error}: ${err.message}`, usage ], 1)
} }
const builtIn = { const builtIn = {

44
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": "<result id='2' name='whatever' />"
}
},
{
"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"
}
] ]
} }

8
example/mock/mocks/five.js

@ -0,0 +1,8 @@
module.exports = {
response: function (ctx, id, name) {
this.body = {
id: id,
name: name
}
}
}

5
example/mock/mocks/four.js

@ -0,0 +1,5 @@
module.exports = {
response: {
body: { id: 2, name: 'eucalyptus', maxHeight: 210 }
}
}

20
example/mock/mocks/search.mock.js

@ -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: '<tree id="2" name="eucalyptus" maxHeight="210"/>'
}
}
]

25
example/mock/mocks/tree.mock.js

@ -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 }
}
]

7
example/mock/mocks/trees.json

@ -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 }
]

34
example/mock/mocks/trees.mock.js

@ -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

21
lib/local-web-server.js

@ -44,7 +44,8 @@ function localWebServer (options) {
mime: {}, mime: {},
forbid: [], forbid: [],
rewrite: [], rewrite: [],
verbose: false
verbose: false,
mocks: []
}, options) }, options)
if (options.verbose) { if (options.verbose) {
@ -106,7 +107,7 @@ function localWebServer (options) {
app.use(mw.blacklist(options.forbid)) app.use(mw.blacklist(options.forbid))
} }
/* Cache */
/* cache */
if (!options['no-cache']) { if (!options['no-cache']) {
const conditional = require('koa-conditional-get') const conditional = require('koa-conditional-get')
const etag = require('koa-etag') const etag = require('koa-etag')
@ -144,7 +145,21 @@ function localWebServer (options) {
} }
/* Mock Responses */ /* 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 */ /* serve static files */
if (options.static.root) { if (options.static.root) {

63
lib/middleware.js

@ -3,6 +3,7 @@ const path = require('path')
const http = require('http') const http = require('http')
const url = require('url') const url = require('url')
const arrayify = require('array-back') const arrayify = require('array-back')
const t = require('typical')
const pathToRegexp = require('path-to-regexp') const pathToRegexp = require('path-to-regexp')
const debug = require('debug')('local-web-server') 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 testValue = require('test-value')
const t = require('typical')
/* find a mock with compatible method and accepts */ /* 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: { request: {
method: [ ctx.method, undefined ], method: [ ctx.method, undefined ],
accepts: type => ctx.accepts(type) 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) 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()
} }
} }
Loading…
Cancel
Save