Comparing version 3.0.0 to 4.0.0
# Changelog | ||
## v4.0 | ||
v4.0 brings some new changes to make fflip even more flexible. | ||
#### New! Plugin Support | ||
FFlip has always promised to support every database & web framework. But up to this point, integrating fflip with your web framework of choice hasn't been easy. Support for Express did come bundled, but it also took advantage of some private logic to work properly. | ||
Starting with v4.0, fflip will support a more open plugin architecture for integrations. The library interface will be more stable, and all previously private properties have been made public. We'll keep integrations open-ended for now, so how you build yours will be up to you. You can check out the new [fflip-express integration](https://github.com/FredKSchott/fflip-express) for some inspiration and to see what's possible. We'll also try to keep an up-to-date list of available plugins in this repo's README. | ||
#### Express Support Gets Pulled Out | ||
Given our new dedication to plugins, express support has been pulled out of the main library and into the new [fflip-express](https://github.com/FredKSchott/fflip-express) package. As such, the following methods & properties are now longer available directly on fflip: | ||
- `fflip.expressMiddleware()` (Will throw error when used) | ||
- `fflip.expressRoute()` (Will throw error when used) | ||
- `fflip.express()` (Will throw error when used) | ||
- `fflip.maxCookieAge` is no longer available / will do nothing if set directly | ||
Check out the fflip-express README for instructions on how to set up the new plugin. At the time of writing most logic has remained unchanged, so moving to the new library should only require a couple small code changes. | ||
#### Updated Interface | ||
The following methods have new function signatures: | ||
- `fflip.userHasFeature(user, featureName)` -> `fflip.isFeatureEnabledForUser(featureName, user)` (Note the new argument order) | ||
- `fflip.userFeatures(user)` -> `fflip.getFeaturesForUser(user)` | ||
The following private properties have been made public: | ||
- `fflip._features` -> `fflip.features` | ||
- `fflip._criteria` -> `fflip.criteria` | ||
## Old Criteria Format No Longer Supported | ||
Now that the `criteria` property is publicly exposed and maintained, we can no longer support two different data formats. Instead of adding new logic to translate the old format to the new, we've decided to remove support for the v2.x format. | ||
## v3.0 | ||
v3.0 brings some major changes to make fflip even more powerful. To help ease the transition, v3.x will strive to be backwards compatible with the v2.x interface and expected behavior. That means no breaking changes are expected in this release, even though we're using the 3.0 version number. | ||
v3.0 brings some major changes to make fflip feature & criteria logic even more powerful. To help ease the transition, v3.x will strive to be backwards compatible with the v2.x interface and expected behavior. That means no breaking changes are expected in this release, even though we're using the 3.0 version number. | ||
@@ -23,4 +64,4 @@ #### Updated Interface | ||
- **Feature criteria now supports matching multiple groups of users**: If a list of criteria exists for a feature, any one criteria may be met to evaluate to true for a given user. | ||
- **Feature criteria now support "vetos"**: If that vetoing criteria evaluates to false, it's entire parent array will also evaluate to false regardless of other criteria met. | ||
- **Feature criteria now support "vetoes"**: If that vetoing criteria evaluates to false, it's entire parent array will also evaluate to false regardless of other criteria met. | ||
View the README for a more in depth explanation of this new behavior. |
@@ -12,3 +12,3 @@ module.exports = function(grunt) { | ||
}, | ||
src: ['test/*'] | ||
src: ['test/*.js', 'test/*/**.js'] | ||
}, | ||
@@ -20,3 +20,3 @@ spec: { | ||
}, | ||
src: ['test/*'] | ||
src: ['test/*.js', 'test/*/**.js'] | ||
} | ||
@@ -23,0 +23,0 @@ } |
172
lib/fflip.js
@@ -7,3 +7,2 @@ 'use strict'; | ||
//-------------------------------------------------------------------------- | ||
var FFlipRequestObject = require('./fflip-request'); | ||
var util = require('util'); | ||
@@ -19,5 +18,15 @@ | ||
/** | ||
* Process the `criteria` data provided by the user. Handle any bad input, | ||
* deprecated formatting, and other edge cases caused by user-data here. | ||
* Used to deprecate old express methods and provide the user with information for upgrading. | ||
* | ||
* @throws {Error} Always! | ||
* @return {void} | ||
*/ | ||
function throwExpressNoLongerSupportedError() { | ||
throw new Error('fflip: Express support is no longer bundled. See fflip CHANGELOG & "fflip-express" package for instructions on updating.'); | ||
} | ||
/** | ||
* Process the `criteria` data provided by the user. Handle any bad input | ||
* and other edge cases caused by user-data here. | ||
* | ||
* @param {*} userInput Expects an array of criteria. Also supports the | ||
@@ -33,3 +42,3 @@ * deprecated object format. | ||
if (!Array.isArray(userInput)) { | ||
return userInput; | ||
throw new Error('fflip: As of v4.0 deprecated criteria format is no longer supported. Please update to new format.'); | ||
} | ||
@@ -39,3 +48,3 @@ | ||
userInput.forEach(function(criteriaObject) { | ||
returnObj[criteriaObject.id] = criteriaObject.check; | ||
returnObj[criteriaObject.id] = criteriaObject; | ||
}); | ||
@@ -77,3 +86,3 @@ return returnObj; | ||
function setCriteria(configVal) { | ||
self._criteria = processUserCriteria(configVal) | ||
self.criteria = processUserCriteria(configVal) | ||
} | ||
@@ -101,3 +110,3 @@ | ||
getFeatures = undefined; | ||
self._features = processUserFeatures(configVal); | ||
self.features = processUserFeatures(configVal); | ||
} | ||
@@ -113,3 +122,3 @@ | ||
function getFeaturesCallback(data) { | ||
self._features = processUserFeatures(data) || self._features; | ||
self.features = processUserFeatures(data) || self.features; | ||
} | ||
@@ -148,5 +157,6 @@ | ||
} | ||
var c_data = criteriaSet[cName]; | ||
var c_func = self._criteria[cName]; | ||
return (c_func(user, c_data) && currentResult); | ||
var criteria = self.criteria[cName]; | ||
var criteriaLogic = criteria.check; | ||
var criteriaDataArgument = criteriaSet[cName]; | ||
return (criteriaLogic(user, criteriaDataArgument) && currentResult); | ||
}, true); | ||
@@ -197,6 +207,6 @@ } | ||
// Object containing all fflip features | ||
_features: {}, | ||
features: {}, | ||
// Object containing all fflip criteria | ||
_criteria: {}, | ||
criteria: {}, | ||
@@ -206,5 +216,2 @@ // The reload rate for reloading features | ||
// The max cookie age for the express integration | ||
maxCookieAge: 900000, | ||
/** | ||
@@ -233,3 +240,3 @@ * Configure fflip. | ||
if(getFeatures.length === 0) { | ||
self._features = processUserFeatures(getFeatures()) || self._features; | ||
self.features = processUserFeatures(getFeatures()) || self.features; | ||
return; | ||
@@ -249,4 +256,4 @@ } | ||
*/ | ||
userHasFeature: function(user, featureName) { | ||
var feature = self._features[featureName]; | ||
isFeatureEnabledForUser: function(featureName, user) { | ||
var feature = self.features[featureName]; | ||
@@ -292,11 +299,11 @@ // If feature does not exist, return null | ||
*/ | ||
userFeatures: function(user, flags) { | ||
getFeaturesForUser: function(user, flags) { | ||
flags = flags || {}; | ||
var userFeatures = {}; | ||
for (var featureName in self._features) { | ||
if (self._features.hasOwnProperty(featureName)) { | ||
for (var featureName in self.features) { | ||
if (self.features.hasOwnProperty(featureName)) { | ||
if(flags[featureName] !== undefined) { | ||
userFeatures[featureName] = flags[featureName]; | ||
} else { | ||
userFeatures[featureName] = self.userHasFeature(user, featureName); | ||
userFeatures[featureName] = self.isFeatureEnabledForUser(featureName, user); | ||
} | ||
@@ -308,112 +315,15 @@ } | ||
/** | ||
* Express middleware. Attaches helper functions to the request object | ||
* and wrap the res.render to automatically include features in the | ||
* template. | ||
* | ||
* @param {Request} req | ||
* @param {Response} res | ||
* @param {Function} next | ||
* @return {void} | ||
*/ | ||
expressMiddleware: function(req, res, next) { | ||
/** @deprecated As of v4.0, Express support is no longer bundled with fflip. See CHANGELOG for instructions on updating. */ | ||
express_middleware: throwExpressNoLongerSupportedError, | ||
expressMiddleware: throwExpressNoLongerSupportedError, | ||
express_route: throwExpressNoLongerSupportedError, | ||
expressRoute: throwExpressNoLongerSupportedError, | ||
express: throwExpressNoLongerSupportedError | ||
// Attach the fflip object to the request | ||
req.fflip = new FFlipRequestObject(self, req.cookies.fflip); | ||
// Wrap res.render() to set options.features automatically | ||
res._render = res.render; | ||
res.render = function(view, options, callback) { | ||
options = options || {}; | ||
options.Features = req.fflip.features; | ||
options.FeaturesJSON = JSON.stringify(req.fflip.features); | ||
res._render(view, options, callback); | ||
}; | ||
// Carry On! | ||
next(); | ||
}, | ||
/** | ||
* Attach routes for manual feature flipping. | ||
* | ||
* @param {Request} req | ||
* @param {Response} res | ||
* @param {Function} next | ||
* @return {void} | ||
*/ | ||
expressRoute: function(req, res, next) { | ||
var name = req.params.name; | ||
var action = req.params.action; | ||
var actionName = ''; | ||
// Check if feature exists. | ||
if(self._features[name] === undefined) { | ||
var notFoundError = new Error('FFlip: Feature ' + name + ' not found'); | ||
notFoundError.fflip = true; | ||
notFoundError.statusCode = 404; | ||
return next(notFoundError); | ||
} | ||
// Check if cookies are enabled. | ||
if(!req.cookies) { | ||
var noCookiesError = new Error('FFlip: Cookies are not enabled.'); | ||
noCookiesError.fflip = true; | ||
noCookiesError.statusCode = 500; | ||
return next(noCookiesError); | ||
} | ||
// Apply the new action. | ||
var flags = req.cookies.fflip || {}; | ||
switch(action) { | ||
// enable | ||
case '1': | ||
flags[name] = true; | ||
actionName = 'enabled'; | ||
break; | ||
// disable | ||
case '0': | ||
flags[name] = false; | ||
actionName = 'disabled'; | ||
break; | ||
// remove | ||
case '-1': | ||
delete flags[name]; | ||
actionName = 'removed'; | ||
break; | ||
// other: propogate error | ||
default: | ||
var err = new Error('FFlip: Bad Input. Action (' + action + ') must be 1 (enable), 0 (disable), or -1 (remove)'); | ||
err.fflip = true; | ||
err.statusCode = 400; | ||
return next(err); | ||
} | ||
// set new fflip cookie with new data | ||
res.cookie('fflip', flags, { maxAge: self.maxCookieAge }); | ||
res.json(200, { | ||
feature: name, | ||
action: action, | ||
status: 200, | ||
message: 'fflip: Feature ' + name + ' is now ' + actionName | ||
}); | ||
}, | ||
/** | ||
* Attach FFlip functionality to an express app. Includes helpers & routes. | ||
* | ||
* @param {Object} app An Express Application | ||
* @return {void} | ||
*/ | ||
express: function(app) { | ||
// Express Middleware | ||
app.use(this.expressMiddleware); | ||
// Manual Flipping Route | ||
app.get('/fflip/:name/:action', this.expressRoute); | ||
} | ||
}; | ||
/** @deprecated v2.x method names have been deprecated. These mappers will be removed in future versions. */ | ||
self.express_route = util.deprecate(self.expressRoute, 'fflip.express_route: Use fflip.expressRoute instead'); | ||
self.express_middleware = util.deprecate(self.expressMiddleware, 'fflip.express_middleware: Use fflip.expressMiddleware instead'); | ||
/** @deprecated v3.x method names have been deprecated. These mappers will be removed in future versions. */ | ||
self.userFeatures = util.deprecate(self.getFeaturesForUser, 'fflip.userFeatures: Use fflip.getFeaturesForUser instead'); | ||
self.userHasFeature = util.deprecate(function(user, featureName) { | ||
return self.isFeatureEnabledForUser(featureName, user); | ||
}, 'fflip.userHasFeature(user, featureName): Use fflip.isFeatureEnabledForUser(featureName, user) instead'); |
@@ -6,3 +6,3 @@ { | ||
"licence": "MIT", | ||
"version": "3.0.0", | ||
"version": "4.0.0", | ||
"main": "lib/fflip", | ||
@@ -9,0 +9,0 @@ "repository": "FredKSchott/fflip", |
@@ -8,6 +8,6 @@ <a href="https://www.npmjs.com/package/fflip"> | ||
- Describes __custom criteria and features__ using easy-to-read JSON | ||
- Delivers features down to the client for __client-side feature flipping__ | ||
- Includes __Express Middleware__ for additional features like __feature flipping via cookie__ | ||
- __System-Agnostic:__ Built to support any database, user representation or web framework you can throw at it | ||
- Create __custom criteria__ to segment users & features based on your audience. | ||
- __View & edit feature access__ in one easy place, and not scattered around your code base. | ||
- __System-Agnostic:__ Support any database, user representation or web framework you can throw at it. | ||
- __Extensible:__ Supports 3rd-party plugins for your favorite libraries (like [our Express integration](https://github.com/FredKSchott/fflip-express)!) | ||
@@ -33,3 +33,3 @@ ``` | ||
// Get all of a user's enabled features... | ||
someFreeUser.features = fflip.userFeatures(someFreeUser); | ||
someFreeUser.features = fflip.getFeaturesForUser(someFreeUser); | ||
if(someFreeUser.features.closedBeta === true) { | ||
@@ -40,3 +40,3 @@ console.log('Welcome to the Closed Beta!'); | ||
// ... or just check this single feature. | ||
if (fflip.userHasFeature(someFreeUser, 'closedBeta') === true) { | ||
if (fflip.isFeatureEnabledForUser('closedBeta', someFreeUser) === true) { | ||
console.log('Welcome to the Closed Beta!'); | ||
@@ -125,6 +125,5 @@ } | ||
- `.config(options) -> void`: Configure fflip (see below) | ||
- `.userHasFeature(user, featureName) -> boolean`: Return true/false if featureName is enabled for user | ||
- `.userFeatures(user) -> Object`: Return object of true/false for all features for user | ||
- `.isFeatureEnabledForUser(featureName, user) -> boolean`: Return true/false if featureName is enabled for user | ||
- `.getFeaturesForUser(user) -> Object`: Return object of true/false for all features for user | ||
- `.reload() -> void`: Force a reload (if loading features dynamically) | ||
- `.express(app) -> void`: Connect with an Express app or router (see below) | ||
@@ -175,48 +174,13 @@ | ||
## Integrations | ||
## Express Integration | ||
As mentioned, fflip's goal is to be flexible enough to integrate with any web framework, database, or ORM. The following integrations are known to exist: | ||
fflip provides two easy integrations with the popular web framework [Express](http://expressjs.com/). | ||
- [fflip-express](https://github.com/FredKSchott/fflip-express): Express.js integration | ||
#### fflip.expressMiddleware() | ||
If you're interested in creating an integration, don't hesitate to reach out or create an issue if some functionality is missing. And if you've created an integration, please [add it](https://github.com/FredKSchott/fflip/edit/master/README.md) to the list above! | ||
```javascript | ||
app.use(fflip.expressMiddleware); | ||
``` | ||
**req.fflip:** A special fflip request object is attached to the request object, and includes the following functionality: | ||
``` | ||
req.fflip = { | ||
setForUser(user): Given a user, attaches the features object to the request (at req.fflip.features). Make sure you do this before calling has()! | ||
has(featureName): Given a feature name, returns the feature boolean, undefined if feature doesn't exist. Throws an error if setForUser() hasn't been called | ||
} | ||
``` | ||
**Use fflip in your templates:** Once `setForUser()` has been called, fflip will include a `Features` template variable that contains your user's enabled features. Here is an example of how to use it with Handlebars: `{{#if Features.closedBeta}} Welcome to the Beta! {{/if}}` | ||
**Use fflip on the client:** Once `setForUser()` has been called, fflip will also include a `FeaturesJSON` template variable that is the JSON string of your user's enabled features. To deliver this down to the client, just make sure your template something like this: `<script>var Features = {{ FeaturesJSON }}; </script>`. | ||
#### fflip.expressRoute() | ||
```javascript | ||
// Feel free to use any route you'd like, as long as `name` & `action` exist as route parameters. | ||
app.get('/custom_path/:name/:action', fflip.expressRoute); | ||
``` | ||
**A route for manually flipping on/off features:** If you have cookies enabled, you can visit this route to manually override a feature to always return true/false for your own session. Just replace ':name' with the Feature name and ':action' with 1 to enable, 0 to disable, or -1 to reset (remove the cookie override). This override is stored in the user's cookie under the name `fflip`, which is then read by `fflip.expressMiddleware()` and `req.fflip` automatically. | ||
#### fflip.express() | ||
Sets up the express middleware and route automatically. Equivilent to running: | ||
```javascript | ||
app.use(fflip.expressMiddleware); | ||
app.get('/custom_path/:name/:action', fflip.expressRoute); | ||
``` | ||
## Special Thanks | ||
Original logo designed by <a href="http://thenounproject.com/Luboš Volkov" target="_blank">Luboš Volkov</a> |
@@ -23,10 +23,16 @@ /** | ||
var configData = { | ||
criteria: { | ||
c1: function(user, bool) { | ||
return bool; | ||
criteria: [ | ||
{ | ||
id: 'c1', | ||
check: function(user, bool) { | ||
return bool; | ||
} | ||
}, | ||
c2: function(user, flag) { | ||
return user.flag == flag; | ||
{ | ||
id: 'c2', | ||
check: function(user, flag) { | ||
return user.flag == flag; | ||
} | ||
} | ||
}, | ||
], | ||
features: { | ||
@@ -73,12 +79,26 @@ fEmpty: {}, | ||
describe('express integration', function(){ | ||
describe('userHasFeature()', function(){ | ||
it('express_middleware() still exists for v2.x backwards compatibility', function() { | ||
assert(fflip.express_middleware); | ||
beforeEach(function() { | ||
fflip.config(configData); | ||
}); | ||
it('express_route() still exists for v2.x backwards compatibility', function() { | ||
assert(fflip.express_route); | ||
it('should return null if features does not exist', function(){ | ||
assert.equal(null, fflip.userHasFeature(userABC, 'notafeature')); | ||
}); | ||
it('should return false if no criteria set', function(){ | ||
assert.equal(false, fflip.userHasFeature(userABC, 'fEmpty')); | ||
}); | ||
it('should return false if all feature critieria evaluates to false', function(){ | ||
assert.equal(false, fflip.userHasFeature(userABC, 'fClosed')); | ||
assert.equal(false, fflip.userHasFeature(userXYZ, 'fEval')); | ||
}); | ||
it('should return true if one feature critieria evaluates to true', function(){ | ||
assert.equal(true, fflip.userHasFeature(userABC, 'fOpen')); | ||
assert.equal(true, fflip.userHasFeature(userABC, 'fEval')); | ||
}); | ||
}); | ||
@@ -88,9 +108,4 @@ | ||
it('correct features are enabled when features and criteria use old format', function() { | ||
assert.deepEqual(fflip.userFeatures(userABC), { | ||
fEmpty: false, | ||
fOpen: true, | ||
fClosed: false, | ||
fEval: true | ||
}); | ||
it('userFeatures() is equivilent to getFeaturesForUser()', function() { | ||
assert.deepEqual(fflip.userFeatures(userABC), fflip.getFeaturesForUser(userABC)); | ||
}); | ||
@@ -100,2 +115,14 @@ | ||
describe('express support', function(){ | ||
it('throws error when called', function() { | ||
assert.throws(function() { fflip.express_middleware(); }, /fflip: Express support is no longer bundled/); | ||
assert.throws(function() { fflip.expressMiddleware(); }, /fflip: Express support is no longer bundled/); | ||
assert.throws(function() { fflip.express_route(); }, /fflip: Express support is no longer bundled/); | ||
assert.throws(function() { fflip.expressRoute(); }, /fflip: Express support is no longer bundled/); | ||
assert.throws(function() { fflip.express(); }, /fflip: Express support is no longer bundled/); | ||
}); | ||
}); | ||
}); |
@@ -117,5 +117,5 @@ 'use strict'; | ||
it('should set features if given static feature array', function(){ | ||
fflip._features = {}; | ||
fflip.features = {}; | ||
fflip.config(configData); | ||
assert.deepEqual(fflip._features, { | ||
assert.deepEqual(fflip.features, { | ||
fEmpty: configData.features[0], | ||
@@ -135,5 +135,5 @@ fOpen: configData.features[1], | ||
}; | ||
fflip._features = {}; | ||
fflip.features = {}; | ||
fflip.config({features: loadSyncronously, criteria: configData.criteria}); | ||
assert.deepEqual(fflip._features, { | ||
assert.deepEqual(fflip.features, { | ||
fEmpty: configData.features[0], | ||
@@ -152,3 +152,3 @@ fOpen: configData.features[1], | ||
callback(configData.features); | ||
assert.deepEqual(fflip._features, { | ||
assert.deepEqual(fflip.features, { | ||
fEmpty: configData.features[0], | ||
@@ -168,7 +168,7 @@ fOpen: configData.features[1], | ||
it('should set criteria if given static criteria array', function(){ | ||
fflip._criteria = {}; | ||
fflip.criteria = {}; | ||
fflip.config(configData); | ||
assert.deepEqual(fflip._criteria, { | ||
c1: configData.criteria[0].check, | ||
c2: configData.criteria[1].check | ||
assert.deepEqual(fflip.criteria, { | ||
c1: configData.criteria[0], | ||
c2: configData.criteria[1] | ||
}); | ||
@@ -215,3 +215,3 @@ }); | ||
describe('userHasFeature()', function() { | ||
describe('isFeatureEnabledForUser()', function() { | ||
@@ -223,7 +223,7 @@ beforeEach(function() { | ||
it('should return null if features does not exist', function(){ | ||
assert.equal(null, fflip.userHasFeature(userABC, 'notafeature')); | ||
assert.equal(null, fflip.isFeatureEnabledForUser('notafeature', userABC)); | ||
}); | ||
it('should return false if no criteria set', function(){ | ||
assert.equal(false, fflip.userHasFeature(userABC, 'fEmpty')); | ||
assert.equal(false, fflip.isFeatureEnabledForUser('fEmpty', userABC)); | ||
}); | ||
@@ -234,32 +234,32 @@ | ||
it('should return false if all feature critieria evaluates to false', function(){ | ||
assert.equal(false, fflip.userHasFeature(userABC, 'fClosed')); | ||
assert.equal(false, fflip.userHasFeature(userXYZ, 'fEval')); | ||
assert.equal(false, fflip.isFeatureEnabledForUser('fClosed', userABC)); | ||
assert.equal(false, fflip.isFeatureEnabledForUser('fEval', userXYZ)); | ||
}); | ||
it('should return false if one feature critieria evaluates to true and the other evaluates to false', function(){ | ||
assert.equal(false, fflip.userHasFeature(userXYZ, 'fEval')); | ||
assert.equal(false, fflip.isFeatureEnabledForUser('fEval', userXYZ)); | ||
}); | ||
it('should return true if all feature critieria evaluates to true', function(){ | ||
assert.equal(true, fflip.userHasFeature(userABC, 'fEval')); | ||
assert.equal(true, fflip.isFeatureEnabledForUser('fEval', userABC)); | ||
}); | ||
it('should return false if zero feature critieria evaluates to true', function(){ | ||
assert.equal(false, fflip.userHasFeature(userXYZ, 'fEvalOr')); | ||
assert.equal(false, fflip.isFeatureEnabledForUser('fEvalOr', userXYZ)); | ||
}); | ||
it('should return true if one feature critieria evaluates to true', function(){ | ||
assert.equal(true, fflip.userHasFeature(userABC, 'fEvalOr')); | ||
assert.equal(true, fflip.isFeatureEnabledForUser('fEvalOr', userABC)); | ||
}); | ||
it('should handle nested arrays', function(){ | ||
assert.equal(true, fflip.userHasFeature(userABC, 'fEvalComplex')); | ||
assert.equal(true, fflip.userHasFeature(userEFG, 'fEvalComplex')); | ||
assert.equal(false, fflip.userHasFeature(userXYZ, 'fEvalComplex')); | ||
assert.equal(true, fflip.isFeatureEnabledForUser('fEvalComplex', userABC)); | ||
assert.equal(true, fflip.isFeatureEnabledForUser('fEvalComplex', userEFG)); | ||
assert.equal(false, fflip.isFeatureEnabledForUser('fEvalComplex', userXYZ)); | ||
}); | ||
it('should handle the $veto property', function(){ | ||
assert.equal(false, fflip.userHasFeature(userABC, 'fEvalVeto')); | ||
assert.equal(true, fflip.userHasFeature(userEFG, 'fEvalVeto')); | ||
assert.equal(false, fflip.userHasFeature(userXYZ, 'fEvalVeto')); | ||
assert.equal(false, fflip.isFeatureEnabledForUser('fEvalVeto', userABC)); | ||
assert.equal(true, fflip.isFeatureEnabledForUser('fEvalVeto', userEFG)); | ||
assert.equal(false, fflip.isFeatureEnabledForUser('fEvalVeto', userXYZ)); | ||
}); | ||
@@ -270,3 +270,3 @@ | ||
describe('userFeatures()', function(){ | ||
describe('getFeaturesForUser()', function(){ | ||
@@ -278,3 +278,3 @@ beforeEach(function() { | ||
it('should return an object of features for a user', function(){ | ||
var featuresABC = fflip.userFeatures(userABC); | ||
var featuresABC = fflip.getFeaturesForUser(userABC); | ||
assert.deepEqual(featuresABC, { | ||
@@ -290,3 +290,3 @@ fEmpty: false, | ||
var featuresEFG = fflip.userFeatures(userEFG); | ||
var featuresEFG = fflip.getFeaturesForUser(userEFG); | ||
assert.deepEqual(featuresEFG, { | ||
@@ -302,3 +302,3 @@ fEmpty: false, | ||
var featuresXYZ = fflip.userFeatures(userXYZ); | ||
var featuresXYZ = fflip.getFeaturesForUser(userXYZ); | ||
assert.deepEqual(featuresXYZ, { | ||
@@ -316,5 +316,5 @@ fEmpty: false, | ||
it('should overwrite values when flags are set', function() { | ||
var featuresXYZ = fflip.userFeatures(userXYZ); | ||
var featuresXYZ = fflip.getFeaturesForUser(userXYZ); | ||
assert.equal(featuresXYZ.fEval, false); | ||
featuresXYZ = fflip.userFeatures(userXYZ, {fEval: true}); | ||
featuresXYZ = fflip.getFeaturesForUser(userXYZ, {fEval: true}); | ||
assert.equal(featuresXYZ.fEval, true); | ||
@@ -325,198 +325,2 @@ }); | ||
describe('express middleware', function(){ | ||
beforeEach(function() { | ||
this.reqMock = { | ||
cookies: { | ||
fflip: { | ||
fClosed: false | ||
} | ||
} | ||
}; | ||
this.renderOriginal = sandbox.spy(); | ||
this.resMock = { | ||
render: this.renderOriginal | ||
}; | ||
this.appMock = { | ||
use: sandbox.spy(), | ||
get: sandbox.spy() | ||
}; | ||
}); | ||
it('should set fflip object onto req', function(done) { | ||
var me = this; | ||
fflip.expressMiddleware(this.reqMock, this.resMock, function() { | ||
assert(me.reqMock.fflip); | ||
assert(me.reqMock.fflip._flags, me.reqMock.cookies.fflip); | ||
done(); | ||
}); | ||
}); | ||
it('should allow res.render() to be called without model object', function(done) { | ||
var me = this; | ||
fflip.expressMiddleware(this.reqMock, this.resMock, function() { | ||
assert.doesNotThrow(function() { | ||
me.resMock.render('testview'); | ||
}); | ||
done(); | ||
}); | ||
}); | ||
it('should wrap res.render() to set features object automatically', function(done) { | ||
var me = this; | ||
fflip.expressMiddleware(this.reqMock, this.resMock, function() { | ||
var features = {features : { fClosed: true }}; | ||
var featuresString = JSON.stringify(features); | ||
me.reqMock.fflip.features = features; | ||
me.resMock.render('testview', {}); | ||
assert(me.renderOriginal.calledOnce); | ||
assert(me.renderOriginal.calledWith('testview', { | ||
Features: features, | ||
FeaturesJSON: featuresString | ||
})); | ||
done(); | ||
}); | ||
}); | ||
it('req.fflip.setFeatures() should call userFeatures() with cookie flags', function(done) { | ||
var me = this; | ||
var spy = sandbox.spy(fflip, 'userFeatures'); | ||
fflip.expressMiddleware(this.reqMock, this.resMock, function() { | ||
me.reqMock.fflip.setForUser(userXYZ); | ||
assert(fflip.userFeatures.calledOnce); | ||
assert(fflip.userFeatures.calledWith(userXYZ, {fClosed: false})); | ||
spy.restore(); | ||
done(); | ||
}); | ||
}); | ||
it('req.fflip.has() should get the correct features', function(done) { | ||
var me = this; | ||
fflip.expressMiddleware(this.reqMock, this.resMock, function() { | ||
me.reqMock.fflip.setForUser(userXYZ); | ||
assert.strictEqual(me.reqMock.fflip.has('fOpen'), true); | ||
assert.strictEqual(me.reqMock.fflip.has('fClosed'), false); | ||
assert.strictEqual(me.reqMock.fflip.has('notafeature'), false); | ||
done(); | ||
}); | ||
}); | ||
it('req.fflip.has() should throw when called before features have been set', function() { | ||
var me = this; | ||
assert.throws(function() { | ||
fflip.expressMiddleware(this.reqMock, this.resMock, function() { | ||
me.reqMock.fflip.has('fOpen'); | ||
}); | ||
}); | ||
}); | ||
it('req.fflip.featuers should be an empty object if setFeatures() has not been called', function(done) { | ||
var me = this; | ||
var consoleErrorStub = sandbox.stub(console, 'error'); // Supress Error Output | ||
fflip.expressMiddleware(this.reqMock, this.resMock, function() { | ||
assert.ok(isObjectEmpty(me.reqMock.fflip.features)); | ||
done(); | ||
consoleErrorStub.restore(); | ||
}); | ||
}); | ||
it('should mount express middleware into provided app', function() { | ||
fflip.express(this.appMock); | ||
assert.ok(this.appMock.use.calledWith(fflip.expressMiddleware)); | ||
}); | ||
it('should add GET route for manual feature flipping into provided app', function() { | ||
fflip.express(this.appMock); | ||
assert.ok(this.appMock.get.calledWith('/fflip/:name/:action', fflip.expressRoute)); | ||
}); | ||
}); | ||
describe('express route', function(){ | ||
beforeEach(function() { | ||
this.reqMock = { | ||
params: { | ||
name: 'fClosed', | ||
action: '1' | ||
}, | ||
cookies: {} | ||
}; | ||
this.resMock = { | ||
json: sandbox.spy(), | ||
cookie: sandbox.spy() | ||
}; | ||
}); | ||
it('should propogate a 404 error if feature does not exist', function(done) { | ||
this.reqMock.params.name = 'doesnotexist'; | ||
fflip.expressRoute(this.reqMock, this.resMock, function(err) { | ||
assert(err); | ||
assert(err.fflip); | ||
assert.equal(err.statusCode, 404); | ||
done(); | ||
}); | ||
}); | ||
it('should propogate a 500 error if cookies are not enabled', function(done) { | ||
this.reqMock.cookies = null; | ||
fflip.expressRoute(this.reqMock, this.resMock, function(err) { | ||
assert(err); | ||
assert(err.fflip); | ||
assert.equal(err.statusCode, 500); | ||
done(); | ||
}); | ||
}); | ||
it('should set the right cookie flags', function() { | ||
fflip.expressRoute(this.reqMock, this.resMock); | ||
assert(this.resMock.cookie.calledWithMatch('fflip', {fClosed: true}, { maxAge: 900000 })); | ||
}); | ||
it('should set the right cookie flags when maxCookieAge is set', function() { | ||
var oneMonthMs = 31 * 86400 * 1000; | ||
var oldMaxCookieAge = fflip.maxCookieAge; | ||
fflip.maxCookieAge = oneMonthMs; | ||
fflip.expressRoute(this.reqMock, this.resMock); | ||
fflip.maxCookieAge = oldMaxCookieAge; | ||
assert(this.resMock.cookie.calledWithMatch('fflip', {fClosed: true}, { maxAge: oneMonthMs })); | ||
}); | ||
it('should send back 200 json response on successful call', function() { | ||
fflip.expressRoute(this.reqMock, this.resMock); | ||
assert(this.resMock.json.calledWith(200)); | ||
}); | ||
// var request = require('supertest')('http://localhost:5555'); | ||
// it('should return a 404 error if feature does not exist', function(done) { | ||
// request.get('/fflip/doesnotexist/1').expect(404, function(err){ | ||
// if(err) done(err); | ||
// done(); | ||
// }); | ||
// }); | ||
// it('should return a 400 error if action is invalid', function() { | ||
// request.get('/fflip/fOpen/5').expect(400, function(err){ | ||
// if(err) done(err); | ||
// done(); | ||
// }); | ||
// }); | ||
// it('should return a 200 sucess if request was valid', function() { | ||
// request.get('/fflip/fOpen/1').expect(400, function(err){ | ||
// if(err) done(err); | ||
// done(); | ||
// }); | ||
// }); | ||
// it('should call res.cookie() on successful request', function() { | ||
// self.expressRoute(this.reqMock, this.resMock); | ||
// assert(res.cookie.calledWith('fflip')); | ||
// }); | ||
}); | ||
}); |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
33562
12
663
182