Huge News!Announcing our $40M Series B led by Abstract Ventures.Learn More
Socket
Sign inDemoInstall
Socket

fflip

Package Overview
Dependencies
Maintainers
1
Versions
18
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

fflip - npm Package Compare versions

Comparing version 2.2.2 to 3.0.0

.eslintrc.json

4

Gruntfile.js

@@ -12,3 +12,3 @@ module.exports = function(grunt) {

},
src: ['test/fflip.js']
src: ['test/*']
},

@@ -20,3 +20,3 @@ spec: {

},
src: ['test/fflip.js']
src: ['test/*']
}

@@ -23,0 +23,0 @@ }

@@ -5,2 +5,3 @@ 'use strict';

* FFlip Request Object - Express/Connect Helper
*
* @param {FFlip} fflip The fflip main module

@@ -21,2 +22,3 @@ * @param {[Object]} flags A set of feature overrides, these flags will

* Sets the features for a given user
*
* @param {Object} user A user object to test criteria against for each feature

@@ -32,2 +34,3 @@ * @return {void}

* Check if a user has a certain feature enabled/disabled
*
* @param {string} featureName The name of the feature to check for

@@ -34,0 +37,0 @@ * @return {Boolean} True if feature is enabled, false otherwise

@@ -1,4 +0,1 @@

/**
* @fileoverview FFlip - Feature Flipping Moduel
*/
'use strict';

@@ -11,2 +8,3 @@

var FFlipRequestObject = require('./fflip-request');
var util = require('util');

@@ -21,3 +19,52 @@

/**
* Process the `criteria` data provided by the user. Handle any bad input,
* deprecated formatting, and other edge cases caused by user-data here.
*
* @param {*} userInput Expects an array of criteria. Also supports the
* deprecated object format.
* @return {Object} The criteria dictionary, indexed by criteria ID
*/
function processUserCriteria(userInput) {
if (typeof userInput !== 'object') {
throw new Error('fflip: bad data passed for `criteria`');
}
if (!Array.isArray(userInput)) {
return userInput;
}
var returnObj = {};
userInput.forEach(function(criteriaObject) {
returnObj[criteriaObject.id] = criteriaObject.check;
});
return returnObj;
}
/**
* Process the `features` data provided by the user. Handle any bad input,
* deprecated formatting, and other edge cases caused by user-data here.
*
* @param {*} userInput Expects an array of features. Also supports the
* deprecated object format.
* @return {Object} The features dictionary, indexed by feature ID
*/
function processUserFeatures(userInput) {
if (typeof userInput !== 'object') {
throw new Error('fflip: bad data passed for `features`');
}
if (!Array.isArray(userInput)) {
return userInput;
}
var returnObj = {};
userInput.forEach(function(featureObject) {
returnObj[featureObject.id] = featureObject;
});
return returnObj;
}
/**
* Set the criteria to the given object.
*
* @param {Object} configVal

@@ -28,3 +75,3 @@ * @return {void}

function setCriteria(configVal) {
self._criteria = configVal;
self._criteria = processUserCriteria(configVal)
}

@@ -34,2 +81,3 @@

* Set the features.
*
* @param {Object} configVal

@@ -40,31 +88,15 @@ * @return {void}

function setFeatures(configVal) {
if(typeof configVal == 'function') {
if (typeof configVal === 'function') {
if (configVal.length > 1) {
throw new Error('FFlip: `features` function signature is invalid. Must accept zero arguments or one callback.');
}
getFeatures = configVal;
updateFeatures();
} else {
getFeatures = undefined;
self.reload();
return;
}
if(typeof configVal == 'object') {
self._features = configVal;
}
}
/**
* Update the features by reloading them, if possible.
* @return {void}
* @private
*/
function updateFeatures() {
if(!getFeatures) {
return;
}
if(getFeatures.length === 0) {
self._features = getFeatures() || self._features;
return;
}
if(getFeatures.length === 1) {
getFeatures(getFeaturesCallback);
return;
}
throw new Error('FFlip: params.features function signature is invalid. Must accept zero arguments or one callback.');
getFeatures = undefined;
self._features = processUserFeatures(configVal);
}

@@ -74,2 +106,3 @@

* The callback called by the user-defined function for reloading features.
*
* @param {Object} data

@@ -80,3 +113,3 @@ * @return {void}

function getFeaturesCallback(data) {
self._features = data || self._features;
self._features = processUserFeatures(data) || self._features;
}

@@ -86,2 +119,3 @@

* Sets the reload rate for fetching new features.
* @param {int} rate The interval to fetch new features on, in seconds

@@ -102,6 +136,60 @@ * @return {void}

/**
* Evaluate a set of critera. Return true if ALL criteria is met.
*
* @param {Object} criteriaSet The set of criteria, where each key is a criteria ID
* and each value is some data to send to that criteria function.
* @param {Object} user The expected user object to check against in each criteria function.
* @return {boolean} Returns true if ALL criteria are met, false otherwise.
*/
function evaluateCriteriaSet(criteriaSet, user) {
return Object.keys(criteriaSet).reduce(function(currentResult, cName) {
if (cName === '$veto') {
return currentResult;
}
var c_data = criteriaSet[cName];
var c_func = self._criteria[cName];
return (c_func(user, c_data) && currentResult);
}, true);
}
/**
* Evaluate a list of criteria. Return true if ANY one member evaluates to true, and no
* "vetoing" members evaluate to false.
* @param {Object} criteriaList A list of criteria sets or nested criteria lists.
* @param {Object} user The expected user object to check against in each criteria function.
* @return {boolean} Returns true if if ANY one member evaluates to true, and no "vetoing"
* members evaluate to false. Returns false otherwise.
*/
function evaluateCriteriaList(criteriaList, user) {
var isEnabled = false;
for (var i = 0, l = criteriaList.length; i < l; i++) {
var listMember = criteriaList[i];
var memberResult;
if (Array.isArray(listMember)) {
// if array, repeat this logic on each member of array, return true if ANY return true & NO vetos return false`
memberResult = evaluateCriteriaList(listMember, user);
} else {
// if object, evaluate all and return true if ALL return true
memberResult = evaluateCriteriaSet(listMember, user);
}
if (listMember.$veto && !memberResult) {
return false;
}
isEnabled = memberResult || isEnabled;
}
return isEnabled;
}
//--------------------------------------------------------------------------
// Public
//--------------------------------------------------------------------------
var self = module.exports = {

@@ -115,5 +203,6 @@

// The reload rate for reloading the features
// The reload rate for reloading features
_reloadRate: 30*1000,
// The max cookie age for the express integration
maxCookieAge: 900000,

@@ -123,2 +212,3 @@

* Configure fflip.
*
* @param {Object} params

@@ -136,13 +226,23 @@ * @return {void}

* Reload the features, if a reload is possible.
*
* @return {void}
*/
reload: function() {
updateFeatures();
if(!getFeatures) {
return;
}
if(getFeatures.length === 0) {
self._features = processUserFeatures(getFeatures()) || self._features;
return;
}
getFeatures(getFeaturesCallback);
},
/**
* Check if a user has some given feature, and returns a boolean.
* Returns null if the feature does not exist.
* Check if a user has some given feature, and returns a boolean. Returns null
* if the feature does not exist.
*
* @param {string} featureName The name of the feature to check for.
* @param {Object} user The User object that criterial will check against.
* @param {string} featureName The name of the feature to check for.
* @return {Boolean|null}

@@ -152,19 +252,33 @@ */

var feature = self._features[featureName];
if(typeof feature != 'object') {
// If feature does not exist, return null
if (typeof feature === 'undefined') {
return null;
}
var featureCriteria = feature.criteria || {};
var criteriaArray = Object.keys(featureCriteria);
var isEnabled = true;
if(criteriaArray.length == 0) {
// If feature isn't an object, something has gone terribly wrong
// TODO(fks) 03-10-2016: Check for this on config, not on the fly
if (typeof feature !== 'object') {
throw new Error('fflip: Features are formatted incorrectly.');
}
// If feature.enabled is set, return its boolean form
if (typeof feature.enabled !== 'undefined') {
return !!feature.enabled;
}
// If feature.criteria is some non-object, return its boolean form
if (typeof feature.criteria !== 'object') {
return !!feature.criteria;
}
var featureCriteria;
if (Array.isArray(feature.criteria)) {
featureCriteria = feature.criteria;
} else {
featureCriteria = [feature.criteria];
}
if(featureCriteria.length == 0) {
return false;
}
criteriaArray.forEach(function(cKey) {
if(isEnabled) {
var c_data = featureCriteria[cKey];
var c_func = self._criteria[cKey];
isEnabled = c_func(user, c_data);
}
});
return isEnabled;
return evaluateCriteriaList(featureCriteria, user);
},

@@ -174,5 +288,5 @@

* Get the availability of all features for a given user.
*
* @param {Object} user The User object that criterial will check against.
* @param {Object} flags A collection of overrides
* [@deprecated this flag will be removed soon]
* @return {Object} The collection of all features and their availability.

@@ -182,11 +296,13 @@ */

flags = flags || {};
var user_features = {};
Object.keys(self._features).forEach(function(featureName) {
if(flags[featureName] !== undefined) {
user_features[featureName] = flags[featureName];
} else {
user_features[featureName] = self.userHasFeature(user, featureName);
var userFeatures = {};
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);
}
}
});
return user_features;
}
return userFeatures;
},

@@ -198,2 +314,3 @@

* template.
*
* @param {Request} req

@@ -204,3 +321,3 @@ * @param {Response} res

*/
express_middleware: function(req, res, next) {
expressMiddleware: function(req, res, next) {

@@ -225,2 +342,3 @@ // Attach the fflip object to the request

* Attach routes for manual feature flipping.
*
* @param {Request} req

@@ -231,3 +349,3 @@ * @param {Response} res

*/
express_route: function(req, res, next) {
expressRoute: function(req, res, next) {
var name = req.params.name;

@@ -239,6 +357,6 @@ var action = req.params.action;

if(self._features[name] === undefined) {
var err = new Error('FFlip: Feature ' + name + ' not found');
err.fflip = true;
err.statusCode = 404;
return next(err);
var notFoundError = new Error('FFlip: Feature ' + name + ' not found');
notFoundError.fflip = true;
notFoundError.statusCode = 404;
return next(notFoundError);
}

@@ -248,6 +366,6 @@

if(!req.cookies) {
var err = new Error('FFlip: Cookies are not enabled.');
err.fflip = true;
err.statusCode = 500;
return next(err);
var noCookiesError = new Error('FFlip: Cookies are not enabled.');
noCookiesError.fflip = true;
noCookiesError.statusCode = 500;
return next(noCookiesError);
}

@@ -293,2 +411,3 @@

* Attach FFlip functionality to an express app. Includes helpers & routes.
*
* @param {Object} app An Express Application

@@ -299,7 +418,11 @@ * @return {void}

// Express Middleware
app.use(this.express_middleware);
app.use(this.expressMiddleware);
// Manual Flipping Route
app.get('/fflip/:name/:action', this.express_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');

@@ -6,3 +6,3 @@ {

"licence": "MIT",
"version": "2.2.2",
"version": "3.0.0",
"main": "lib/fflip",

@@ -15,6 +15,9 @@ "repository": "FredKSchott/fflip",

"devDependencies": {
"eslint": "^2.3.0",
"eslint-config-airbnb": "^6.1.0",
"eslint-plugin-react": "^4.2.1",
"grunt": "~0.4.1",
"grunt-mocha-test": "~0.7.0",
"supertest": "~0.8.1",
"sinon": "~1.7.3"
"sinon": "~1.7.3",
"supertest": "~0.8.1"
},

@@ -21,0 +24,0 @@ "keywords": [

@@ -6,8 +6,8 @@ <a href="https://www.npmjs.com/package/fflip">

Working on an experimental new design? Starting a closed beta? Rolling out a new feature over the next few weeks? Fa-fa-fa-flip it! __fflip__ gives you complete control over releasing new functionality to your users based on their user id, join date, membership status, and whatever else you can think of. __fflip's__ goal is to be the most powerful and extensible feature flipping/toggling module on. the. planet.
Working on an experimental new design? Starting a closed beta? Rolling out a new feature over the next few weeks? Fa-fa-fa-flip it! __fflip__ gives you complete control over releasing new functionality to your users based on their user id, join date, membership status, and whatever else you can think of. __fflip's__ goal is to be the most powerful and extensible feature flipping/toggling module out there.
- Describes __custom criteria and features__ using easy-to-read JSON
- Delivers features down to the client for additional __client-side feature flipping__
- Delivers features down to the client for __client-side feature flipping__
- Includes __Express Middleware__ for additional features like __feature flipping via cookie__
- __Everything-Agnostic:__ Supports any database, user representation or framework you can throw at it
- __System-Agnostic:__ Built to support any database, user representation or web framework you can throw at it

@@ -18,4 +18,7 @@ ```

##Getting Started
## Getting Started
Below is a simple example that uses __fflip__ to deliver a closed beta to a fraction of users:
```javascript

@@ -26,13 +29,14 @@ // Include fflip

fflip.config({
criteria: ExampleCriteriaObject, // defined below
features: ExampleFeaturesObject // defined below
criteria: ExampleCriteria, // defined below
features: ExampleFeatures // defined below
});
// Get all of a user's enabled features
var Features = fflip.userFeatures(someFreeUser);
if(Features.closedBeta) {
// Get all of a user's enabled features...
someFreeUser.features = fflip.userFeatures(someFreeUser);
if(someFreeUser.features.closedBeta === true) {
console.log('Welcome to the Closed Beta!');
}
// Or, just get a single one
if (fflip.userHasFeature(someFreeUser, 'closedBeta')) {
// ... or just check this single feature.
if (fflip.userHasFeature(someFreeUser, 'closedBeta') === true) {
console.log('Welcome to the Closed Beta!');

@@ -42,59 +46,95 @@ }

###Criteria
Criteria are the rules that features can test users against. Each rule takes a user and a data argument to test against, and returns true/false if the user matches that criteria. The data argument can be any type, as long as you handle it correctly in the function you describe.
### Criteria
**Criteria** are the rules that define access to different features. Each criteria takes a user object and some data as arguments, and returns true/false if the user matches that criteria. You will use these criteria to restrict/allow features for different subsets of your userbase.
```javascript
var ExampleCriteriaObject = {
isPaidUser: function(user, isPaid) {
return user.isPaid == isPaid;
var ExampleCriteria = [
{
id: 'isPaidUser', // required
check: function(user, isPaid) { // required
return user.isPaid == isPaid;
}
},
percentageOfUsers: function(user, percent) {
return (user.id % 100 < percent * 100);
{
id: 'percentageOfUsers',
check: function(user, percent) {
return (user.id % 100 < percent * 100);
}
},
allowUserIDs: function(user, idArr) {
return idArr.indexOf(user.id) > -1;
{
id: 'allowUserIDs',
check: function(user, allowedIDs) {
return allowedIDs.indexOf(user.id) > -1;
}
}
}
];
```
###Features
Features contain sets of criteria to test users against. The value associated with the criteria is passed in as the data argument of the criteria function. A user will have a featured enabled if they match all listed criteria, otherwise the feature is disabled. Features can include other optional properties for context. Features are described as follows:
### Features
**Features** represent some special behaviors in your application. They also define a set of criteria to test users against for each feature. When you ask fflip if a feature is enabled for some user, it will check that user against each rule/criteria, and return "true" if the user passes.
Features are described in the following way:
```javascript
var ExampleFeaturesObject = {
paidFeature: {
criteria: {
isPaidUser: true
}
var ExampleFeatures = [
{
id: 'closedBeta', // required
// if `criteria` is in an object, ALL criteria in that set must evaluate to true to enable for user
criteria: {isPaidUser: true, percentageOfUsers: 0.50}
},
closedBeta: {
name: "A Closed Beta",
criteria: {
allowUserIDs: [20,30,80,181]
}
{
id: 'newFeatureRollout',
// if `criteria` is in an array, ANY ONE set of criteria must evaluate to true to enable for user
criteria: [{isPaidUser: true}, {percentageOfUsers: 0.50}]
},
newFeatureRollout: {
name: "A New Feature Rollout",
description: "Rollout of that new feature over the next month",
owner: "FredKSchott", // Remember: These are all optional, only criteria is required
criteria: {
isPaidUser: false,
percentageOfUsers: 0.50
}
}
}
{
id: 'experimentalFeature',
name: 'An Experimental Feature', // user-defined properties are optional but can be used to add important metadata
description: 'Experimental feature still in development, useful for internal development', // user-defined
owner: 'Fred K. Schott <fkschott@gmail.com>', // user-defined
enabled: false, // sets the feature on or off for all users, required unless `criteria` is present instead
},
]
```
##Usage
The value present for each rule is passed in as the data argument to it's criteria function. This allows you to write more general, flexible, reusable rules.
Rule sets & lists can be nested and combined. It can help to think of criteria sets as a group of `AND` operators, and lists as a set of `OR` operators.
#### Veto Criteria
If you'd like to allow wider access to your feature while still preventing a specific group of users, you can use the `$veto` property. If the `$veto` property is present on a member of a criteria list (array), and that member evaluates to false, the entire list will evaluate to false regardless of it's other members.
```javascript
{
// Enabled if user is paid OR in the lucky 50% group of other users currently using a modern browser
criteria: [{isPaidUser: true}, {percentageOfUsers: 0.50, usingModernBrowser: true}]
// Enabled if user is paid OR in the lucky 50% group of other users, BUT ONLY if using a modern browser
criteria: [{isPaidUser: true}, {percentageOfUsers: 0.50}, {usingModernBrowser: true, $veto: true}]
}
```
void config(options) // Configure fflip (see below)
Object userFeatures(user) // Return object of true/false for all features for user
Bool userHasFeature(user, featureName) // Return true/false if featureName is enabled for user
void reload() // Force a reload (if loading features dynamically)
void express(app) // Connect with an Express app or router (see below)
```
Configure __fflip__ using any of the following options:
## Usage
- `.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
- `.reload() -> void`: Force a reload (if loading features dynamically)
- `.express(app) -> void`: Connect with an Express app or router (see below)
### Configuration
Configure fflip using any of the following options:
```javascript
fflip.config({
criteria: {}, // Criteria Object
features: {}, // Features Object | Function (see below)
criteria: {}, // Criteria Array
features: {}, // Features Array | Function (see below)
reload: 30, // Interval for refreshing features, in seconds

@@ -104,4 +144,7 @@ });

###Loading Features Dynamically
__fflip__ also accepts functions for loading features. If __fflip__ is passed a function with no arguments it will call the function and accept the return value. To load asynchronously, pass a function that sends a features object to a callback. __fflip__ will receive the callback and set the data accordingly. In both cases, __fflip__ will save the function and call it again every X seconds, as set by the reload parameter.
### Loading Features Dynamically
fflip also accepts functions for loading features. If fflip is passed a function with no arguments it will call the function and accept the return value. To load asynchronously, pass a function that sends a features object to a callback. fflip will receive the callback and set the data accordingly. In both cases, fflip will save the function and call it again every X seconds, as set by the reload parameter.
```javascript

@@ -112,13 +155,13 @@ // Load Criteria Synchronously

var featuresArr = collection.find().toArray();
/* Process featuresArr -> featuresObj (format described above) */
return featuresObj;
/* Process/Format `featuresArr` if needed (format described above) */
return featuresArr;
}
// Load Features Asynchronously
var getFeaturesAsync = function(fflip_callback) {
var getFeaturesAsync = function(callback) {
var collection = db.collection('features');
collection.find().toArray(function(err, featuresArr) {
/* Handle err
* Process featuresArr -> featuresObj (format described above) */
fflip_callback(featuresObj);
* Process/Format `featuresArr` if needed (format described above) */
callback(featuresArr);
});

@@ -135,13 +178,15 @@ }

##Express Integration
__fflip__ provides easy integration with the popular web framework [Express](https://github.com/visionmedia/express).
Just call ``fflip.express()`` with your Express application or Express 4.0 router to enable the following:
## Express Integration
fflip provides two easy integrations with the popular web framework [Express](http://expressjs.com/).
####A route for manually flipping on/off features
If you have cookies enabled, you can visit ``/fflip/:name/:action`` 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 as in the user's cookie under the name `fflip`.
#### fflip.expressMiddleware()
####req.fflip
A __fflip__ object is attached to the request, and includes the following functionality:
```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 = {

@@ -153,18 +198,28 @@ setForUser(user): Given a user, attaches the features object to the request (at req.fflip.features). Make sure you do this before calling has()!

####Use fflip in your templates
*NOTE: This will only be populated if you call `req.fflip.setForUser` beforehand.*
The __fflip__ Express middleware includes 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 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
*NOTE: This will only be populated if you call `req.fflip.setForUser` beforehand.*
The __fflip__ Express middleware also includes 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>``.
**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>`.
####Low-level integration
If you need a finer-grained Express integration, such as changing the URL for manual overrides, adding security middleware, or applying middleware on a subset of routes, you can use ``express_middleware`` and ``express_route`` directly.
#### 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);
```
app.use(fflip.express_middleware);
app.get('/custom_path/:name/:action', fflip.express_route);
**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
## Special Thanks
Original logo designed by <a href="http://thenounproject.com/Luboš Volkov" target="_blank">Luboš Volkov</a>

@@ -21,3 +21,3 @@ 'use strict';

return true;
};
}

@@ -28,13 +28,22 @@ var sandbox = sinon.sandbox.create();

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: {
fEmpty: {},
fOpen: {
],
features: [
{
id: 'fEmpty'
},
{
id: 'fOpen',
name: 'fOpen',

@@ -46,3 +55,4 @@ description: 'true for all users',

},
fClosed: {
{
id: 'fClosed',
criteria: {

@@ -52,8 +62,34 @@ c1: false

},
fEval: {
{
id: 'fEval',
criteria: {
c1: true,
c2: 'abc'
}
},
{
id: 'fEvalOr',
criteria: [
{c1: false},
{c2: 'abc'},
{c2: 'efg'}
]
},
{
id: 'fEvalComplex',
criteria: [
{c1: false, c2: 'abc'},
{c1: true, c2: 'abc'},
[{c1: false, c2: 'xyz'}, {c1: true, c2: 'efg'}]
]
},
{
id: 'fEvalVeto',
criteria: [
{c1: false},
{c2: 'abc'},
{c2: 'efg', $veto: true}
]
}
},
],
reload: 0

@@ -65,2 +101,5 @@ };

};
var userEFG = {
flag: 'efg'
};
var userXYZ = {

@@ -82,6 +121,14 @@ flag: 'xyz'

it('should set features if given static feature object', function(){
it('should set features if given static feature array', function(){
fflip._features = {};
fflip.config(configData);
assert.equal(configData.features, fflip._features);
assert.deepEqual(fflip._features, {
fEmpty: configData.features[0],
fOpen: configData.features[1],
fClosed: configData.features[2],
fEval: configData.features[3],
fEvalOr: configData.features[4],
fEvalComplex: configData.features[5],
fEvalVeto: configData.features[6]
});
});

@@ -93,4 +140,13 @@

};
fflip.config({features: loadSyncronously});
assert.equal(configData.features, fflip._features);
fflip._features = {};
fflip.config({features: loadSyncronously, criteria: configData.criteria});
assert.deepEqual(fflip._features, {
fEmpty: configData.features[0],
fOpen: configData.features[1],
fClosed: configData.features[2],
fEval: configData.features[3],
fEvalOr: configData.features[4],
fEvalComplex: configData.features[5],
fEvalVeto: configData.features[6]
});
});

@@ -101,12 +157,23 @@

callback(configData.features);
assert.equal(configData.features, fflip._features);
assert.deepEqual(fflip._features, {
fEmpty: configData.features[0],
fOpen: configData.features[1],
fClosed: configData.features[2],
fEval: configData.features[3],
fEvalOr: configData.features[4],
fEvalComplex: configData.features[5],
fEvalVeto: configData.features[6]
});
done();
};
fflip.config({features: loadAsyncronously});
fflip.config({features: loadAsyncronously, criteria: configData.criteria});
});
it('should set criteria if given static criteria object', function(){
it('should set criteria if given static criteria array', function(){
fflip._criteria = {};
fflip.config(configData);
assert.equal(configData.criteria, fflip._criteria);
assert.deepEqual(fflip._criteria, {
c1: configData.criteria[0].check,
c2: configData.criteria[1].check
});
});

@@ -122,3 +189,4 @@

describe('reload()', function(){
describe('reload()', function() {
beforeEach(function() {

@@ -134,3 +202,3 @@

};
fflip.config({features: loadAsyncronously, reload: 0.2});
fflip.config({features: loadAsyncronously, reload: 0.2, criteria: configData.criteria});
});

@@ -146,3 +214,3 @@

};
fflip.config({features: loadAsyncronously});
fflip.config({features: loadAsyncronously, criteria: configData.criteria});
testReady = true;

@@ -154,3 +222,3 @@ fflip.reload();

describe('userHasFeature()', function(){
describe('userHasFeature()', function() {

@@ -169,2 +237,4 @@ beforeEach(function() {

// TODO(fks) 03-14-2016: (Edge Case) Test that an empty criteria object disables a feature
it('should return false if all feature critieria evaluates to false', function(){

@@ -175,7 +245,31 @@ assert.equal(false, fflip.userHasFeature(userABC, 'fClosed'));

it('should return true if one feature critieria evaluates to true', function(){
assert.equal(true, fflip.userHasFeature(userABC, 'fOpen'));
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'));
});
it('should return true if all feature critieria evaluates to true', function(){
assert.equal(true, fflip.userHasFeature(userABC, 'fEval'));
});
it('should return false if zero feature critieria evaluates to true', function(){
assert.equal(false, fflip.userHasFeature(userXYZ, 'fEvalOr'));
});
it('should return true if one feature critieria evaluates to true', function(){
assert.equal(true, fflip.userHasFeature(userABC, 'fEvalOr'));
});
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'));
});
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'));
});
});

@@ -191,6 +285,33 @@

var featuresABC = fflip.userFeatures(userABC);
assert.equal(featuresABC.fEmpty, false);
assert.equal(featuresABC.fOpen, true);
assert.equal(featuresABC.fClosed, false);
assert.equal(featuresABC.fEval, true);
assert.deepEqual(featuresABC, {
fEmpty: false,
fOpen: true,
fClosed: false,
fEval: true,
fEvalOr: true,
fEvalComplex: true,
fEvalVeto: false
});
var featuresEFG = fflip.userFeatures(userEFG);
assert.deepEqual(featuresEFG, {
fEmpty: false,
fOpen: true,
fClosed: false,
fEval: false,
fEvalOr: true,
fEvalComplex: true,
fEvalVeto: true
});
var featuresXYZ = fflip.userFeatures(userXYZ);
assert.deepEqual(featuresXYZ, {
fEmpty: false,
fOpen: true,
fClosed: false,
fEval: false,
fEvalOr: false,
fEvalComplex: false,
fEvalVeto: false
});
});

@@ -229,3 +350,3 @@

var me = this;
fflip.express_middleware(this.reqMock, this.resMock, function() {
fflip.expressMiddleware(this.reqMock, this.resMock, function() {
assert(me.reqMock.fflip);

@@ -239,3 +360,3 @@ assert(me.reqMock.fflip._flags, me.reqMock.cookies.fflip);

var me = this;
fflip.express_middleware(this.reqMock, this.resMock, function() {
fflip.expressMiddleware(this.reqMock, this.resMock, function() {
assert.doesNotThrow(function() {

@@ -250,3 +371,3 @@ me.resMock.render('testview');

var me = this;
fflip.express_middleware(this.reqMock, this.resMock, function() {
fflip.expressMiddleware(this.reqMock, this.resMock, function() {
var features = {features : { fClosed: true }};

@@ -270,3 +391,3 @@ var featuresString = JSON.stringify(features);

var spy = sandbox.spy(fflip, 'userFeatures');
fflip.express_middleware(this.reqMock, this.resMock, function() {
fflip.expressMiddleware(this.reqMock, this.resMock, function() {
me.reqMock.fflip.setForUser(userXYZ);

@@ -282,3 +403,3 @@ assert(fflip.userFeatures.calledOnce);

var me = this;
fflip.express_middleware(this.reqMock, this.resMock, function() {
fflip.expressMiddleware(this.reqMock, this.resMock, function() {
me.reqMock.fflip.setForUser(userXYZ);

@@ -295,3 +416,3 @@ assert.strictEqual(me.reqMock.fflip.has('fOpen'), true);

assert.throws(function() {
fflip.express_middleware(this.reqMock, this.resMock, function() {
fflip.expressMiddleware(this.reqMock, this.resMock, function() {
me.reqMock.fflip.has('fOpen');

@@ -305,3 +426,3 @@ });

var consoleErrorStub = sandbox.stub(console, 'error'); // Supress Error Output
fflip.express_middleware(this.reqMock, this.resMock, function() {
fflip.expressMiddleware(this.reqMock, this.resMock, function() {
assert.ok(isObjectEmpty(me.reqMock.fflip.features));

@@ -315,3 +436,3 @@ done();

fflip.express(this.appMock);
assert.ok(this.appMock.use.calledWith(fflip.express_middleware));
assert.ok(this.appMock.use.calledWith(fflip.expressMiddleware));
});

@@ -321,3 +442,3 @@

fflip.express(this.appMock);
assert.ok(this.appMock.get.calledWith('/fflip/:name/:action', fflip.express_route));
assert.ok(this.appMock.get.calledWith('/fflip/:name/:action', fflip.expressRoute));
});

@@ -344,5 +465,4 @@

it('should propogate a 404 error if feature does not exist', function(done) {
var next = sandbox.stub();
this.reqMock.params.name = 'doesnotexist';
fflip.express_route(this.reqMock, this.resMock, function(err) {
fflip.expressRoute(this.reqMock, this.resMock, function(err) {
assert(err);

@@ -356,5 +476,4 @@ assert(err.fflip);

it('should propogate a 500 error if cookies are not enabled', function(done) {
var next = sandbox.stub();
this.reqMock.cookies = null;
fflip.express_route(this.reqMock, this.resMock, function(err) {
fflip.expressRoute(this.reqMock, this.resMock, function(err) {
assert(err);

@@ -368,3 +487,3 @@ assert(err.fflip);

it('should set the right cookie flags', function() {
fflip.express_route(this.reqMock, this.resMock);
fflip.expressRoute(this.reqMock, this.resMock);
assert(this.resMock.cookie.calledWithMatch('fflip', {fClosed: true}, { maxAge: 900000 }));

@@ -377,3 +496,3 @@ });

fflip.maxCookieAge = oneMonthMs;
fflip.express_route(this.reqMock, this.resMock);
fflip.expressRoute(this.reqMock, this.resMock);
fflip.maxCookieAge = oldMaxCookieAge;

@@ -385,3 +504,3 @@

it('should send back 200 json response on successful call', function() {
fflip.express_route(this.reqMock, this.resMock);
fflip.expressRoute(this.reqMock, this.resMock);
assert(this.resMock.json.calledWith(200));

@@ -413,3 +532,3 @@ });

// it('should call res.cookie() on successful request', function() {
// self.express_route(this.reqMock, this.resMock);
// self.expressRoute(this.reqMock, this.resMock);
// assert(res.cookie.calledWith('fflip'));

@@ -416,0 +535,0 @@ // });

Sorry, the diff of this file is not supported yet

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc