diff --git a/lib/ext.js b/lib/ext.js new file mode 100755 index 000000000..31bedf5b1 --- /dev/null +++ b/lib/ext.js @@ -0,0 +1,280 @@ +// Load modules + +var Domain = require('domain'); +var Boom = require('boom'); +var Async = require('async'); +var Utils = require('./utils'); + + +// Declare internals + +var internals = {}; + +/* + Extension functions use the following signature: function (request, next) { next(); } +*/ + +module.exports = internals.Ext = function () { + + this._events = { + onRequest: null, // New request, before handing over to the router (allows changes to the request method, url, etc.) + onPreHandler: null, // After validation and body parsing, before route handler + onPostHandler: null // After route handler returns, before sending response + }; +}; + + +internals.Ext.prototype.add = function (event, func, options) { + + return this._add(event, func, options); +}; + + +internals.Ext.prototype._add = function (event, func, options, plugin) { + + options = options || {}; + + Utils.assert(['onRequest', 'onPreHandler', 'onPostHandler'].indexOf(event) !== -1, 'Unknown event type: ' + event); + + this._events[event] = this._events[event] || [] + + var ext = { + priority: this._events[event].length, + before: [].concat(options.before || []), + after: [].concat(options.after || []), + group: plugin || '?', + func: func + }; + + // Validate rules + + Utils.assert(ext.before.indexOf(ext.group) === -1, 'Plugin ext cannot come before itself (' + ext.plugin + ')'); + Utils.assert(ext.before.indexOf('?') === -1, 'Plugin ext cannot come before unassociated exts'); + Utils.assert(ext.after.indexOf(ext.group) === -1, 'Plugin ext cannot come after itself (' + ext.plugin + ')'); + Utils.assert(ext.after.indexOf('?') === -1, 'Plugin ext cannot come after unassociated exts'); + + // Insert event + + this._events[event].push(ext); + this.sort(event); +}; + + +internals.Ext.prototype.invoke = function (request, event, callback) { + + var handlers = this._events[event]; // onRequest, onPreHandler, onPostHandler + if (!handlers) { + return callback(); + } + + Async.forEachSeries(handlers, function (ext, next) { + + internals.Ext.runProtected(request.log.bind(request), event, next, function (run, protectedNext) { + + run(function () { + + ext.func(request, protectedNext); + }); + }); + }, + function (err) { + + return callback(err); + }); +}; + + +internals.Ext.runProtected = function (log, tags, callback, setup) { + + var domain = Domain.createDomain(); + + // Ensure only one callback returned + + var isFinished = false; + var finish = function () { + + if (isFinished) { + log(['duplicate', 'callback', 'error'].concat(tags || [])); + return; + } + + isFinished = true; + + domain.exit(); + return callback.apply(null, arguments); + }; + + setup(function (run) { + + domain.on('error', function (err) { + + domain.dispose(); + log(['uncaught'].concat(tags || []), err); + return finish(Boom.internal('Uncaught error', err)); + }); + + // Execute functon + + domain.enter(); + run(); + }, + finish); +}; + + +internals.Ext.prototype.sort = function (event) { + + var exts = this._events[event]; + if (!exts) { + return; + } + + // Sort + + var graph = internals.getGraph(exts); + var ancestors = internals.getAncestors(graph); + var sorted = internals.topoSort(ancestors, exts.length); + + var priorityIndex = {}; + exts.forEach(function (ext) { + + priorityIndex[ext.priority] = ext; + }); + + this._events[event] = sorted.map(function (value) { + + return priorityIndex[value]; + }); +}; + + +internals.getGraph = function (exts) { + + var groups = {}; + var graph = {}; + var graphAfters = {}; + + for (var i = 0, il = exts.length; i < il; ++i) { + var ext = exts[i]; + var priority = ext.priority; + var group = ext.group; + + // Determine Groups + + if (groups.hasOwnProperty(group)) { + if (groups[group].indexOf(priority) == -1) { + groups[group].push(priority); + } + } + else { + groups[group] = [priority]; + } + + // Build intermediary graph using 'before' + + var before = ext.before; + graph[priority] = (graph[priority] || []).concat(before); + + // Build second intermediary graph with 'after' + + var after = ext.after; + for (var j = 0, jl = after.length; j < jl; ++j) { + graphAfters[after[j]] = (graphAfters[after[j]] || []).concat(priority); + } + } + + // Expand intermediary graph + + Object.keys(graph).forEach(function (node) { + + var expandedGroups = []; + for (var groupIndex in graph[node]) { + var group = graph[node][groupIndex]; + groups[group] = groups[group] || []; + groups[group].forEach(function (d) { + expandedGroups.push(d); + }); + } + graph[node] = expandedGroups; + }); + + // Merge intermediary graph using graphAfters into final graph + + var afterNodes = Object.keys(graphAfters); + for (var n in afterNodes) { + var group = afterNodes[n]; + + for (var itemIndex in groups[group]) { + var node = groups[group][itemIndex]; + graph[node] = (graph[node] || []).concat(graphAfters[group]) + } + } + + return graph; +}; + + +internals.getAncestors = function (graph) { + + var ancestors = {}; + var graphNodes = Object.keys(graph); + for (var i in graphNodes) { + var node = graphNodes[i]; + var children = graph[node]; + + for (var j = 0, jl = children.length; j < jl; ++j) { + ancestors[children[j]] = (ancestors[children[j]] || []).concat(node); + } + } + + return ancestors; +}; + + +internals.topoSort = function (ancestorsGraph, length) { + + var visited = {}; + var sorted = []; + length = length || ancestorsGraph.length; + + var ancNodes = Object.keys(ancestorsGraph); + + for (var i = 0, il = length; i < il; ++i) { + var next = i; + + if (ancestorsGraph[i]) { + next = null; + for (var j = 0, jl = length; j < jl; ++j) { + if (visited[j] == true) { + continue; + } + + if (!ancestorsGraph[j]) { + ancestorsGraph[j] = []; + } + + var shouldSeeCount = ancestorsGraph[j].length; + var seenCount = 0; + for (var l = 0, ll = shouldSeeCount; l < ll; ++l) { + if (sorted.indexOf(ancestorsGraph[j][l]) >= 0) { + ++seenCount; + } + } + + if (seenCount == shouldSeeCount) { + next = j; + break; + } + } + } + + if (next !== null) { + next = next.toString(); // Normalize to string + visited[next] = true; + sorted.push(next); + } + } + + Utils.assert(sorted.length === length, 'Invalid ext dependencies detected'); + return sorted; +}; diff --git a/lib/pack.js b/lib/pack.js index b2742baef..d9db62fe4 100755 --- a/lib/pack.js +++ b/lib/pack.js @@ -112,6 +112,10 @@ internals.Pack.prototype.validate = function (plugin) { return new Error('Plugin missing name'); } + if (plugin.name === 'hapi') { + return new Error('Plugin name cannot be \'hapi\''); + } + if (!plugin.version) { return new Error('Plugin missing version'); } @@ -202,16 +206,16 @@ internals.Pack.prototype._register = function (plugin, permissions, options, cal } if (permissions.state) { - methods.state = function (name, options) { + methods.state = function () { - self._applySync(selection.servers, Server.prototype.state, [name, options]); + self._applySync(selection.servers, Server.prototype.state, arguments); }; } if (permissions.helper) { - methods.helper = function (name, method, options) { + methods.helper = function () { - self._applySync(selection.servers, Server.prototype.helper, [name, method, options]); + self._applySync(selection.servers, Server.prototype.helper, arguments); }; } @@ -220,9 +224,9 @@ internals.Pack.prototype._register = function (plugin, permissions, options, cal } if (permissions.ext) { - methods.ext = function (event, func) { + methods.ext = function () { - self._applySync(selection.servers, Server.prototype.ext, [event, func]); + self._applySync(selection.servers, Server.prototype._ext, [arguments[0], arguments[1], arguments[2], plugin.name]); }; } diff --git a/lib/request.js b/lib/request.js index aa408f477..1a0ae03ca 100755 --- a/lib/request.js +++ b/lib/request.js @@ -1,17 +1,17 @@ // Load modules -var Domain = require('domain'); var Stream = require('stream'); var Url = require('url'); var Async = require('async'); -var Utils = require('./utils'); var Boom = require('boom'); +var Utils = require('./utils'); var Payload = require('./payload'); var State = require('./state'); var Auth = require('./auth'); var Validation = require('./validation'); var Response = require('./response'); var Cached = require('./response/cached'); +var Ext = require('./ext'); // Declare internals @@ -200,32 +200,6 @@ internals.Request.prototype.getLog = function (tags) { }; -internals.Request.prototype._ext = function (event, callback) { - - var self = this; - - var handlers = this.server._ext[event]; // onRequest, onPreHandler, onPostHandler - if (!handlers) { - return callback(); - } - - Async.forEachSeries(handlers, function (func, next) { - - self._runProtected(event, next, function (run, protectedNext) { - - run(function () { - - func(self, protectedNext); - }); - }); - }, - function (err) { - - return callback(err); - }); -}; - - internals.Request.prototype._onRequestExt = function (callback) { var self = this; @@ -235,7 +209,7 @@ internals.Request.prototype._onRequestExt = function (callback) { this.setUrl = this._setUrl; this.setMethod = this._setMethod; - this._ext('onRequest', function (err) { + this.server._ext.invoke(this, 'onRequest', function (err) { // Undecorate request @@ -280,7 +254,13 @@ internals.Request.prototype._execute = function (route) { self._serverTimeoutId = setTimeout(timeoutReply, serverTimeout); } - var ext = function (event) { return function (request, next) { self._ext(event, next); }; }; // Handler wrappers + var ext = function (event) { + + return function (request, next) { + + self.server._ext.invoke(self, event, next); + }; + }; var funcs = [ // 'onRequest' in Server @@ -417,8 +397,8 @@ internals.Request.prototype._replyInterface = function (callback, withProperties process(); }; - if (!withProperties) { - return reply; + if (!withProperties) { + return reply; } reply.send = function () { @@ -560,16 +540,13 @@ internals.handler = function (request, next) { }); }; + var log = request.log.bind(request); + var lookup = function () { // Lookun in cache - var logFunc = function () { - - return request.log.apply(request, arguments); - }; - - request._route.cache.getOrGenerate(request.url.path, logFunc, generate, function (response, cached) { // request.url.path contains query + request._route.cache.getOrGenerate(request.url.path, log, generate, function (response, cached) { // request.url.path contains query if (cached && response instanceof Error === false) { @@ -591,8 +568,8 @@ internals.handler = function (request, next) { return callback(err); } - request._runProtected('handler', callback, function (run, next) { - + Ext.runProtected(log, 'handler', callback, function (run, next) { + var timer = new Utils.Timer(); var finalize = function (response) { @@ -600,38 +577,38 @@ internals.handler = function (request, next) { // Check for Error result if (response && - (response.isBoom || response.varieties.error)) { + (response.isBoom || response.varieties.error)) { request.log(['handler', 'result', 'error'], { msec: timer.elapsed() }); - return next(response); + return next(response); } - if (request._route.cache.rule.strict) { - Utils.assert(response.varieties.cacheable, 'Attempted to cache non-cacheable item'); + if (request._route.cache.rule.strict) { + Utils.assert(response.varieties.cacheable, 'Attempted to cache non-cacheable item'); } request.log(['handler', 'result'], { msec: timer.elapsed() }); - return next(null, response); + return next(null, response); }; // Execute handler run(function () { - switch (request.route.handler.length) { - case 0: // function: () this: request this.reply + properties - request.reply = request._replyInterface(finalize, true); + switch (request.route.handler.length) { + case 0: // function: () this: request this.reply + properties + request.reply = request._replyInterface(finalize, true); request.route.handler.call(request); - break; - case 1: // function: (request) this: null request.reply + properties - request.reply = request._replyInterface(finalize, true); + break; + case 1: // function: (request) this: null request.reply + properties + request.reply = request._replyInterface(finalize, true); request.route.handler.call(null, request); - break; - case 2: // function: (request, reply) this: null reply - default: + break; + case 2: // function: (request, reply) this: null reply + default: request.route.handler.call(null, request, request._replyInterface(finalize, false)); - break; - } + break; + } }); }); }); @@ -647,45 +624,6 @@ internals.handler = function (request, next) { }; -internals.Request.prototype._runProtected = function (tags, callback, setup) { - - var self = this; - - var domain = Domain.createDomain(); - - // Ensure only one callback returned - - var isFinished = false; - var finish = function () { - - if (isFinished) { - return; - } - - isFinished = true; - - domain.exit(); - return callback.apply(null, arguments); - }; - - setup(function (run) { - - domain.on('error', function (err) { - - domain.dispose(); - self.log(['uncaught'].concat(tags || []), err); - return finish(Boom.internal('Uncaught error', err)); - }); - - // Execute functon - - domain.enter(); - run(); - }, - finish); -}; - - internals.Request.prototype._addTail = function (name) { var self = this; diff --git a/lib/server.js b/lib/server.js index 8fab9f90b..7f9e871e6 100755 --- a/lib/server.js +++ b/lib/server.js @@ -12,6 +12,7 @@ var Request = require('./request'); var Router = require('./router'); var Schema = require('./schema'); var Views = require('./views'); +var Ext = require('./ext'); var Utils = require('./utils'); // Pack delayed required inline @@ -75,15 +76,7 @@ module.exports = internals.Server = function (/* host, port, options */) { // Extensions - this._ext = { - - // The following extension functions use the following signature: - // function (request, next) { next(); } - - onRequest: null, // New request, before handing over to the router (allows changes to the request method, url, etc.) - onPreHandler: null, // After validation and body parsing, before route handler - onPostHandler: null // After route handler returns, before sending response - }; + this._ext = new Ext(); // Set optional configuration // false -> null, true -> defaults, {} -> override defaults @@ -289,10 +282,15 @@ internals.Server.prototype._plugin = function () { // Register an extension function -internals.Server.prototype.ext = function (event, func) { +internals.Server.prototype.ext = function () { + + return this._ext.add.apply(this._ext, arguments); +}; + + +internals.Server.prototype._ext = function () { - Utils.assert(['onRequest', 'onPreHandler', 'onPostHandler'].indexOf(event) !== -1, 'Unknown event type: ' + event); - this._ext[event] = (this._ext[event] || []).concat(func); + return this._ext._add.apply(this._ext, arguments); }; diff --git a/test/integration/pack.js b/test/integration/pack.js index ade8910fe..e4017db95 100755 --- a/test/integration/pack.js +++ b/test/integration/pack.js @@ -39,7 +39,7 @@ describe('Pack', function () { var server3 = new Hapi.Server({ tls: {}, cache: 'memory' }); var server4 = new Hapi.Server({ cache: 'memory' }); - var pack = new Hapi.Pack({ a: 1 }); + var pack = new Hapi.Pack(); pack.server('s1', server1, { labels: ['a', 'b'] }); pack.server('s2', server2, { labels: ['a', 'c'] }); pack.server('s3', server3, { labels: ['a', 'b', 'd'] }); @@ -103,7 +103,7 @@ describe('Pack', function () { var server3 = new Hapi.Server({ tls: {}, cache: 'memory' }); var server4 = new Hapi.Server({ cache: 'memory' }); - var pack = new Hapi.Pack({ a: 1 }); + var pack = new Hapi.Pack(); pack.server('s1', server1, { labels: ['a', 'b'] }); pack.server('s2', server2, { labels: ['a', 'test'] }); pack.server('s3', server3, { labels: ['a', 'b', 'd'] }); @@ -196,7 +196,7 @@ describe('Pack', function () { it('fails to require missing module', function (done) { var server1 = new Hapi.Server(); - var pack = new Hapi.Pack({ a: 1 }); + var pack = new Hapi.Pack(); pack.server('s1', server1, { labels: ['a', 'b'] }); pack.allow({}).require('./pack/none', function (err) { @@ -210,7 +210,7 @@ describe('Pack', function () { it('fails to require missing module in default route', function (done) { var server1 = new Hapi.Server(); - var pack = new Hapi.Pack({ a: 1 }); + var pack = new Hapi.Pack(); pack.server('s1', server1, { labels: ['a', 'b'] }); pack.require('none', function (err) { @@ -228,7 +228,7 @@ describe('Pack', function () { var server3 = new Hapi.Server(0, { tls: {}, cache: 'memory' }); var server4 = new Hapi.Server(0, { cache: 'memory' }); - var pack = new Hapi.Pack({ a: 1 }); + var pack = new Hapi.Pack(); pack.server('s1', server1, { labels: ['a', 'b'] }); pack.server('s2', server2, { labels: ['a', 'test'] }); pack.server('s3', server3, { labels: ['a', 'b', 'd'] }); @@ -255,7 +255,7 @@ describe('Pack', function () { it('fails to register a bad plugin', function (done) { - var pack = new Hapi.Pack({ a: 1 }); + var pack = new Hapi.Pack(); pack.register({ version: '0.0.0', register: function (pack, options, next) { next(); } }, function (err) { expect(err).to.exist; @@ -266,7 +266,7 @@ describe('Pack', function () { it('invalidates missing name', function (done) { - var pack = new Hapi.Pack({ a: 1 }); + var pack = new Hapi.Pack(); var err = pack.validate({ version: '0.0.0', register: function (pack, options, next) { next(); } }); expect(err).to.exist; @@ -274,9 +274,19 @@ describe('Pack', function () { done(); }); + it('invalidates bad name', function (done) { + + var pack = new Hapi.Pack(); + var err = pack.validate({ name: 'hapi', version: '0.0.0', register: function (pack, options, next) { next(); } }); + + expect(err).to.exist; + expect(err.message).to.equal('Plugin name cannot be \'hapi\''); + done(); + }); + it('invalidates missing version', function (done) { - var pack = new Hapi.Pack({ a: 1 }); + var pack = new Hapi.Pack(); var err = pack.validate({ name: 'test', register: function (pack, options, next) { next(); } }); expect(err).to.exist; @@ -286,7 +296,7 @@ describe('Pack', function () { it('invalidates missing register method', function (done) { - var pack = new Hapi.Pack({ a: 1 }); + var pack = new Hapi.Pack(); var err = pack.validate({ name: 'test', version: '0.0.0' }); expect(err).to.exist; @@ -325,4 +335,46 @@ describe('Pack', function () { }); }); }); + + it('adds multiple ext functions', function (done) { + + var server1 = new Hapi.Server(); + var server2 = new Hapi.Server(); + var server3 = new Hapi.Server(); + + var pack = new Hapi.Pack(); + pack.server('s1', server1, { labels: ['a', 'b'] }); + pack.server('s2', server2, { labels: ['a', 'c'] }); + pack.server('s3', server3, { labels: ['c', 'b'] }); + + var handler = function () { + + return this.reply(this.plugins.deps); + }; + + server1.route({ method: 'GET', path: '/', handler: handler }); + server2.route({ method: 'GET', path: '/', handler: handler }); + server3.route({ method: 'GET', path: '/', handler: handler }); + + pack.allow({ ext: true }).require(['./pack/--deps1', './pack/--deps2', './pack/--deps3'], function (err) { + + expect(err).to.not.exist; + + server1.inject({ method: 'GET', url: '/' }, function (res) { + + expect(res.result).to.equal('|1|2|') + + server2.inject({ method: 'GET', url: '/' }, function (res) { + + expect(res.result).to.equal('|1|3|') + + server3.inject({ method: 'GET', url: '/' }, function (res) { + + expect(res.result).to.equal('|2|3|') + done(); + }); + }); + }); + }); + }); }); diff --git a/test/integration/pack/--deps1/index.js b/test/integration/pack/--deps1/index.js new file mode 100755 index 000000000..76c02f126 --- /dev/null +++ b/test/integration/pack/--deps1/index.js @@ -0,0 +1,18 @@ +// Declare internals + +var internals = {}; + + +// Plugin registration + +exports.register = function (pack, options, next) { + + pack.select({ label: 'a' }).ext('onRequest', function (request, cont) { + + request.plugins.deps = request.plugins.deps || '|'; + request.plugins.deps += '1|' + cont(); + }); + + return next(); +}; diff --git a/test/integration/pack/--deps1/package.json b/test/integration/pack/--deps1/package.json new file mode 100755 index 000000000..c0bb52ba8 --- /dev/null +++ b/test/integration/pack/--deps1/package.json @@ -0,0 +1,7 @@ +{ + "name": "--deps1", + "description": "Test plugin module", + "version": "0.0.1", + "private": true, + "main": "index" +} diff --git a/test/integration/pack/--deps2/index.js b/test/integration/pack/--deps2/index.js new file mode 100755 index 000000000..1b31f2903 --- /dev/null +++ b/test/integration/pack/--deps2/index.js @@ -0,0 +1,18 @@ +// Declare internals + +var internals = {}; + + +// Plugin registration + +exports.register = function (pack, options, next) { + + pack.select({ label: 'b' }).ext('onRequest', function (request, cont) { + + request.plugins.deps = request.plugins.deps || '|'; + request.plugins.deps += '2|' + cont(); + }); + + return next(); +}; diff --git a/test/integration/pack/--deps2/package.json b/test/integration/pack/--deps2/package.json new file mode 100755 index 000000000..ccf604f9e --- /dev/null +++ b/test/integration/pack/--deps2/package.json @@ -0,0 +1,7 @@ +{ + "name": "--deps2", + "description": "Test plugin module", + "version": "0.0.1", + "private": true, + "main": "index" +} diff --git a/test/integration/pack/--deps3/index.js b/test/integration/pack/--deps3/index.js new file mode 100755 index 000000000..a2c6785c6 --- /dev/null +++ b/test/integration/pack/--deps3/index.js @@ -0,0 +1,18 @@ +// Declare internals + +var internals = {}; + + +// Plugin registration + +exports.register = function (pack, options, next) { + + pack.select({ label: 'c' }).ext('onRequest', function (request, cont) { + + request.plugins.deps = request.plugins.deps || '|'; + request.plugins.deps += '3|' + cont(); + }); + + return next(); +}; diff --git a/test/integration/pack/--deps3/package.json b/test/integration/pack/--deps3/package.json new file mode 100755 index 000000000..26e3fb386 --- /dev/null +++ b/test/integration/pack/--deps3/package.json @@ -0,0 +1,7 @@ +{ + "name": "--deps3", + "description": "Test plugin module", + "version": "0.0.1", + "private": true, + "main": "index" +} diff --git a/test/unit/ext.js b/test/unit/ext.js new file mode 100755 index 000000000..a31f0c2cb --- /dev/null +++ b/test/unit/ext.js @@ -0,0 +1,137 @@ +// Load modules + +var Lab = require('lab'); +var Hapi = require('../..'); +var Ext = require('../../lib/ext'); + + +// Declare internals + +var internals = {}; + + +// Test shortcuts + +var expect = Lab.expect; +var before = Lab.before; +var after = Lab.after; +var describe = Lab.experiment; +var it = Lab.test; + + +describe('Ext', function () { + + describe('#sort', function () { + + it('skips when no exts added', function (done) { + + var ext = new Ext(); + ext.sort('onRequest'); + expect(ext._events.onRequest).to.equal(null); + done(); + }); + + var testDeps = function (scenario, callback) { + + var generateExt = function (value) { + + return function (request, next) { + + request.x = request.x || ''; + request.x += value; + next(); + }; + }; + + var ext = new Ext(); + scenario.forEach(function (record, i) { + + ext._add('onRequest', generateExt(record.id), { before: record.before, after: record.after }, record.group); + }); + + var request = { + log: function () { } + }; + + ext.invoke(request, 'onRequest', function (err) { + + expect(err).to.not.exist; + callback(request.x); + }); + }; + + it('sorts dependencies (1)', function (done) { + + var scenario = [ + { id: '0', before: 'a' }, + { id: '1', after: 'f', group: 'a' }, + { id: '2', before: 'a' }, + { id: '3', before: ['b', 'c'], group: 'a' }, + { id: '4', after: 'c', group: 'b' }, + { id: '5', group: 'c' }, + { id: '6', group: 'd' }, + { id: '7', group: 'e' }, + { id: '8', before: 'd' }, + { id: '9', after: 'c', group: 'a' } + ]; + + testDeps(scenario, function (result) { + + expect(result).to.equal('0213547869'); + done(); + }); + }); + + it('sorts dependencies (explicit)', function (done) { + + var set = '0123456789abcdefghijklmnopqrstuvwxyz'; + var array = set.split(''); + + var scenario = []; + for (var i = 0, il = array.length; i < il; ++i) { + var item = { + id: array[i], + group: array[i], + after: i ? array.slice(0, i) : [], + before: array.slice(i + 1) + }; + scenario.push(item); + } + + var fisherYates = function (array) { + + var i = array.length; + while (--i) { + var j = Math.floor(Math.random() * (i + 1)); + var tempi = array[i]; + var tempj = array[j]; + array[i] = tempj; + array[j] = tempi; + } + }; + + fisherYates(scenario); + testDeps(scenario, function (result) { + + expect(result).to.equal(set); + done(); + }); + }); + + it('throws on circular dependency', function (done) { + + var scenario = [ + { id: '0', before: 'a', group: 'b'}, + { id: '1', before: 'c', group: 'a' }, + { id: '2', before: 'b', group: 'c' } + ]; + + expect(function () { + + testDeps(scenario, function (result) { }); + }).to.throw(); + + done(); + }); + }); +}); \ No newline at end of file