Creating the basics for the new server... tests coming

This commit is contained in:
Chris Cowan 2015-04-09 12:25:29 -07:00
parent dd9ff1852f
commit bba50454f2
26 changed files with 903 additions and 1 deletions

View file

@ -30,7 +30,7 @@
],
"scripts": {
"test": "grunt test",
"start": "node ./src/server/bin/kibana.js",
"start": "node ./src/hapi/index.js",
"server": "node ./src/server/bin/kibana.js",
"precommit": "grunt lintStagedFiles"
},
@ -50,12 +50,20 @@
"elasticsearch": "^3.1.1",
"express": "~4.10.6",
"glob": "^4.3.2",
"good": "^5.1.2",
"good-console": "^4.1.0",
"good-file": "^4.0.2",
"good-reporter": "^3.0.1",
"hapi": "^8.4.0",
"http-auth": "^2.2.5",
"jade": "~1.8.2",
"js-yaml": "^3.2.5",
"json-stringify-safe": "^5.0.0",
"less-middleware": "1.0.x",
"lodash": "^2.4.1",
"moment": "^2.9.0",
"morgan": "~1.5.1",
"numeral": "^1.5.3",
"request": "^2.40.0",
"requirefrom": "^0.2.0",
"semver": "^4.2.0",

53
src/hapi/config/index.js Normal file
View file

@ -0,0 +1,53 @@
var _ = require('lodash');
var fs = require('fs');
var yaml = require('js-yaml');
var path = require('path');
var listPlugins = require('../lib/list_plugins');
var configPath = process.env.CONFIG_PATH || path.join(__dirname, 'kibana.yml');
var kibana = yaml.safeLoad(fs.readFileSync(configPath, 'utf8'));
var env = process.env.NODE_ENV || 'development';
function checkPath(path) {
try {
fs.statSync(path);
return true;
} catch (err) {
return false;
}
}
// Check if the local public folder is present. This means we are running in
// the NPM module. If it's not there then we are running in the git root.
var public_folder = path.resolve(__dirname, '..', 'public');
if (!checkPath(public_folder)) public_folder = path.resolve(__dirname, '..', '..', 'kibana');
// Check to see if htpasswd file exists in the root directory otherwise set it to false
var htpasswdPath = path.resolve(__dirname, '..', '..', '.htpasswd');
if (!checkPath(htpasswdPath)) htpasswdPath = path.resolve(__dirname, '..', '..', '..', '..', '.htpasswd');
if (!checkPath(htpasswdPath)) htpasswdPath = false;
var packagePath = path.resolve(__dirname, '..', 'package.json');
try {
fs.statSync(packagePath);
} catch (err) {
packagePath = path.resolve(__dirname, '..', '..', '..', 'package.json');
}
var config = module.exports = {
port : kibana.port || 5601,
host : kibana.host || '0.0.0.0',
elasticsearch : kibana.elasticsearch_url || 'http : //localhost : 9200',
root : path.normalize(path.join(__dirname, '..')),
quiet : false,
public_folder : public_folder,
external_plugins_folder : process.env.PLUGINS_FOLDER || null,
bundled_plugins_folder : path.resolve(public_folder, 'plugins'),
kibana : kibana,
package : require(packagePath),
htpasswd : htpasswdPath,
buildNum : '@@buildNum',
maxSockets : kibana.maxSockets || Infinity,
log_file : kibana.log_file || null
};
config.plugins = listPlugins(config);

View file

@ -0,0 +1,54 @@
# Kibana is served by a back end server. This controls which port to use.
port: 5601
# The host to bind the server to.
host: "0.0.0.0"
# The Elasticsearch instance to use for all your queries.
elasticsearch_url: "http://localhost:9200"
# preserve_elasticsearch_host true will send the hostname specified in `elasticsearch`. If you set it to false,
# then the host you use to connect to *this* Kibana instance will be sent.
elasticsearch_preserve_host: true
# Kibana uses an index in Elasticsearch to store saved searches, visualizations
# and dashboards. It will create a new index if it doesn't already exist.
kibana_index: ".kibana"
# If your Elasticsearch is protected with basic auth, this is the user credentials
# used by the Kibana server to perform maintence on the kibana_index at statup. Your Kibana
# users will still need to authenticate with Elasticsearch (which is proxied thorugh
# the Kibana server)
# kibana_elasticsearch_username: user
# kibana_elasticsearch_password: pass
# The default application to load.
default_app_id: "discover"
# Time in milliseconds to wait for responses from the back end or elasticsearch.
# This must be > 0
request_timeout: 300000
# Time in milliseconds for Elasticsearch to wait for responses from shards.
# Set to 0 to disable.
shard_timeout: 0
# Set to false to have a complete disregard for the validity of the SSL
# certificate.
verify_ssl: true
# If you need to provide a CA certificate for your Elasticsarech instance, put
# the path of the pem file here.
# ca: /path/to/your/CA.pem
# SSL for outgoing requests from the Kibana Server (PEM formatted)
# ssl_key_file: /path/to/your/server.key
# ssl_cert_file: /path/to/your/server.crt
# Set the path to where you would like the process id file to be created.
# pid_file: /var/run/kibana.pid
# If you would like to send the log output to a file you can set the path below.
# This will also turn off the STDOUT log output.
# log_file: ./kibana.log

9
src/hapi/index.js Normal file
View file

@ -0,0 +1,9 @@
module.exports.extendHapi = require('./lib/extend_hapi');
module.exports.Plugin = require('./lib/plugin');
module.exports.start = require('./lib/start');
if (require.main === module) {
module.exports.start().catch(function (err) {
process.exit(1);
});
}

View file

@ -0,0 +1,18 @@
var _ = require('lodash');
var checkDependencies = module.exports = function (name, deps, callStack) {
if (!deps[name]) throw new Error('Missing dependency: ' + name);
callStack = callStack || [];
if (_.contains(callStack, name)) {
callStack.push(name);
throw new Error('Circular dependency: ' + callStack.join(' -> '));
}
for (var i = 0; i < deps[name].length; i++) {
var task = deps[name][i];
if (!deps[task]) throw new Error('Missing dependency: ' + task);
if (deps[task].length) {
checkDependencies(task, deps, callStack.concat(name));
}
}
return true;
};

3
src/hapi/lib/config.js Normal file
View file

@ -0,0 +1,3 @@
module.exports = function () {
return require('../config');
};

View file

@ -0,0 +1,4 @@
module.exports = function (server) {
server.decorate('server', 'config', require('./config'));
server.decorate('server', 'loadKibanaPlugins', require('./load_kibana_plugins'));
};

View file

@ -0,0 +1,128 @@
var moment = require('moment');
var _ = require('lodash');
var env = process.env.NODE_ENV || 'development';
var numeral = require('numeral');
var ansicolors = require('ansicolors');
var stringify = require('json-stringify-safe');
var querystring = require('querystring');
function serializeError(err) {
return {
message: err.message,
name: err.name,
stack: err.stack,
code: err.code,
signal: err.signal
};
}
var levelColor = function (code) {
if (code < 299) {
return ansicolors.green(code);
}
if (code < 399) {
return ansicolors.yellow(code);
}
if (code < 499) {
return ansicolors.magenta(code);
}
return ansicolors.red(code);
};
function lookup(name) {
switch (name) {
case 'error':
return 'error';
default:
return 'info';
}
}
module.exports = function (name, event) {
var data = {
'@timestamp': moment.utc(event.timestamp).format(),
level: lookup(event),
node_env: env,
tags: event.tags,
pid: event.pid
};
if (name === 'response') {
_.defaults(data, _.pick(event, [
'method',
'statusCode'
]));
data.req = {
url: event.path,
method: event.method,
headers: event.headers,
remoteAddress: event.source.remoteAddress,
userAgent: event.source.remoteAddress,
referer: event.source.referer
};
var contentLength = 0;
if (typeof event.responsePayload === 'object') {
contentLength = stringify(event.responsePayload).length;
} else {
contentLength = event.responsePayload.toString().length;
}
data.res = {
statusCode: event.statusCode,
responseTime: event.responseTime,
contentLength: contentLength
};
var query = querystring.stringify(event.query);
if (query) data.req.url += '?' + query;
data.message = data.req.method.toUpperCase() + ' ';
data.message += data.req.url;
data.message += ' ';
data.message += levelColor(data.res.statusCode);
data.message += ' ';
data.message += ansicolors.brightBlack(data.res.responseTime + 'ms');
data.message += ansicolors.brightBlack(' - ' + numeral(contentLength).format('0.0b'));
}
else if (name === 'ops') {
_.defaults(data, _.pick(event, [
'pid',
'os',
'proc',
'load'
]));
data.message = ansicolors.brightBlack('memory: ');
data.message += numeral(data.proc.mem.heapUsed).format('0.0b');
data.message += ' ';
data.message += ansicolors.brightBlack('uptime: ');
data.message += numeral(data.proc.uptime).format('00:00:00');
data.message += ' ';
data.message += ansicolors.brightBlack('load: [');
data.message += data.os.load.map(function (val) {
return numeral(val).format('0.00');
}).join(' ');
data.message += ansicolors.brightBlack(']');
data.message += ' ';
data.message += ansicolors.brightBlack('delay: ');
data.message += numeral(data.proc.delay).format('0.000');
}
else if (name === 'error') {
data.level = 'error';
data.message = event.error.message;
data.error = serializeError(event.error);
data.url = event.url;
}
else {
if (event.data instanceof Error) {
data.level = _.contains(event.tags, 'fatal') ? 'fatal' : 'error';
data.message = event.data.message;
data.error = serializeError(event.data);
} else {
data.message = event.data;
}
}
return data;
};

View file

@ -0,0 +1,58 @@
var ansicolors = require('ansicolors');
var eventToJson = require('./_event_to_json');
var GoodReporter = require('good-reporter');
var util = require('util');
var moment = require('moment');
var stringify = require('json-stringify-safe');
var querystring = require('querystring');
var numeral = require('numeral');
var colors = {
log: 'blue',
req: 'green',
res: 'green',
ops: 'cyan',
err: 'red',
info: 'blue',
error: 'red',
fatal: 'magenta'
};
function stripColors(string) {
return string.replace(/\u001b[^m]+m/g, '');
}
var Console = module.exports = function (events, options) {
this._json = options.json;
GoodReporter.call(this, events);
};
util.inherits(Console, GoodReporter);
Console.prototype.stop = function () { };
Console.prototype._report = function (name, data) {
data = eventToJson(name, data);
var nameCrayon = ansicolors[colors[name.substr(0, 3)]];
var typeCrayon = ansicolors[colors[data.level]];
var output;
if (this._json) {
data.message = stripColors(data.message);
output = stringify(data);
} else {
output = nameCrayon(name.substr(0, 3));
output += ': ';
output += typeCrayon(data.level.toUpperCase());
output += ' ';
output += '[ ';
output += ansicolors.brightBlack(moment(data.timestamp).format());
output += ' ] ';
if (data.error) {
output += ansicolors.red(data.error.stack);
} else {
output += data.message;
}
}
console.log(output);
};

View file

View file

@ -0,0 +1,19 @@
var _ = require('lodash');
var glob = require('glob');
var path = require('path');
var plugins = function (dir) {
if (!dir) return [];
var files = glob.sync(path.join(dir, '*', 'index.js')) || [];
return files.map(function (file) {
return file.replace(dir, 'plugins').replace(/\.js$/, '');
});
};
module.exports = function (config) {
var bundled_plugin_ids = config.kibana.bundled_plugin_ids || [];
var bundled_plugins = plugins(config.bundled_plugins_folder);
var external_plugins = plugins(config.external_plugins_folder);
return bundled_plugin_ids.concat(bundled_plugins, external_plugins);
};

View file

@ -0,0 +1,11 @@
var _ = require('lodash');
var Promise = require('bluebird');
var registerPlugins = require('./register_plugins');
var requirePlugins = require('./require_plugins');
var setupLogging = require('./setup_logging');
module.exports = function (externalPlugins) {
var plugins = requirePlugins().concat(externalPlugins);
return setupLogging(this).then(function (server) {
registerPlugins(server, plugins);
});
};

13
src/hapi/lib/plugin.js Normal file
View file

@ -0,0 +1,13 @@
var _ = require('lodash');
var Promise = require('bluebird');
function Plugin(options) {
options = _.defaults(options, {
require: [],
init: function (server, options) {
return Promise.reject(new Error('You must override the init function for plugins'));
}
});
_.assign(this, options);
}
module.exports = Plugin;

View file

@ -0,0 +1,71 @@
var _ = require('lodash');
var Promise = require('bluebird');
var checkDependencies = require('./check_dependencies');
function checkForCircularDependency(tasks) {
var deps = {};
tasks.forEach(function (task) {
deps[task.name] = [];
if (task.require) deps[task.name] = task.require;
});
return _(deps).keys().map(function (task) {
return checkDependencies(task, deps);
}).every(function (result) {
return result;
});
}
module.exports = function (server, plugins) {
var total = plugins.length;
var results = {};
var running = {};
var finished = false;
var todo = plugins.concat();
function allDone(tasks) {
var done = _.keys(results);
return tasks.every(function (dep) {
return _.contains(done, dep);
});
}
function registerPlugin(plugin) {
var config = server.config();
return new Promise(function (resolve, reject) {
var register = function (server, options, next) {
Promise.try(plugin.init, [server, options]).nodeify(next);
};
register.attributes = { name: plugin.name };
var options = config[plugin.name] || {};
server.register({ register: register, options: options }, function (err) {
if (err) return reject(err);
resolve();
});
});
}
return new Promise(function (resolve, reject) {
// Check to see if we have a circular dependency
if (checkForCircularDependency(plugins)) {
(function runPending() {
plugins.forEach(function (plugin) {
// The running tasks are the same length as the results then we are
// done with all the plugin initalization tasks
if (_.keys(results).length === total) return resolve(results);
// If the current plugin is done or running the continue to the next one
if (results[plugin.name] || running[plugin.name]) return;
// If the current plugin doesn't have dependencies or all the dependencies
// are fullfilled then try running the plugin.
if (!plugin.require || (plugin.require && allDone(plugin.require))) {
running[plugin.name] = true;
registerPlugin(plugin)
.then(function () {
results[plugin.name] = true;
runPending();
}).catch(reject);
}
});
})();
}
});
};

View file

@ -0,0 +1,17 @@
var path = require('path');
var join = path.join;
var glob = require('glob');
var Promise = require('bluebird');
module.exports = function (globPath) {
globPath = globPath || join( __dirname, '..', 'plugins', '*', 'index.js');
return glob.sync(globPath).map(function (file) {
var module = require(file);
var regex = new RegExp('([^' + path.sep + ']+)' + path.sep + 'index.js');
var matches = file.match(regex);
if (!module.name && matches) {
module.name = matches[1];
}
return module;
});
};

View file

@ -0,0 +1,6 @@
var Promise = require('bluebird');
module.exports = function (server, tasks) {
return Promise.each(tasks, function (task) {
return task(server);
});
};

View file

@ -0,0 +1,29 @@
var Promise = require('bluebird');
var good = require('good');
var path = require('path');
var join = path.join;
var Console = require('./good_reporters/console');
var reporters = [
{
reporter: Console,
args: [{ ops: '*', log: '*', response: '*', error: '*' }, { json: false }]
}
];
module.exports = function (server) {
return new Promise(function (resolve, reject) {
server.register({
register: good,
options: {
opsInterval: 5000,
logRequestHeaders: true,
logResponsePayload: true,
reporters: reporters
}
}, function (err) {
if (err) return reject(err);
resolve(server);
});
});
};

46
src/hapi/lib/start.js Normal file
View file

@ -0,0 +1,46 @@
var Promise = require('bluebird');
var Hapi = require('hapi');
var requirePlugins = require('./require_plugins');
var validatePlugin = require('./validate_plugin');
var extendHapi = require('./extend_hapi');
module.exports = function (plugins) {
// Plugin authors can use this to add plugins durring development
plugins = plugins || [];
if (plugins.length && !plugins.every(validatePlugin)) {
return Promise.reject(new Error('Plugins must have a name attribute.'));
}
// Initalize the Hapi server
var server = new Hapi.Server();
// Extend Hapi with Kibana
extendHapi(server);
// Create a new connection
server.connection({ host: server.config().host, port: server.config().port });
// Load external plugins
var externalPlugins = [];
if (server.config().external_plugins_folder) {
externalPlugins = requirePlugins(server.config().external_plugins_folder);
}
// Load the plugins
return server.loadKibanaPlugins(externalPlugins.concat(plugins))
.then(function () {
// Start the server
return new Promise(function (resolve, reject) {
server.start(function (err) {
if (err) return reject(err);
server.log('server', 'Server running at ' + server.info.uri);
resolve(server);
});
});
})
.catch(function (err) {
server.log('fatal', err);
return Promise.reject(err);
});
};

View file

@ -0,0 +1,3 @@
module.exports = function (plugin) {
return !!plugin.name;
};

View file

@ -0,0 +1,25 @@
var _ = require('lodash');
var Promise = require('bluebird');
var kibana = require('../../');
module.exports = new kibana.Plugin({
init: function (server, options) {
server.route({
method: 'GET',
path: '/config',
handler: function (request, reply) {
var config = server.config();
var keys = [
'kibana_index',
'default_app_id',
'shard_timeout'
];
var data = _.pick(config.kibana, keys);
data.plugins = config.plugins;
reply(data);
}
});
}
});

View file

@ -0,0 +1,53 @@
var url = require('url');
var http = require('http');
var fs = require('fs');
var querystring = require('querystring');
var kibana = require('../../');
module.exports = new kibana.Plugin({
require: ['status'],
init: function (server, options) {
var config = server.config();
var target = url.parse(config.elasticsearch);
var agentOptions = {
rejectUnauthorized: config.kibana.verify_ssl
};
var customCA;
if (/^https/.test(target.protocol) && config.kibana.ca) {
customCA = fs.readFileSync(config.kibana.ca, 'utf8');
agentOptions.ca = [customCA];
}
// Add client certificate and key if required by elasticsearch
if (/^https/.test(target.protocol) &&
config.kibana.kibana_elasticsearch_client_crt &&
config.kibana.kibana_elasticsearch_client_key) {
agentOptions.crt = fs.readFileSync(config.kibana.kibana_elasticsearch_client_crt, 'utf8');
agentOptions.key = fs.readFileSync(config.kibana.kibana_elasticsearch_client_key, 'utf8');
}
server.route({
method: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS'],
path: '/elasticsearch/{path*}',
handler: {
proxy: {
mapUri: function (request, callback) {
var url = config.elasticsearch;
if (!/\/$/.test(url)) url += '/';
if (request.params.path) url += request.params.path;
var query = querystring.stringify(request.query);
if (query) url += '?' + query;
callback(null, url);
},
passThrough: true,
agent: new http.Agent(agentOptions)
}
}
});
}
});

View file

@ -0,0 +1,16 @@
var kibana = require('../../');
module.exports = new kibana.Plugin({
init: function (server, options) {
var config = server.config();
server.route({
method: 'GET',
path: '/{param*}',
handler: {
directory: {
path: config.public_folder
}
}
});
}
});

View file

@ -0,0 +1,77 @@
var join = require('path').join;
var kibana = require('../../');
function Series(size) {
this.size = size;
this.data = [];
}
Series.prototype.push = function (value) {
this.data.push([Date.now(), value]);
if (this.data.length > this.size) this.data.shift();
};
Series.prototype.toJSON = function () {
return this.data;
};
module.exports = new kibana.Plugin({
init: function (server, options) {
var config = server.config();
var fiveMinuteData = {
rss: new Series(60),
heapTotal: new Series(60),
heapUsed: new Series(60),
load: new Series(60),
delay: new Series(60),
concurrency: new Series(60),
responseTimeAvg: new Series(60),
responseTimeMax: new Series(60),
requests: new Series(60),
};
server.plugins.good.monitor.on('ops', function (event) {
var port = String(config.port);
fiveMinuteData.rss.push(event.psmem.rss);
fiveMinuteData.heapTotal.push(event.psmem.heapTotal);
fiveMinuteData.heapUsed.push(event.psmem.heapUsed);
fiveMinuteData.load.push(event.osload);
fiveMinuteData.delay.push(event.psdelay);
fiveMinuteData.concurrency.push(parseInt(event.concurrents[port], 0));
if (event.responseTimes[port]) {
var responseTimeAvg = event.responseTimes[port].avg;
if (isNaN(responseTimeAvg)) responseTimeAvg = 0;
fiveMinuteData.responseTimeAvg.push(responseTimeAvg);
fiveMinuteData.responseTimeMax.push(event.responseTimes[port].max);
} else {
fiveMinuteData.responseTimeAvg.push(0);
fiveMinuteData.responseTimeMax.push(0);
}
if (event.requests[port]) {
fiveMinuteData.requests.push(event.requests[port].total);
} else {
fiveMinuteData.requests.push(0);
}
});
server.route({
method: 'GET',
path: '/status/{param*}',
handler: {
directory: {
path: join(__dirname, 'public')
}
}
});
server.route({
method: 'GET',
path: '/status/health',
handler: function (request, reply) {
return reply({
metrics: fiveMinuteData
});
}
});
}
});

View file

@ -0,0 +1,9 @@
<html>
<head>
<title>Kibana Status</title>
</head>
<body>
<h1>Kibana Status Page</h1>
<p>Statusy stuff goes here... it's goign to be totally awesome!</p>
</body>
</html>

View file

@ -0,0 +1,76 @@
var checkDependencies = require('../../../../src/hapi/lib/check_dependencies');
var expect = require('expect.js');
describe('src/server/lib/check_dependencies', function () {
it('should return true for first -> second -> third', function () {
var deps = {
first: [],
second: ['first'],
third: ['second']
};
var results = checkDependencies('first', deps);
expect(results).to.be(true);
});
it('should throw an error for first -> third -> second -> first', function () {
var deps = {
first: ['third'],
second: ['first'],
third: ['second']
};
var run = function () {
checkDependencies('first', deps);
};
expect(run).to.throwException(function (e) {
expect(e.message).to.be('Circular dependency: first -> third -> second -> first');
});
});
it('should throw an error for first -> missing', function () {
var deps = {
first: ['missing']
};
var run = function () {
checkDependencies('first', deps);
};
expect(run).to.throwException(function (e) {
expect(e.message).to.be('Missing dependency: missing');
});
});
it('should throw an error for missing dependency', function () {
var deps = {
first: ['missing']
};
var run = function () {
checkDependencies('missing', deps);
};
expect(run).to.throwException(function (e) {
expect(e.message).to.be('Missing dependency: missing');
});
});
it('should throw an error on complex circulars', function () {
var deps = {
first: ['second', 'fifth'],
second: ['fourth'],
third: [],
fourth: ['third'],
fifth: ['sixth'],
sixth: ['first']
};
var run = function () {
checkDependencies('first', deps);
};
expect(run).to.throwException(function (e) {
expect(e.message).to.be('Circular dependency: first -> fifth -> sixth -> first');
});
});
});

View file

@ -0,0 +1,96 @@
var expect = require('expect.js');
var sinon = require('sinon');
var registerPlugins = require('../../../../src/hapi/lib/register_plugins');
var Promise = require('bluebird');
describe('server/lib/register_plugins', function () {
describe('registerPlugins() wrapper', function () {
it('should pass server, options and next to the init function', function () {
var options = { foo: 'bar' };
var server = { register: sinon.stub() };
var next = function (err) {
server.register.args[0][1](err);
};
server.register.yieldsTo('register', [server, options, next]);
var plugin = { name: 'first', init: sinon.stub().yields() };
var plugins = [plugin];
return registerPlugins(server, plugins).then(function () {
expect(plugin.init.args[0][0]).to.equal(server);
expect(plugin.init.args[0][1]).to.equal(options);
expect(plugin.init.args[0][2]).to.equal(next);
});
});
it('should call next() when plugin.init completes', function () {
var called = false;
var options = { foo: 'bar' };
var server = { register: sinon.stub() };
var next = function (err) {
called = true;
server.register.args[0][1](err);
};
server.register.yieldsTo('register', [server, options, next]);
var plugin = { name: 'first', init: sinon.stub().yields() };
var plugins = [plugin];
return registerPlugins(server, plugins).then(function () {
expect(called).to.be(true);
});
});
});
describe('dependencies', function () {
var server, nextStub;
beforeEach(function () {
server = { register: sinon.stub() };
var count = 0;
var next = function (err) {
server.register.args[count++][1](err);
};
server.register.yieldsTo('register', [server, {}, next]);
});
it('should run second after first and third and third after first', function () {
var first = { name: 'first', init: sinon.stub().yields() };
var second = { name: 'second', require: ['first', 'third'], init: sinon.stub().yields() };
var third = { name: 'third', require: ['first'], init: sinon.stub().yields() };
var plugins = [second, first, third];
return registerPlugins(server, plugins).then(function () {
expect(second.init.calledAfter(first.init)).to.be(true);
expect(second.init.calledAfter(third.init)).to.be(true);
expect(third.init.calledAfter(first.init)).to.be(true);
sinon.assert.calledThrice(server.register);
});
});
it('should run first, second, third', function () {
var first = { name: 'first', init: sinon.stub().yields() };
var second = { name: 'second', require: ['first'], init: sinon.stub().yields() };
var third = { name: 'third', require: ['second'], init: sinon.stub().yields() };
var plugins = [second, first, third];
return registerPlugins(server, plugins).then(function () {
sinon.assert.calledOnce(first.init);
expect(second.init.calledAfter(first.init)).to.be(true);
expect(third.init.calledAfter(second.init)).to.be(true);
sinon.assert.calledThrice(server.register);
});
});
it('should detect circular dependencies', function (done) {
var first = { name: 'first', require: ['third'], init: sinon.stub() };
var second = { name: 'second', require: ['first'], init: sinon.stub() };
var third = { name: 'third', require: ['second'], init: sinon.stub() };
var plugins = [second, first, third];
registerPlugins(server, plugins).catch(function (err) {
expect(err).to.be.a(Error);
expect(err.message).to.be('Circular dependency: second -> first -> third -> second');
done();
});
});
}); // end dependencies tests
});