reflip
Version:
key-value-store-backed feature flipping middleware for connect/express.js
393 lines (333 loc) • 8.78 kB
JavaScript
/*global describe:true, it:true, before:true, after:true */
;
var
_ = require('lodash'),
demand = require('must'),
fs = require('fs'),
path = require('path'),
redis = require('redis'),
sinon = require('sinon')
;
var Reflip = require('../index');
var testfile = path.join(__dirname, './mocks/features.json');
var testFeatures = require('./mocks/features.json');
var file2 = path.join(__dirname, './mocks/f2.json');
describe('Reflip', function()
{
before(function(done)
{
var r = redis.createClient();
var chain = r.multi();
var features = [];
_.each(testFeatures.features, function(item)
{
features.push(item.name);
chain.hmset('reflip:' + item.name, item);
});
chain.set('reflip:ttl', testFeatures.ttl);
chain.sadd('reflip:features', features);
chain.exec(function(err, replies)
{
done();
});
});
describe('with file adapter', function()
{
it('can be constructed', function(done)
{
var flipper = new Reflip(
{
storage: new Reflip.FileAdapter({ filename: testfile }),
});
flipper.must.be.an.object();
done();
});
it('calls refresh on its adapter', function(done)
{
var flipper = new Reflip({ storage: new Reflip.FileAdapter({ filename: testfile }), });
flipper.on('ready', function()
{
flipper.must.have.property('storage');
flipper.must.have.property('features');
flipper.features.must.be.an.object();
Object.keys(flipper.features).length.must.equal(4);
done();
});
});
});
describe('with redis adapter', function()
{
it('can be constructed', function(done)
{
var flipper = new Reflip({ storage: new Reflip.RedisAdapter({ client: redis.createClient() }), });
flipper.must.be.an.object();
done();
});
it('calls refresh on its adapter', function(done)
{
var flipper = new Reflip({ storage: new Reflip.RedisAdapter({ client: redis.createClient() }), });
flipper.on('ready', function()
{
flipper.must.have.property('storage');
flipper.storage.must.have.property('refreshTimer');
flipper.storage.refreshTimer.must.be.an.object();
flipper.storage.ttl.must.equal(60000);
flipper.features.must.be.an.object();
Object.keys(flipper.features).length.must.equal(4);
done();
});
});
});
describe('register()', function()
{
var flipper;
before(function(done)
{
flipper = new Reflip({ features: {} });
done();
});
it('is a function on the reflip object', function(done)
{
flipper.register.must.be.a.function();
done();
});
it('adds a feature to the reflip object', function(done)
{
function check() { return true; }
flipper.register('aardwolf', check);
flipper.features.must.have.property('aardwolf');
flipper.features.aardwolf.type.must.equal('custom');
flipper.features.aardwolf.checker.must.equal(check);
done();
});
it('the checker function is called when the registered feature is checked', function(done)
{
var check = sinon.stub();
check.returns(true);
flipper.register('anaconda', check);
var feature = flipper.features.anaconda;
feature.check().must.equal(true);
check.called.must.be.truthy();
done();
});
it('can accept a Feature object argument', function(done)
{
var feature = new Reflip.Feature(
{
name: 'armadillo',
type: 'boolean',
enabled: true
});
flipper.register(feature);
flipper.features.must.have.property('armadillo');
done();
});
});
describe('flip()', function()
{
var flipper;
before(function(done)
{
flipper = new Reflip({ storage: new Reflip.FileAdapter({ filename: testfile }), });
flipper.on('ready', done);
});
it('returns a function', function(done)
{
var middleware = flipper.flip();
middleware.must.be.a.function();
done();
});
it('behaves like connect middleware', function(done)
{
var middleware = flipper.flip();
var request = {}, response = {}, next = sinon.spy();
middleware(request, response, next);
next.calledOnce.must.be.truthy();
done();
});
it('decorates its first argument with a check function', function(done)
{
var middleware = flipper.flip();
var request = {}, response = {};
middleware(request, response, function()
{
request.must.have.property('check');
request.check.must.be.a.function();
var features = request.check();
features.must.be.an.object();
features.alpacas.must.be.truthy();
features.aardvarks.must.be.true();
features.archaeopteryx.must.equal(request.check('archaeopteryx'));
request.check('agouti').must.equal(flipper.default);
done();
});
});
it('obeys its `exportName` option', function(done)
{
var f2 = new Reflip({ exportName: 'wobble', storage: new Reflip.FileAdapter({ filename: testfile }), });
var middleware = f2.flip();
var request = {}, response = {};
middleware(request, response, function()
{
request.must.have.property('wobble');
request.wobble.must.be.a.function();
done();
});
});
});
describe('gate()', function()
{
var flipper;
before(function(done)
{
flipper = new Reflip({ storage: new Reflip.FileAdapter({ filename: testfile }), });
flipper.on('ready', done);
});
it('requires a feature name argument', function(done)
{
function shouldThrow()
{
return flipper.gate();
}
shouldThrow.must.throw();
done();
});
it('returns a function', function(done)
{
var gater = flipper.gate('archaeopteryx');
gater.must.be.a.function();
done();
});
it('invokes its callback with no argument if the feature is enabled', function(done)
{
var req = {}, res = {};
req.check = sinon.stub();
req.check.returns(true);
var next = sinon.spy();
var gater = flipper.gate('aardvarks');
gater(req, res, next);
req.check.calledOnce.must.be.truthy();
next.calledOnce.must.be.truthy();
done();
});
it('invokes its callback with reflip.httpcode if the feature is disabled', function(done)
{
var req = {}, res = {};
req.check = sinon.stub();
req.check.returns(false);
res.send = sinon.spy();
var next = sinon.spy();
var gater = flipper.gate('archaeopteryx');
gater(req, res, next);
req.check.calledOnce.must.be.truthy();
res.send.calledOnce.must.be.truthy();
next.calledOnce.must.be.false();
var arglist = res.send.args[0];
arglist.length.must.equal(2);
arglist[0].must.equal(404);
arglist[1].must.be.a.string();
arglist[1].must.equal('Not Found');
done();
});
it('allows specifying a custom failure handler if the feature is disabled', function(done)
{
var req = {}, res = {};
req.check = sinon.stub();
req.check.returns(false);
res.json = sinon.spy();
var next = sinon.spy();
var gater = flipper.gate('aardwolf', function(req, res)
{
res.json(403,
{
code: 'ForbiddenError',
message: 'This feature is unavailable.'
});
});
gater(req, res, next);
req.check.calledOnce.must.be.truthy();
res.json.calledOnce.must.be.truthy();
next.calledOnce.must.be.false();
var arglist = res.json.args[0];
arglist.length.must.equal(2);
arglist[0].must.equal(403);
arglist[1].must.eql(
{
code: 'ForbiddenError',
message: 'This feature is unavailable.'
});
done();
});
it('updates its responses', function(done)
{
var features2 =
{
"features":
[
{
"name": "aardvarks",
"type": "boolean",
"enabled": false
},
{
"name": "archaeopteryx",
"type": "boolean",
"enabled": true
},
{
"name": "aardwolf",
"type": "metered",
"enabled": true,
"chance": 25
}
]
};
fs.writeFile(file2, JSON.stringify(testFeatures), function(err)
{
demand(err).not.exist();
var flipper = new Reflip({ storage: new Reflip.FileAdapter({ filename: file2 }), });
var count = 0;
function blort()
{
var bar = flipper.features.aardvarks.check({});
if (count === 0)
{
fs.writeFileSync(file2, JSON.stringify(features2));
count++;
}
if (bar === false)
{
flipper.shutdown();
flipper.removeListener('ready', blort);
done();
}
}
flipper.on('ready', blort);
});
});
after(function(done)
{
flipper.shutdown();
done();
});
});
after(function(done)
{
fs.unlink(file2, function(err)
{
demand(err).not.exist();
var r = redis.createClient();
var chain = r.multi();
chain.del('reflip:ttl');
chain.del('reflip:features');
_.each(Object.keys(testFeatures.features), function(k)
{
chain.del('reflip:' + k);
});
chain.exec(function(err, replies)
{
done();
});
});
});
});