flipr-etcd
Version:
Flipr source to retrieve config from etcd: a distributed, consistent, partition-tolerant key/value store
223 lines (221 loc) • 6.72 kB
JavaScript
;
var chai = require('chai');
var expect = chai.expect;
var sinon = require('sinon');
chai.use(require('sinon-chai'));
var proxyquire = require('proxyquire');
var NodeEtcdFixture = require('./fixture/node-etcd');
var eventsFixture = require('./fixture/events');
var sutPath = '../lib/flipr-etcd';
var Sut;
var sut;
var nodeEtcd;
describe('flipr-etcd', function(){
beforeEach(function(){
Sut = proxyquire(sutPath, {
'node-etcd': NodeEtcdFixture,
'events': eventsFixture
});
sut = new Sut();
nodeEtcd = NodeEtcdFixture.current();
});
it('passes default options to node-etcd', function(){
expect(nodeEtcd.host).to.equal('127.0.0.1');
expect(nodeEtcd.port).to.equal('4001');
});
it('sends new options to node-etcd', function(){
sut = new Sut({host: 'a', port: 'b'});
nodeEtcd = NodeEtcdFixture.current();
expect(nodeEtcd.host).to.equal('a');
expect(nodeEtcd.port).to.equal('b');
});
it('sends hosts to node-etcd instead of host/port if hosts has at least one value', function(){
sut = new Sut({
hosts: ['10.0.0.1:2379'],
ssl: 'sslopts'
});
nodeEtcd = NodeEtcdFixture.current();
expect(nodeEtcd.host).to.eql(['10.0.0.1:2379']);
expect(nodeEtcd.port).to.equal('sslopts');
});
it('forces instantiation', function(){
sut = Sut(); //jshint ignore:line
expect(sut).to.be.instanceof(Sut);
});
describe('has events', function(){
it('beforeChange', function(){
expect(sut.events.beforeChange).to.equal('before-change');
});
it('flush', function(){
expect(sut.events.flush).to.equal('flush');
});
it('afterChange', function(){
expect(sut.events.afterChange).to.equal('after-change');
});
it('error', function(){
expect(sut.events.error).to.equal('error');
});
});
describe('#getConfig', function(){
it('retrieves config from etcd', function(done){
Sut = proxyquire(sutPath, {
'node-etcd': NodeEtcdFixture,
'events': eventsFixture,
'./parse-client-result': sinon.stub().returns({config:'someconfig'})
});
sut = new Sut();
sut.getConfig(function(err, result){
if(err)
return done(err);
expect(result).to.deep.equal('someconfig');
done();
});
});
it('memoizes config from etcd', function(done){
var etcdResponse1 = {
node: {
value: '{"some":"config"}'
}
};
var etcdResponse2 = {
node: {
value: '{"new":"config"}'
}
};
nodeEtcd.mock({getReturn: etcdResponse1});
sut.getConfig(function(err, result){
if(err)
return done(err);
expect(result).to.eql({
some: 'config'
});
nodeEtcd.mock({getReturn: etcdResponse2});
sut.getConfig(function(err2, result2){
if(err)
return done(err);
expect(result2).to.eql({
some: 'config'
});
done();
});
});
});
it('returns last valid config if next call to etcd errors', function(done){
var etcdResponse1 = {
node: {
value: '{"some":"config"}'
}
};
var etcdResponse2 = 'error!';
nodeEtcd.mock({getReturn: etcdResponse1});
sut.getConfig(function(err, result){
if(err)
return done(err);
expect(result).to.deep.equal({
some: 'config'
});
nodeEtcd.mock({getError: etcdResponse2});
sut.getConfig(function(err2, result2){
if(err)
return done(err);
expect(result2).to.deep.equal({
some: 'config'
});
done();
});
});
});
it('returns error if no last valid config found after etcd error', function(done){
var etcdResponse = 'error!';
nodeEtcd.mock({getError: etcdResponse});
sut.getConfig(function(err){
expect(err).to.equal('error!');
done();
});
});
it('passes last error from parseClientResult as err to cb', function(done){
Sut = proxyquire(sutPath, {
'node-etcd': NodeEtcdFixture,
'events': eventsFixture,
'./parse-client-result': sinon.stub().returns({errors:['error1', 'error2']})
});
sut = new Sut();
sut.getConfig(function(err){
expect(err).to.equal('error2');
done();
});
});
it('passes error to cb even if parseClientResult does not return one', function(done){
Sut = proxyquire(sutPath, {
'node-etcd': NodeEtcdFixture,
'events': eventsFixture,
'./parse-client-result': sinon.stub().returns({})
});
sut = new Sut();
sut.getConfig(function(err){
expect(err).to.eql(new Error());
done();
});
});
});
describe('#flush', function(){
it('clears memoized getConfig', function(done){
var etcdResponse1 = {
node: {
value: '{"some":"config"}'
}
};
var etcdResponse2 = {
node: {
value: '{"im":"different"}'
}
};
nodeEtcd.mock({getReturn: etcdResponse1});
sut.getConfig(function(err, result){
if(err)
return done(err);
expect(result).to.deep.equal({
some: 'config'
});
sut.flush();
nodeEtcd.mock({getReturn: etcdResponse2});
sut.getConfig(function(err2, result2){
if(err)
return done(err);
expect(result2).to.deep.equal({
im: 'different'
});
done();
});
});
});
});
describe('watcher', function(){
it('watches for changes and triggers events, calls flush, and preloads changes', function(done){
sinon.spy(sut, 'flush');
sut.preload = function(cb){cb();};
sut.watcher.on('change', function(){
expect(sut.emit).to.be.calledWith('before-change');
expect(sut.flush).to.be.called;
expect(sut.emit).to.be.calledWith('after-change');
done();
});
sut.watcher.emit('change', {action: 'set'});
});
it('will not trigger change if etcd action is in exclusion list', function(done){
sut.watcher.on('change', function(){
expect(sut.emit).to.not.be.called;
done();
});
sut.emit.reset();
sut.watcher.emit('change', {action: 'delete'});
});
it('emits error and will not trigger change if etcd response is not recognized', function(done){
sut.watcher.on('change', function(){
expect(sut.emit).to.be.calledWithMatch('error', Error);
done();
});
sut.watcher.emit('change', 'notavalidresponse');
});
});
});