diff --git a/packages/skyring/conf/index.js b/packages/skyring/conf/index.js index ee59b803..1672f2ea 100644 --- a/packages/skyring/conf/index.js +++ b/packages/skyring/conf/index.js @@ -9,11 +9,12 @@ module.exports = conf.defaults({ backend: "memdown" , path: null } +, autobalance: false , channel: { host: "127.0.0.1" , port: 3455 } -, PORT: 3000 +, port: 3000 , nats: { hosts: "127.0.0.1:4222" } diff --git a/packages/skyring/index.js b/packages/skyring/index.js index 8f2f8730..8708b639 100644 --- a/packages/skyring/index.js +++ b/packages/skyring/index.js @@ -26,13 +26,13 @@ if( require.main === module ){ const server = new Server(); - server.listen(conf.get('PORT'), (err) => { + server.listen(conf.get('port'), (err) => { if(err) { process.exitCode = 1; console.error(err); throw err; } - debug('server listening', conf.get('PORT')); + debug('server listening', conf.get('port')); }); function onSignal() { diff --git a/packages/skyring/lib/nats.js b/packages/skyring/lib/nats.js index 2451e5d0..8bb04e68 100644 --- a/packages/skyring/lib/nats.js +++ b/packages/skyring/lib/nats.js @@ -45,7 +45,7 @@ Object.defineProperty(exports, 'client', { function createClient(options) { const hosts = (options && options.hosts) || nats_hosts; const servers = Array.isArray(hosts) ? hosts : parse(hosts); - const opts = Object.assign({}, options, {servers}); + const opts = Object.assign({json: true}, options, {servers}); debug('creating nats client', opts); const client = nats.connect(opts); client.on('error', (err) => { diff --git a/packages/skyring/lib/server/api/middleware/proxy.js b/packages/skyring/lib/server/api/middleware/proxy.js index 88c0d4f6..f0650008 100644 --- a/packages/skyring/lib/server/api/middleware/proxy.js +++ b/packages/skyring/lib/server/api/middleware/proxy.js @@ -1,5 +1,5 @@ 'use strict'; -/*jshint laxcomma: true, smarttabs: true, esnext: true, node: true*/ + /** * Middleware that determines if the current server should handle the request, and will proxy * it to the appropriate node if it isn't @@ -13,10 +13,9 @@ */ const uuid = require('uuid') - , body = require('body') - , debug = require('debug')('skyring:proxy') - , json = require('../../../json') - ; +const body = require('body') +const debug = require('debug')('skyring:proxy') +const json = require('../../../json') /** * @function @@ -27,21 +26,24 @@ const uuid = require('uuid') * @param {Function} next **/ module.exports = function proxy(req, res, node, cb) { - const timer_id = req.$.headers['x-timer-id'] || uuid.v4(); - req.headers['x-timer-id'] = timer_id; - res.setHeader('location', `/timer/${timer_id}`); - const do_handle = node.handleOrProxy(timer_id, req, res); - if (!do_handle) return debug('proxing', timer_id); + const timer_id = req.$.headers['x-timer-id'] || uuid.v4() + req.headers['x-timer-id'] = timer_id + res.setHeader('location', `/timer/${timer_id}`) + const do_handle = node.handleOrProxy(timer_id, req, res) + if (!do_handle) return debug('proxing', timer_id) debug('handle request') body(req, res, (err, data) => { - if (err) return cb(err); + if (err) return cb(err) - const {error, value} = json.parse(data); + const {error, value} = json.parse(data) - if (error) return cb(error); + if (error) { + error.statusCode = 400 + return cb(error) + } - req.$.body = value; - cb(); - }); -}; + req.$.body = value + cb() + }) +} diff --git a/packages/skyring/lib/server/api/middleware/validate.js b/packages/skyring/lib/server/api/middleware/validate.js index e83df487..f01caa8c 100644 --- a/packages/skyring/lib/server/api/middleware/validate.js +++ b/packages/skyring/lib/server/api/middleware/validate.js @@ -3,5 +3,5 @@ const validate = require('../validators/timer'); module.exports = function validatePayload( req, res, node, next ) { - validate(req.$.body || undefined, next); + validate(req.$.body, next); }; diff --git a/packages/skyring/lib/server/api/validators/timer.js b/packages/skyring/lib/server/api/validators/timer.js index 14ab5f49..71e4ac60 100644 --- a/packages/skyring/lib/server/api/validators/timer.js +++ b/packages/skyring/lib/server/api/validators/timer.js @@ -1,61 +1,61 @@ -'use strict'; +'use strict' -const type_exp = /^\[object (.*)\]$/; -const transports = require('../../../transports'); +const type_exp = /^\[object (.*)\]$/ +const transports = require('../../../transports') function typeOf(obj) { - if (obj === null) return 'Null'; - if (obj === undefined) return 'Undefined'; - return type_exp.exec( Object.prototype.toString.call(obj) )[1]; + if (obj === null) return 'Null' + if (obj === undefined) return 'Undefined' + return type_exp.exec(Object.prototype.toString.call(obj))[1] } // 2^32 - 1 -const MAX_TIMEOUT_VALUE = 2147483647; +const MAX_TIMEOUT_VALUE = 2147483647 module.exports = function(data = {}, cb) { if (isNaN(data.timeout) || data.timeout < 1) { - const err = new TypeError('timeout is required and must be a positive number'); - err.statusCode = 400; - return setImmediate(cb, err); + const err = new TypeError('timeout is required and must be a positive number') + err.statusCode = 400 + return setImmediate(cb, err) } if (data.timeout > MAX_TIMEOUT_VALUE) { - const err = new TypeError(`timeout must be less than or equal to 2147483647 milliseconds`); - err.statusCode = 400; - return setImmediate(cb, err); + const err = new TypeError(`timeout must be less than or equal to 2147483647 milliseconds`) + err.statusCode = 400 + return setImmediate(cb, err) } - if (data.data) { - const type = typeOf(data.data); + const type = typeOf(data.data) + if (data.data != null) { if (type !== 'String' && type !== 'Object') { - const err = new TypeError('data is required and must be a string or object'); - err.statusCode = 400; - return setImmediate(cb, err); + const err = new TypeError(`data is required and must be a string or object. Got ${type}`) + err.statusCode = 400 + return setImmediate(cb, err) } } if (typeOf(data.callback) !== 'Object') { - const err = new TypeError('callback is required and must be an object'); - err.statusCode = 400; - return setImmediate(cb, err); + const err = new TypeError('callback is required and must be an object') + err.statusCode = 400 + return setImmediate(cb, err) } if (typeOf(data.callback.transport) !== 'String') { - const err = new TypeError('callback.transport is required and must be a string'); - err.statusCode = 400; - return setImmediate(cb, err); + const err = new TypeError(`callback.transport is required and must be a string. Got ${type}`) + err.statusCode = 400 + return setImmediate(cb, err) } if (typeOf(data.callback.uri) !== 'String') { - const err = new TypeError('callback.uri is required and must be a string'); - err.statusCode = 400; - return setImmediate(cb, err); + const err = new TypeError(`callback.uri is required and must be a string. Got ${type}`) + err.statusCode = 400 + return setImmediate(cb, err) } if (typeOf(data.callback.method) !== 'String') { - const err = new TypeError('callback.method is required and must be a string'); - err.statusCode = 400; - return setImmediate(cb, err); + const err = new TypeError(`callback.method is required and must be a string. Got ${type}`) + err.statusCode = 400 + return setImmediate(cb, err) } - setImmediate(cb, null, data); -}; + setImmediate(cb, null, data) +} diff --git a/packages/skyring/lib/server/index.js b/packages/skyring/lib/server/index.js index e90b58b3..b803361a 100644 --- a/packages/skyring/lib/server/index.js +++ b/packages/skyring/lib/server/index.js @@ -1,5 +1,5 @@ /*jshint laxcomma: true, smarttabs: true, node: true, esnext: true*/ -'use strict'; +'use strict' /** * Primary server instance for a skyring app. * @module skyring/lib/server @@ -11,15 +11,17 @@ * @requires skyring/lib/timer */ -const http = require('http') -const mock = require('@esatterwhite/micromock') -const util = require('util') -const Debug = require('debug') -const routes = require('./api') -const Node = require('./node') -const Router = require('./router') -const Timer = require('../timer') -const debug = Debug('skyring:server') +const {isFunction} = require('util') +const http = require('http') +const mock = require('@esatterwhite/micromock') +const util = require('util') +const Debug = require('debug') +const routes = require('./api') +const Node = require('./node') +const Router = require('./router') +const Timer = require('../timer') +const conf = require('../../conf') +const debug = Debug('skyring:server') /** * @constructor @@ -61,15 +63,16 @@ server.listen(5000) class Server extends http.Server { constructor(opts = {}){ super((req, res) => { - this._router.handle(req, res); - }); - this.closed = false; + this._router.handle(req, res) + }) + this.closed = false this.options = Object.assign({}, { seeds: null , nats: null , storage: null , transports: [] - }, opts); + , autobalance: conf.get('autobalance') + }, opts) if( opts.node ){ this._node = opts.node instanceof Node ? opts.node @@ -78,14 +81,20 @@ class Server extends http.Server { opts.node.port, opts.node.name, opts.node.app - ); + ) } else { - this._node = new Node(); + this._node = new Node() } - this._group = this._node.name; + this._group = this._node.name this._node.on('bootstrap', (seeds) => { - this.emit('bootstrap', seeds); - }); + this.emit('bootstrap', seeds) + }) + } + + route(opts) { + const route = this._router.route(opts.path, opts.method, opts.handler) + item.middleware && route.before( item.middleware ) + debug('loaded: %s %s', item.method, item.path) } /** @@ -99,61 +108,72 @@ class Server extends http.Server { **/ listen(port, ...args) { const callback = args[args.length - 1] - debug('seed nodes', this.options.seeds); + if (this.listening) return isFunction(callback) ? callback() : null + + debug('seed nodes', this.options.seeds) this._timers = new Timer({ nats: this.options.nats , storage: this.options.storage , transports: this.options.transports }, (err) => { - if (err) return typeof callback === 'function' ? callback(err) : null; - this._router = new Router(this._node, this._timers); + if (err) return isFunction(callback) ? callback(err) : null + this._router = new Router(this._node, this._timers) for (const key of Object.keys(routes)) { const item = routes[key] const route = this._router.route( item.path , item.method , item.handler - ); + ) debug('loaded: %s %s', item.method, item.path) - item.middleware && route.before( item.middleware ); + item.middleware && route.before( item.middleware ) } // When nodes are added / removed exec a rebalanace of local timers // If this node is not the owner, sent it back in the ring - this._node.on('ringchange', (evt) => { - this._timers.rebalance(evt, this._node, (data) => { - this.proxy(data); - }); - }); + + if (this.options.autobalance) { + this._node.on('ringchange', (evt) => { + this._timers.rebalance(evt, this._node, (data) => { + this.proxy(data) + }) + }) + } + + process.on('SIGUSR2', () => { + this._timers.rebalance({}, this._node, (data) => { + this.proxy(data) + }) + }) // Join the ring this._node.join(this.options.seeds, (err) => { if (err) { - return typeof callback === 'function' ? callback(err) : null; + return isFunction(callback) ? callback(err) : null } // delegate mock requests from the ring to the // API router this._node.handle(( req, res ) => { - this._router.handle( req, res ); - }); + this._router.handle( req, res ) + }) - // listen timers being purged over nats when a remote + // listen for timers being purged over nats when a remote // node is evicted or shutdown this._timers.watch(`skyring:${this._group}`, (err, data) => { - this.proxy(data); - }); + this.proxy(data) + }) debug('binding to port', port) - super.listen(port, ...args); - }); - }); - return this; + super.listen(port, ...args) + }) + }) + return this } proxy(data) { - debug('fabricating request', data.id); + debug('fabricating request', data.id) const opts = { url: '/timer' , method: 'POST' @@ -161,11 +181,11 @@ class Server extends http.Server { "x-timer-id": data.id } , payload: JSON.stringify(data) - }; - const res = new mock.Response(); - const req = new mock.Request( opts ); + } + const res = new mock.Response() + const req = new mock.Request( opts ) debug('routing fabricated request', data.id) - this._router.handle( req, res ); + this._router.handle( req, res ) this.emit('proxy', data) } /** @@ -175,7 +195,7 @@ class Server extends http.Server { * @param {Function} callback A callback to be called when the server is completely shut down **/ close( cb ){ - if(this.closed) return cb && setImmediate(cb); + if(this.closed) return isFunction(cb) ? setImmediate(cb) : null super.close(() => { this._node.close(() => { const active = this._node._ring.membership.members.filter((m) => { @@ -184,22 +204,22 @@ class Server extends http.Server { if (active.length) { return this._timers.shutdown(() => { - debug('closing server'); - this.closed = true; - cb && cb(); - }); + debug('closing server') + this.closed = true + cb && cb() + }) } debug('Last node in cluster - skipping rebalanace') this._timers.disconnect(() => { - debug('closing server'); - this.closed = true; - cb && cb(); + debug('closing server') + this.closed = true + cb && cb() }) - }); - }); + }) + }) } } -module.exports = Server; -module.exports.Router = Router; +module.exports = Server +module.exports.Router = Router diff --git a/packages/skyring/lib/server/request.js b/packages/skyring/lib/server/request.js index 594bfeb0..20bf45ac 100644 --- a/packages/skyring/lib/server/request.js +++ b/packages/skyring/lib/server/request.js @@ -26,6 +26,8 @@ function Request( req ) { const parsed = parseurl(req); this._body = false; this.body = null; + this.timers = null; + this.res = null; if ( parsed ) { this.query = parsed.query; @@ -42,8 +44,8 @@ function Request( req ) { * @returns {String} The request header, if set */ Request.prototype.get = function get( key ) { - const _key = key.toLowerCae(); - const headers = this.req.headers || {}; + const _key = key.toLowerCase(); + const headers = this.headers || {}; switch (_key) { case 'referrer': case 'referer': diff --git a/packages/skyring/lib/server/response.js b/packages/skyring/lib/server/response.js index 68769868..9d305526 100644 --- a/packages/skyring/lib/server/response.js +++ b/packages/skyring/lib/server/response.js @@ -35,12 +35,12 @@ Response.prototype.error = function error( err, msg ) { }); } err.statusCode = err.statusCode || err.code; - if( !err.statusCode ) { + if(!err.statusCode) { err.statusCode = 500; err.message = 'Internal Server Error'; } - this.status( err.statusCode ); + this.status(err.statusCode); debug(err); this.res.setHeader('x-skyring-reason', err.message); return this.end(); diff --git a/packages/skyring/lib/server/route.js b/packages/skyring/lib/server/route.js index be0e711b..78fe576b 100644 --- a/packages/skyring/lib/server/route.js +++ b/packages/skyring/lib/server/route.js @@ -1,4 +1,4 @@ -'use strict'; +'use strict' /** * represents the middleware stack for a url / method combination * @module skyring/lib/server/route @@ -7,7 +7,7 @@ * @requires path-to-regexp */ -const pathToRegExp = require('path-to-regexp'); +const pathToRegExp = require('path-to-regexp') /** * @constructor @@ -25,17 +25,17 @@ rte.use((req, res, node, next) => { }) **/ function Route(path, method) { - this.path = path; - this.method = method; - this._keys = []; - this.stack = []; - this.regexp = pathToRegExp(path, this._keys); - this.keys = new Array(this._keys.length); - this.params = Object.create(null); + this.path = path + this.method = method + this._keys = [] + this.stack = [] + this.regexp = pathToRegExp(path, this._keys) + this.keys = new Array(this._keys.length) + this.params = Object.create(null) for( var idx = 0; idx < this._keys.length; idx++ ) { - this.keys[ idx ] = this._keys[ idx ].name; - this.params[ this._keys[ idx ].name ] = undefined; + this.keys[ idx ] = this._keys[ idx ].name + this.params[ this._keys[ idx ].name ] = undefined } } @@ -44,65 +44,65 @@ function Route(path, method) { * @method module:skyring/lib/server/route#use * @param {module:skyring/lib/server/route~Middleware} fn a the middelware function to add **/ -Route.prototype.use = function use( fn ) { - if( Array.isArray( fn ) ) { +Route.prototype.use = function use(fn) { + if (Array.isArray(fn)) { for(var idx = 0; idx < fn.length; idx++) { - this.stack.push( fn[idx] ); + this.stack.push(fn[idx]) } } else { - this.stack.push( fn ); + this.stack.push(fn) } - return this; -}; + return this +} /** * Adds a middleware function to the beginning of the internal route stack * @method module:skyring/lib/server/route#before * @param {module:skyring/lib/server/route~Middleware} fn a the middelware function to add **/ -Route.prototype.before = function before( fn ) { - if( Array.isArray( fn ) ) { - this.stack.unshift( ...fn ); +Route.prototype.before = function before(fn) { + if (Array.isArray(fn)) { + this.stack.unshift(...fn) } else { - this.stack.unshift( fn ); + this.stack.unshift(fn) } - return this; -}; + return this +} -Route.prototype.match = function match( path ) { - const matches = this.regexp.exec( path ); - if ( !matches ) return null; +Route.prototype.match = function match(path) { + const matches = this.regexp.exec(path) + if (!matches) return null - const keys = this.keys; - const params = Object.assign({}, this.params); + const keys = this.keys + const params = Object.assign({}, this.params) - for( var idx = 1; idx < matches.length; idx++ ) { - params[ keys[ idx - 1 ] ] = matches[ idx ]; + for (var idx = 1; idx < matches.length; idx++) { + params[keys[idx - 1]] = matches[idx] } - return params; -}; + return params +} -Route.prototype.process = function process( req, res, node, next ) { - const stack = this.stack; +Route.prototype.process = function process(req, res, node, next) { + const stack = this.stack ;(function run( idx ) { - const fn = stack[ idx]; + const fn = stack[idx] try { - fn(req, res, node, (err) => { - if ( err ) return next( err ); - if( idx === stack.length -1 ) return next(); - run(++idx); - }); + fn(req, res, node, (err, body) => { + if ( err ) return next( err ) + if( idx === stack.length -1 ) return next() + run(++idx) + }) } catch ( err ){ - err.statusCode = err.statusCode || 500; - return next( err ); + err.statusCode = err.statusCode || 500 + return next( err ) } - })(0); -}; + })(0) +} -module.exports = Route; +module.exports = Route /** * A route middleware function diff --git a/packages/skyring/lib/server/router.js b/packages/skyring/lib/server/router.js index 1b06da95..4f8df41d 100644 --- a/packages/skyring/lib/server/router.js +++ b/packages/skyring/lib/server/router.js @@ -1,5 +1,5 @@ /*jshint laxcomma: true, smarttabs: true, node: true, esnext: true*/ -'use strict'; +'use strict' /** * Simple router class for directing requests * @module skyring/lib/server/router @@ -11,10 +11,9 @@ */ const Route = require('./route') - , Request = require('./request') - , Response = require('./response') - , debug = require('debug')('skyring:server:router') - ; +const Request = require('./request') +const Response = require('./response') +const debug = require('debug')('skyring:server:router') /** * @constructor @@ -25,10 +24,10 @@ const Route = require('./route') router.handle(req, res) */ function Router( node, timers ) { - this.routes = new Map(); - this.route_options = new Map(); - this.node = node; - this.timers = timers; + this.routes = new Map() + this.route_options = new Map() + this.node = node + this.timers = timers } /** @@ -37,8 +36,8 @@ function Router( node, timers ) { * @param {Function} handler The handler function to call when the route is matched **/ Router.prototype.get = function get( path, fn ) { - this.route( path, 'GET', fn ); -}; + return this.route( path, 'GET', fn ) +} /** * Adds a new put handler to the router @@ -46,8 +45,8 @@ Router.prototype.get = function get( path, fn ) { * @param {Function} handler The handler function to call when the route is matched **/ Router.prototype.put = function put( path, fn ) { - this.route( path, 'PUT', fn); -}; + return this.route( path, 'PUT', fn) +} /** * Adds a new post handler to the router @@ -55,8 +54,8 @@ Router.prototype.put = function put( path, fn ) { * @param {Function} handler The handler function to call when the route is matched **/ Router.prototype.post = function post( path, fn ) { - this.route( path, 'POST', fn); -}; + return this.route( path, 'POST', fn) +} /** * Adds a new patch handler to the router @@ -64,8 +63,8 @@ Router.prototype.post = function post( path, fn ) { * @param {Function} handler The handler function to call when the route is matched **/ Router.prototype.patch = function patch( path, fn ) { - this.route( path, 'PATCH', fn); -}; + return this.route( path, 'PATCH', fn) +} /** * Adds a new delete handler to the router @@ -73,8 +72,8 @@ Router.prototype.patch = function patch( path, fn ) { * @param {Function} handler The handler function to call when the route is matched **/ Router.prototype.delete = function( path, fn ) { - this.route( path, 'DELETE', fn ); -}; + return this.route( path, 'DELETE', fn ) +} /** * Adds a new opts handler to the router @@ -82,8 +81,8 @@ Router.prototype.delete = function( path, fn ) { * @param {Function} handler The handler function to call when the route is matched **/ Router.prototype.options = function options( path, fn ) { - this.route( path, 'OPTIONS', fn ); -}; + return this.route( path, 'OPTIONS', fn ) +} /** * Adds a new route handler to the router @@ -92,22 +91,22 @@ Router.prototype.options = function options( path, fn ) { * @param {Function} The handler function to call when the route is matched * @returns {module:skyring/lib/server/route} **/ -Router.prototype.route = function route( path, method, fn ) { - const _method = method.toUpperCase(); - const map = this.routes.get(_method) || new Map(); - - if ( map.has( path ) ) { - const rte = map.get( path ); - rte.use( fn ); - return rte; +Router.prototype.route = function route(path, method, fn) { + const _method = method.toUpperCase() + const map = this.routes.get(_method) || new Map() + + if (map.has(path)) { + const rte = map.get(path) + rte.use(fn) + return rte } - const rte = new Route( path, _method); - rte.use( fn ); - map.set( path, rte ); - this.routes.set( _method, map ); - return rte; -}; + const rte = new Route(path, _method) + rte.use(fn) + map.set(path, rte) + this.routes.set(_method, map) + return rte +} /** * Entrypoint for an incoming request @@ -120,33 +119,33 @@ http.createServer((req, res) => { router.handle(req, res) }) **/ -Router.prototype.handle = function handle( req, res ) { - req.$ = new Request( req ); - res.$ = new Response( res ); - req.$.timers = this.timers; - - const path = req.$.path; - const method = req.method.toUpperCase(); - const map = this.routes.get( method ); - - if( map ) { - let rte = map.get( path ); - if ( rte ) { - req.$.params = Object.create(null); - return this.handleRoute( rte, req, res ); +Router.prototype.handle = function handle(req, res) { + req.$ = new Request(req) + res.$ = new Response(res) + req.$.timers = this.timers + + const path = req.$.path + const method = req.method.toUpperCase() + const map = this.routes.get(method) + + if (map) { + let rte = map.get(path) + if (rte) { + req.$.params = Object.create(null) + return this.handleRoute(rte, req, res) } - for ( const route of map.values() ){ - const params = route.match( path ); - if ( params ) { - req.$.params = params; - return this.handleRoute( route, req, res ); + for (const route of map.values()) { + const params = route.match(path) + if (params) { + req.$.params = params + return this.handleRoute(route, req, res) } } } - return notFound( req, res ); -}; + return notFound(req, res) +} /** * Responsible for executing the middleware stack on the route ( including the end handler ) @@ -154,21 +153,21 @@ Router.prototype.handle = function handle( req, res ) { * @param {http.IncomingMessage} req * @param {http.ServerResponse} res **/ -Router.prototype.handleRoute = function handleRoute( route, req, res ) { +Router.prototype.handleRoute = function handleRoute(route, req, res) { debug('routing ', route.method, route.path) - route.process(req, res, this.node, ( err ) => { - if ( err ) return res.$.error( err ); - if ( res.$.body ) return res.$.json( res.$.body ); - return res.$.end(); - }); -}; + route.process(req, res, this.node, (err) => { + if (err) return res.$.error(err) + if (res.$.body) return res.$.json(res.$.body) + return res.$.end() + }) +} function notFound( req, res ) { res.writeHead(404,{ 'Content-Type': 'application/json' - }); - res.end( JSON.stringify({message: 'Not Found' }) ); + }) + res.end(JSON.stringify({message: 'Not Found' })) } -module.exports = Router; +module.exports = Router diff --git a/packages/skyring/lib/timer.js b/packages/skyring/lib/timer.js index 289c0890..0b687db8 100644 --- a/packages/skyring/lib/timer.js +++ b/packages/skyring/lib/timer.js @@ -31,7 +31,7 @@ const shutdown = Symbol.for('kShutdown') const kNode = Symbol('nodeid') const kRemove = Symbol('remove') const noop = () => {} - +const REBALANCE_SUB = 'skyring.rebalance' const EVENT_STATUS = { CREATED: 'create' , UPDATED: 'replace' @@ -111,10 +111,10 @@ class Timer extends Map { store('storage backend ready', store_opts) debug('node id', this[kNode]) this.recover(() => { - this.nats.publish('skyring:node', JSON.stringify({ + this.nats.publish('skyring:node', { node: this[kNode] , type: EVENT_STATUS.READY - }), cb) + }, cb) }) }) @@ -186,14 +186,14 @@ timers.create(id, options, (err) => { , this ) - this.nats.publish('skyring:events', JSON.stringify({ + this.nats.publish('skyring:events', { type: EVENT_STATUS.EXEC , timer: id , node: this[kNode] , executed: Date.now() , created: created , payload: payload - }), noop) + }, noop) cb(null, id) return null @@ -215,13 +215,13 @@ timers.create(id, options, (err) => { return null } - this.nats.publish('skyring:events', JSON.stringify({ + this.nats.publish('skyring:events', { type: EVENT_STATUS.CREATED , timer: id , node: this[kNode] , created: data.created , payload: payload - }), noop) + }, noop) data.timer = setTimeout( transport.exec.bind(transport) @@ -247,11 +247,11 @@ timers.create(id, options, (err) => { **/ success(id, cb = noop) { this[kRemove](id, (err) => { - this.nats.publish('skyring:events', JSON.stringify({ + this.nats.publish('skyring:events', { type: EVENT_STATUS.SUCCESS , timer: id , node: this[kNode] - }), cb) + }, cb) }) } @@ -268,14 +268,14 @@ timers.failure('2e2f6dad-9678-4caf-bc41-8e62ca07d551', error) **/ failure(id, error, cb = noop) { this[kRemove](id, (err) => { - this.nats.publish('skyring:events', JSON.stringify({ + this.nats.publish('skyring:events', { type: EVENT_STATUS.FAIL , timer: id , node: this[kNode] , message: error.message , stack: error.stack , error: error.code || error.name - }), cb) + }, cb) }) } @@ -289,12 +289,11 @@ timers.failure('2e2f6dad-9678-4caf-bc41-8e62ca07d551', error) cancel(id, cb = noop) { this[kRemove](id, (err) => { if (err) return cb(err) - this.nats.publish('skyring:events', JSON.stringify({ + this.nats.publish('skyring:events', { type: EVENT_STATUS.CANCELLED , timer: id , node: this[kNode] - })) - cb() + }, cb) }) } @@ -326,10 +325,10 @@ timers.failure('2e2f6dad-9678-4caf-bc41-8e62ca07d551', error) if(!size) return rebalance('node %s begin rebalance; timers: %d', this[kNode], size) - this.nats.publish('skyring:node', JSON.stringify({ + this.nats.publish('skyring:node', { node: this[kNode] , type: EVENT_STATUS.REBALANCE - }), noop) + }, noop) const records = this.values() @@ -347,16 +346,16 @@ timers.failure('2e2f6dad-9678-4caf-bc41-8e62ca07d551', error) rebalance('node %s no longer the owner of %s', this[kNode], obj.id) - this.nats.publish('skyring:events', JSON.stringify({ + this.nats.publish('skyring:events', { node: this[kNode] , type: EVENT_STATUS.EVICT , timer: obj.id - }), noop) + }, noop) cb(data) } - for( var record of records ) { + for(var record of records) { run(record) } @@ -366,10 +365,10 @@ timers.failure('2e2f6dad-9678-4caf-bc41-8e62ca07d551', error) } recover(cb = noop) { - this.nats.publish('skyring:node', JSON.stringify({ + this.nats.publish('skyring:node', { node: this[kNode] , type: EVENT_STATUS.RECOVERY - }), noop) + }, noop) const fn = (data) => { store('recover', data.key) @@ -428,12 +427,12 @@ timers.failure('2e2f6dad-9678-4caf-bc41-8e62ca07d551', error) disconnect(cb = noop) { this[storage].close(noop) this.transports[shutdown](() => { - this.nats.publish('skyring:node', JSON.stringify({ + this.nats.publish('skyring:node', { node: this[kNode] , type: EVENT_STATUS.SHUTDOWN - }), noop) + }, noop) - this.nats.flush((err) => { + this.nats.drainSubscription(this._sid, (err) => { if (err) return cb(err) this.nats.quit(cb) }) @@ -451,11 +450,12 @@ timers.failure('2e2f6dad-9678-4caf-bc41-8e62ca07d551', error) if (!size) { this[storage].close() return this.transports[shutdown](() => { - this.nats.publish('skyring:node', JSON.stringify({ + this.nats.publish('skyring:node', { node: this[kNode] , type: EVENT_STATUS.SHUTDOWN - }), noop) - this.nats.flush((err) => { + }, noop) + + this.nats.drainSubscription(this._sid, (err) => { if (err) return cb(err) this.nats.quit(cb) }) @@ -476,10 +476,10 @@ timers.failure('2e2f6dad-9678-4caf-bc41-8e62ca07d551', error) const data = Object.assign({}, obj.payload, { id: obj.id , created: obj.created - , count: ++senst + , count: ++sent }) - this.nats.publish('skyring', JSON.stringify(data), () => { + this.nats.request(REBALANCE_SUB, data, (reply) => { if (++acks === size) { return batch.write(() => { store('batch delete finished') @@ -490,10 +490,10 @@ timers.failure('2e2f6dad-9678-4caf-bc41-8e62ca07d551', error) }) } - this.nats.publish('skyring:node', JSON.stringify({ + this.nats.publish('skyring:node', { node: this[kNode] , type: EVENT_STATUS.PURGE - }), noop) + }, noop) for(let record of this.values()) { run(record) @@ -511,10 +511,10 @@ timers.failure('2e2f6dad-9678-4caf-bc41-8e62ca07d551', error) watch(key, cb) { if (this._bail) return const opts = { queue: key } - this._sid = this.nats.subscribe('skyring', opts, ( data ) => { + this._sid = this.nats.subscribe(REBALANCE_SUB, opts, (data, reply) => { + if (reply) this.nats.publish(reply, {node: this[kNode], timer: data.id}) if(this._bail) return - const value = json.parse(data) - cb(value.error, value.value) + cb(null, data) }) return this._sid } diff --git a/packages/skyring/test/integration/post-timer.spec.js b/packages/skyring/test/integration/post-timer.spec.js index bbc18d13..ff8c38ed 100644 --- a/packages/skyring/test/integration/post-timer.spec.js +++ b/packages/skyring/test/integration/post-timer.spec.js @@ -1,10 +1,10 @@ 'user strict'; -const http = require('http') -const os = require('os') -const {test} = require('tap') -const supertest = require('supertest') -const Server = require('../../lib') -const {ports} = require('../util') +const http = require('http') +const os = require('os') +const {test} = require('tap') +const supertest = require('supertest') +const Server = require('../../lib') +const {sys, testCase} = require('../../../../test') let hostname = null; @@ -45,14 +45,20 @@ test('skyring:api', (t) => { server = new Server({ seeds: [`${hostname}:3455`] }); - request = supertest('http://localhost:3333'); - server.listen(3333, null, null, tt.end) + server.listen(3333, (err) => { + tt.error(err, 'starting the server should not error') + request = supertest(`http://localhost:${server.address().port}`); + tt.end() + }) }); t.test('#POST /timer', (tt) => { let sone, stwo, sthree - tt.test('should set a timer postback (201)', (ttt) => { + testCase(tt, { + code: 201 + , description: 'should set a timer postback' + }, (ttt) => { ttt.plan(6) toServer(8989, 'hello', 'post', 1000, ttt) request @@ -165,57 +171,67 @@ test('skyring:api', (t) => { ttt.end() }); }); - tt.test('should not allow request with no callback - (400)', (ttt) => { - request - .post('/timer') - .send({ - timeout:1000 - , data: 'hello' - }) - .expect(400) - .end((err, res) => { - ttt.error(err) - ttt.end() - }); + + testCase(tt, { + code: 400 + , description: 'should not allow request with no callback' + }, async (ttt) => { + const payload ={ + timeout:1000 + , data: 'hello' + } + const {headers} = await request.post('/timer').send(payload).expect(400) + ttt.match(headers, { + location: /\/timer\/(\w+)/ + , 'x-skyring-reason': /callback is required/ig + }) }); - tt.test('should not allow request with no uri - (400)', (ttt) => { - request - .post('/timer') - .send({ - timeout:1000 - , data: 'hello' - , callback: { - transport: 'http' - , method: 'post' - } - }) - .expect(400) - .end((err, res) => { - ttt.error(err) - ttt.end() - }); + testCase(tt, { + code: 400 + , description: 'should not allow request with no uri' + }, async (ttt) => { + const payload = { + timeout:1000 + , data: 'hello' + , callback: { + transport: 'http' + , method: 'post' + } + } + await request.post('/timer').send(payload).expect(400) }); - tt.test('should not allow request with no transport - (400)', (ttt) => { - request + testCase(tt, { + code: 400 + , description: 'should not allow request with no transport' + }, async (ttt) => { + const payload = { + timeout:1000 + , data: 'hello' + , callback: { + uri: 'http://foo.com' + , method: 'post' + } + } + await request.post('/timer').send(payload).expect(400) + }); + + testCase(tt, { + code: 400 + , description: 'Invalid JSON' + }, async (ttt) => { + await request .post('/timer') - .send({ - timeout:1000 - , data: 'hello' - , callback: { - uri: 'http://foo.com' - , method: 'post' - } + .set({ + 'Content-Type': 'application/json' }) + .send('{"foo","bar"}') .expect(400) - .end((err, res) => { - ttt.error(err) - ttt.end() - }); - }); + }) tt.end() }); + t.test('close server', (tt) => { server.close(tt.end) }) diff --git a/packages/skyring/test/unit/router.spec.js b/packages/skyring/test/unit/router.spec.js new file mode 100644 index 00000000..09f2e5a6 --- /dev/null +++ b/packages/skyring/test/unit/router.spec.js @@ -0,0 +1,436 @@ +'use strict' + +const http = require('http') +const body = require('body') +const {test, threw} = require('tap') +const supertest = require('supertest') +const Router = require('../../lib/server/router') +const {testCase} = require('../../../../test') + +test('router', async (t) => { + testCase(t, { + code: 'GET' + , description: 'router#get adds handler for a get request' + }, (tt) => { + let server = null + let request = null + const router = new Router() + tt.on('end', () => { + server && server.close() + }) + + tt.test('setup http', (ttt) => { + server = http.createServer((req, res) => { + router.handle(req, res) + }).listen(0, (err) => { + ttt.error(err) + request = supertest(`http://localhost:${server.address().port}`) + ttt.end() + }) + }) + + tt.test('/foo/:bar', (ttt) => { + + router.get('/foo/:bar', (req, res, node, cb) => { + const header = req.$.get('x-manual-header') + ttt.equal(header, 'foobar', 'x-manual-header = foobar') + ttt.match(req.$.params, {bar: '1'}) + res.$.set('x-response-header', 'test') + res.$.status(200) + cb() + }) + + request + .get('/foo/1') + .set({'x-manual-header': 'foobar'}) + .expect(200) + .end((err, res) => { + ttt.error(err) + ttt.end() + }) + }) + tt.end() + }) + + testCase(t, { + code: 'POST' + , description: 'router#post adds handler for a post request' + }, (tt) => { + let server = null + let request = null + const router = new Router() + tt.on('end', () => { + server && server.close() + }) + + tt.test('setup http', (ttt) => { + server = http.createServer((req, res) => { + router.handle(req, res) + }).listen(0, (err) => { + ttt.error(err) + request = supertest(`http://localhost:${server.address().port}`) + ttt.end() + }) + }) + + tt.test('/foo', (ttt) => { + const route = router.post('/foo', (req, res, node, cb) => { + ttt.match(req.$.body, { + bar: 'baz' + }) + res.$.status(201) + cb(null, {baz: 'foo'}) + }) + + route.before([ + (req, res, _, cb) => { + body(req, res, (err, data) => { + try { + req.$.body = JSON.parse(data) + return cb() + } catch (err) { + cb(err) + } + }) + } + ]) + + request + .post('/foo') + .set({'Content-Type': 'application/json'}) + .send({bar: 'baz'}) + .expect(201) + .end((err, res) => { + ttt.error(err) + ttt.end() + }) + }) + tt.end() + }) + + testCase(t, { + code: 'PUT' + , description: 'router#put adds handler for a put request' + }, (tt) => { + let server = null + let request = null + const router = new Router() + tt.on('end', () => { + server && server.close() + }) + + tt.test('setup http', (ttt) => { + server = http.createServer((req, res) => { + router.handle(req, res) + }).listen(0, (err) => { + ttt.error(err) + request = supertest(`http://localhost:${server.address().port}`) + ttt.end() + }) + }) + + tt.test('/foo/:baz(\w+)', (ttt) => { + const route = router.put('/foo/:baz(\\w+)', (req, res, node, cb) => { + ttt.match(req.$.body, { + bar: 'baz' + }) + ttt.match(req.$.params, { + baz: 'foobar' + }) + res.$.status(202) + cb(null, {baz: 'foo'}) + }) + + route.before([ + (req, res, _, cb) => { + body(req, res, (err, data) => { + try { + req.$.body = JSON.parse(data) + return cb() + } catch (err) { + cb(err) + } + }) + } + ]) + + request + .put('/foo/foobar') + .set({'Content-Type': 'application/json'}) + .send({bar: 'baz'}) + .expect(202) + .end((err, res) => { + ttt.error(err) + ttt.end() + }) + }) + tt.end() + }) + + testCase(t, { + code: 'PATCH' + , description: 'router#patch adds handler for a patch request' + }, (tt) => { + let server = null + let request = null + const router = new Router() + tt.on('end', () => { + server && server.close() + }) + + tt.test('setup http', (ttt) => { + server = http.createServer((req, res) => { + router.handle(req, res) + }).listen(0, (err) => { + ttt.error(err) + request = supertest(`http://localhost:${server.address().port}`) + ttt.end() + }) + }) + + tt.test('/patch/:baz(\w+)', (ttt) => { + const route = router.patch('/patch/:baz(\\w+)', (req, res, node, cb) => { + ttt.match(req.$.body, { + bar: 'baz' + }) + ttt.match(req.$.params, { + baz: 'resource' + }) + res.$.status(202) + cb() + }) + + route.before([ + (req, res, _, cb) => { + body(req, res, (err, data) => { + try { + req.$.body = JSON.parse(data) + return cb() + } catch (err) { + cb(err) + } + }) + } + ]) + + request + .patch('/patch/resource') + .set({'Content-Type': 'application/json'}) + .send({bar: 'baz'}) + .expect(202) + .end((err, res) => { + ttt.error(err) + ttt.end() + }) + }) + tt.end() + }) + + testCase(t, { + code: 'DELETE' + , description: 'router#delete adds handler for a delete request' + }, (tt) => { + let server = null + let request = null + const router = new Router() + tt.on('end', () => { + server && server.close() + }) + + tt.test('setup http', (ttt) => { + server = http.createServer((req, res) => { + router.handle(req, res) + }).listen(0, (err) => { + ttt.error(err) + request = supertest(`http://localhost:${server.address().port}`) + ttt.end() + }) + }) + + tt.test('/delete/:whiz(\\w+)', (ttt) => { + const route = router.delete('/delete/:whiz(\\w+)', (req, res, node, cb) => { + ttt.match(req.$.params, { + whiz: 'resource' + }) + res.$.status(204) + cb() + }) + + request + .delete('/delete/resource') + .expect(204) + .end((err, res) => { + ttt.error(err) + ttt.end() + }) + }) + tt.end() + }) + + testCase(t, { + code: 'OPTIONS' + , description: 'router#options adds handler for a options request' + }, (tt) => { + let server = null + let request = null + const router = new Router() + tt.on('end', () => { + server && server.close() + }) + + tt.test('setup http', (ttt) => { + server = http.createServer((req, res) => { + router.handle(req, res) + }).listen(0, (err) => { + ttt.error(err) + request = supertest(`http://localhost:${server.address().port}`) + ttt.end() + }) + }) + + tt.test('/opts', (ttt) => { + const route = router.options('/opts', (req, res, node, cb) => { + res.$.status(204) + cb() + }) + + request + .options('/opts') + .expect(204) + .end((err, res) => { + ttt.error(err) + ttt.end() + }) + }) + tt.end() + }) + + testCase(t, { + code: 200 + , description: 'route#route duplicate routes paths stack' + }, (tt) => { + let server = null + let request = null + const router = new Router() + tt.on('end', () => { + server && server.close() + }) + + tt.test('setup http', (ttt) => { + server = http.createServer((req, res) => { + router.handle(req, res) + }).listen(0, (err) => { + ttt.error(err) + request = supertest(`http://localhost:${server.address().port}`) + ttt.end() + }) + }) + + tt.test('/stack', (ttt) => { + const route = router.route('/stack', 'GET', (req, res, node, cb) => { + res.$.body.stack++ + cb() + }) + + router.route('/stack', 'GET', [(req, res, node, cb) => { + res.$.body.stack++ + cb() + }]) + + route.before( + (req, res, node, cb) => { + res.$.body = {stack: 0} + cb() + } + ) + + request + .get('/stack') + .set({ + 'Accept': 'application/json' + }) + .expect(200) + .end((err, res) => { + ttt.error(err) + ttt.match(res, { + body: { + stack: 2 + } + }) + ttt.end() + }) + }) + tt.end() + }) + testCase(t, { + code: 404 + , description: 'Route not found' + }, (tt) => { + let server = null + let request = null + const router = new Router() + tt.on('end', () => { + server && server.close() + }) + + tt.test('setup http', (ttt) => { + server = http.createServer((req, res) => { + router.handle(req, res) + }).listen(0, (err) => { + ttt.error(err) + request = supertest(`http://localhost:${server.address().port}`) + ttt.end() + }) + }) + + tt.test('/route/not/found', (ttt) => { + request + .get('/route/not/found') + .expect(404) + .end((err, res) => { + ttt.error(err) + ttt.end() + }) + }) + tt.end() + }) + + testCase(t, { + code: 501 + , description: 'error.statusCode is bubbled to status' + }, (tt) => { + let server = null + let request = null + const router = new Router() + tt.on('end', () => { + server && server.close() + }) + + tt.test('setup http', (ttt) => { + server = http.createServer((req, res) => { + router.handle(req, res) + }).listen(0, (err) => { + ttt.error(err) + request = supertest(`http://localhost:${server.address().port}`) + ttt.end() + }) + }) + + tt.test('/error', (ttt) => { + router.route('/error', 'GET', (req, res, node, cb) => { + const error = new Error('Broken') + error.statusCode = 501 + throw error + }) + + request + .get('/error') + .expect(501) + .end((err, res) => { + ttt.error(err) + ttt.end() + }) + }) + tt.end() + }) +}).catch(threw) diff --git a/packages/skyring/test/unit/server.spec.js b/packages/skyring/test/unit/server.spec.js index 8429843f..ba0cd5b3 100644 --- a/packages/skyring/test/unit/server.spec.js +++ b/packages/skyring/test/unit/server.spec.js @@ -9,24 +9,24 @@ const supertest = require('supertest') const async = require('async') const conf = require('keef') const {test, pass} = require('tap') -const {ports} = require('../util') const Server = require('../../lib/server') +const {sys, rand} = require('../../../../test') test('server', async (t) => { let sone, stwo, sthree, sfour - var hostname; + var hostname if(!process.env.TEST_HOST) { hostname = os.hostname() console.log(`env variable TEST_HOST not set. using ${hostname} as hostname`) } else { - hostname = process.env.TEST_HOST; + hostname = process.env.TEST_HOST } const [ http_one, http_two, http_three , ring_one, ring_two, ring_three, ring_four , callback_port - ] = await ports(8) + ] = await sys.ports(8) t.test('setup server nodes', (tt) => { async.parallel([ @@ -39,11 +39,11 @@ test('server', async (t) => { } , seeds: [`${hostname}:${ring_one}`, `${hostname}:${ring_two}`] , storage:{ - path: path.join(os.tmpdir(), crypto.randomBytes(10).toString('hex')) + path: path.join(os.tmpdir(), rand.bytes()) , backend: 'leveldown' } }) - .listen(http_one, cb); + .listen(http_one, cb) } , (cb) => { stwo = new Server({ @@ -54,11 +54,11 @@ test('server', async (t) => { } , seeds: [`${hostname}:${ring_one}`, `${hostname}:${ring_two}`] , storage:{ - path: path.join(os.tmpdir(), crypto.randomBytes(10).toString('hex')) + path: path.join(os.tmpdir(), rand.bytes()) , backend: 'leveldown' } }) - .listen(http_two, null, null, cb); + .listen(http_two, cb) } , (cb) => { sthree = new Server({ @@ -69,11 +69,11 @@ test('server', async (t) => { } , seeds: [`${hostname}:${ring_one}`, `${hostname}:${ring_two}`] , storage:{ - path: path.join(os.tmpdir(), crypto.randomBytes(10).toString('hex')) + path: path.join(os.tmpdir(), rand.bytes()) , backend: 'memdown' } }) - .listen(http_three, cb); + .listen(http_three, cb) } ], (err) => { tt.error(err) @@ -84,54 +84,55 @@ test('server', async (t) => { , app: 'rebalance' } , seeds: [`${hostname}:${ring_one}`, `${hostname}:${ring_two}`] + , autobalance: true , storage:{ - path: path.join(os.tmpdir(), crypto.randomBytes(10).toString('hex')) + path: path.join(os.tmpdir(), rand.bytes()) , backend: 'memdown' } }) tt.end() - }); + }) }) t.on('end', () => { sone.close(() => { t.comment('sone closed') - }); + }) stwo.close(() => { t.comment('stwo closed') - }); + }) sfour.close(() => { t.comment('sfour closed') - }); - }); + }) + }) t.test('should bootstrap server nodes', (tt) => { - tt.ok(sone); - tt.ok(stwo); - tt.ok(sthree); - tt.ok(sfour); - tt.end(); + tt.ok(sone) + tt.ok(stwo) + tt.ok(sthree) + tt.ok(sfour) + tt.end() }) t.test('rebalance on shutdown', function(tt) { let count = 0, postback tt.on('end',(done) => { - postback && postback.close(done); + postback && postback.close(done) }) tt.test('should survive a nodes moving', (ttt) => { - const request = supertest(`http://localhost:${http_one}`); + const request = supertest(`http://localhost:${http_one}`) const requests = Array.from(Array(100).keys()) postback = http.createServer((req, res) => { const parsed = url.parse(req.url) const q = qs.parse(parsed.query) ttt.pass(`timer ${q.idx} handled`) res.writeHead(200) - res.end(); - }).listen(callback_port); + res.end() + }).listen(callback_port) async.until( async function _test() { @@ -170,7 +171,7 @@ test('server', async (t) => { }) } ) - }); + }) tt.end() - }); -}); + }) +}) diff --git a/packages/skyring/test/unit/validator.spec.js b/packages/skyring/test/unit/validator.spec.js new file mode 100644 index 00000000..cc9f30ad --- /dev/null +++ b/packages/skyring/test/unit/validator.spec.js @@ -0,0 +1,172 @@ +'use strict' + +const {test} = require('tap') +const validator = require('../../lib/server/api/validators/timer') +const {testCase} = require('../../../../test') + +test('timer payload validation', async (t) => { + testCase(t, { + code: '400' + , description: 'timeout must be a number' + }, (tt) => { + validator({ + timeout: null + }, (err) => { + tt.type(err, Error, 'error is of type Error') + tt.match(err, { + statusCode: 400 + , message: /timeout is required and must be a positive number/ + }) + tt.end() + }) + }) + + testCase(t, { + code: '400' + , description: 'timemout must be positive' + }, (tt) => { + validator({ + timeout: -1 + }, (err) => { + tt.type(err, Error, 'error is of type Error') + tt.match(err, { + statusCode: 400 + , message: /timeout is required and must be a positive number/i + }) + tt.end() + }) + }) + + testCase(t, { + code: '400' + , description: 'Exceeds maximum value' + }, (tt) => { + const MAX = Math.pow(2, 32 -1) - 1 + validator({ + timeout: MAX + 1 + }, (err) => { + tt.type(err, Error, 'error is of type Error') + tt.match(err, { + statusCode: 400 + , message: new RegExp(`less than or equal to ${MAX}`, 'ig') + }) + tt.end() + }) + }) + + testCase(t, { + code: '400' + , description: 'null value is not a valid data value' + }, (tt) => { + validator({ + timeout: 1 + , data: false + }, (err) => { + tt.type(err, Error, 'error is of type Error') + tt.match(err, { + statusCode: 400 + , message: /must be a string or object/ig + }) + tt.end() + }) + }) + + testCase(t, { + code: '400' + , description: 'callback must be an object' + }, (tt) => { + validator({ + timeout: 1 + , data: 'test' + , callback: 1 + }, (err) => { + tt.type(err, Error, 'error is of type Error') + tt.match(err, { + statusCode: 400 + , message: /callback is required and must be an object/i + }) + tt.end() + }) + }) + + testCase(t, { + code: '400' + , description: 'callback.transport must be a string' + }, (tt) => { + validator({ + timeout: 1 + , data: 'test' + , callback: { + transport: {} + } + }, (err) => { + tt.type(err, Error, 'error is of type Error') + tt.match(err, { + statusCode: 400 + , message: /callback.transport is required and must be a string/i + }) + tt.end() + }) + }) + + testCase(t, { + code: '400' + , description: 'callback.uri must be a string' + }, (tt) => { + validator({ + timeout: 1 + , data: 'test' + , callback: { + transport: 'http' + , uri: undefined + } + }, (err) => { + tt.type(err, Error, 'error is of type Error') + tt.match(err, { + statusCode: 400 + , message: /callback.uri is required and must be a string/i + }) + tt.end() + }) + }) + + testCase(t, { + code: '400' + , description: 'callback.method must be a string' + }, (tt) => { + validator({ + timeout: 1 + , data: 'test' + , callback: { + transport: 'http' + , uri: '/test' + , method: undefined + } + }, (err) => { + tt.type(err, Error, 'error is of type Error') + tt.match(err, { + statusCode: 400 + , message: /callback.method is required and must be a string/i + }) + tt.end() + }) + }) + + testCase(t, { + code: '200' + , description: 'callback.method must be a string' + }, (tt) => { + validator({ + timeout: 1 + , data: 'test' + , callback: { + transport: 'http' + , uri: '/test' + , method: 'post' + } + }, (err) => { + tt.error(err) + tt.end() + }) + }) +}) diff --git a/test/index.js b/test/index.js index 9b44bdb3..a57ed9a5 100644 --- a/test/index.js +++ b/test/index.js @@ -2,4 +2,10 @@ module.exports = { sys: require('./sys') +, rand: require('./rand') +, testCase: testCase +} + +function testCase(t, opts, cb) { + return t.test(`(${opts.code}) ${opts.description}`, cb).catch(t.threw) } diff --git a/test/rand.js b/test/rand.js new file mode 100644 index 00000000..4b41034f --- /dev/null +++ b/test/rand.js @@ -0,0 +1,9 @@ +'use strict' + +const crypto = require('crypto') + +module.exports = { bytes } + +function bytes(n = 5) { + return crypto.randomBytes(n).toString('hex') +}