├── .eslintignore ├── .eslintrc ├── .gitignore ├── .nvmrc ├── README.md ├── docker-compose.yml ├── example ├── README.md ├── public │ └── index.html ├── server.js └── util.js ├── index.js ├── lib └── messenger.js ├── models └── toggle.js ├── package.json └── test ├── mocha.opts └── toggles.spec.js /.eslintignore: -------------------------------------------------------------------------------- 1 | docs 2 | node-modules 3 | .npm-init.js 4 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "eslint:recommended", 3 | "env": { 4 | "es6": true, 5 | "node": true, 6 | "mocha": true 7 | }, 8 | "parserOptions": { 9 | "ecmaVersion": 6, 10 | "sourceType": "module" 11 | }, 12 | "rules": { 13 | "array-bracket-spacing": [2, "never"], 14 | "arrow-parens": [2, "always"], 15 | "arrow-spacing": [2, {"before": true, "after": true}], 16 | "brace-style": [2, "1tbs", {"allowSingleLine": false}], 17 | "comma-spacing": [2, {"before": false, "after": true}], 18 | "curly": [2, "all"], 19 | "indent": [2, 4, {"SwitchCase": 1, "VariableDeclarator": 1}], 20 | "key-spacing": [2, {"beforeColon": false, "afterColon": true}], 21 | "keyword-spacing": 2, 22 | "linebreak-style": [2, "unix"], 23 | "no-console": 0, 24 | "no-trailing-spaces": 2, 25 | "object-curly-spacing": [2, "never"], 26 | "one-var": [2, "never"], 27 | "prefer-template": 2, 28 | "quote-props": [2, "as-needed"], 29 | "quotes": [2, "single"], 30 | "semi": [2, "always"], 31 | "space-before-blocks": [2, "always"], 32 | "space-before-function-paren": [2, {"anonymous": "always", "named": "never"}], 33 | "space-infix-ops": [2, {"int32Hint": false}] 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .c9 2 | .idea 3 | .vscode 4 | docs 5 | node_modules 6 | npm-debug.log 7 | coverage 8 | .aws/credentials 9 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 4 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # aws-feature-toggles 2 | 3 | This is a module that provides feature toggle capability for distributed systems, using only AWS services (DynamoDB, SNS, SQS). 4 | 5 | Features can be enabled based on a name, and a regular expression. For example: 6 | 7 | ``` 8 | const config = { 9 | dynamodb: { 10 | region: 'us-west-2', 11 | endpoint: 'http://localhost:8000' 12 | }, 13 | sns: { 14 | region: 'us-west-2' 15 | }, 16 | sqs: { 17 | region: 'us-west-2' 18 | }, 19 | toggles: { 20 | system: 'mocha-test' 21 | } 22 | } 23 | const toggles = require('aws-feature-toggles')(config); 24 | 25 | toggles.init(() => { 26 | toggles.put('testFeature', ['sarah.*'], () => { // notice the array of regex patterns 27 | const enabled = toggles.check('testFeature', 'sarah@gmail.com'); 28 | // enabled should be true, because the email address matches the regular expression 29 | }); 30 | }); 31 | ``` 32 | 33 | Of course, the target can be anything, not just email addresses. Use ```['.*']``` to match anything. 34 | 35 | - **config.toggles** is used internally by this module. 36 | - **config.dynamodb** is passed into the underlying AWS SDK. See http://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/DynamoDB.html for options. 37 | - **config.sns** is passed into the underlying AWS SDK. See http://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/SNS.html for options. 38 | - **config.sqs** is passed into the underlying AWS SDK. See http://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/SQS.html for options. 39 | 40 | ## Prep 41 | Log into your AWS console and create a new user in IAM. Make sure you save the users credientials. 42 | Attach the User Policies for Amazon SQS Full Access and Amazon SNS Full Access. 43 | 44 | Create ~/.aws/credentials 45 | 46 | Add the access key and secret access key for the IAM user you just created. 47 | ``` 48 | [snssqs] 49 | aws_access_key_id = 50 | aws_secret_access_key = 51 | ``` 52 | 53 | ## Install Packages 54 | 55 | npm install 56 | 57 | ### Testing 58 | 59 | To start a local copy of dynamoDB for testing, run ```docker-compose up -d```. See docker-compose.yml. 60 | 61 | This module requires AWS credentials to run integration tests. Run as follows (using your own AWS profile name): 62 | 63 | ``` 64 | AWS_PROFILE= npm test 65 | ``` 66 | 67 | This module uses the debug module. If you want to see more of what it is doing internally, run with the DEBUG variable like so: 68 | ``` 69 | DEBUG=* AWS_PROFILE= npm test 70 | ``` 71 | 72 | ### Inspiration 73 | 74 | Borrowed heavily from https://github.com/markcallen/snssqs for SNS and SQS messaging components. 75 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | db: 2 | image: tutum/dynamodb:latest 3 | expose: 4 | - "8000" 5 | ports: 6 | - "8000:8000" 7 | volumes: 8 | - /usr/local/var/dynamodb:/data 9 | -------------------------------------------------------------------------------- /example/README.md: -------------------------------------------------------------------------------- 1 | ## running the examples 2 | 3 | To run the examples, you'll need to open 2 terminals and open a browser to http://localhost:3000/ 4 | 5 | ``` 6 | # run the server in the first terminal 7 | DEBUG=aws-feature-toggles:* AWS_PROFILE= node example/server.js 8 | 9 | # run the utility in the second terminal 10 | AWS_PROFILE=snssqs node example/util.js 11 | ``` 12 | 13 | ####notes:#### 14 | The page is hard coded to use the username "bob@gmail.com". 15 | 16 | The utility allows you to enable and disable 3 demo features. You use a regular expression to specify who is allowed to access each feature. The system allows multiple regular expressions for matching multiple users, etc. 17 | 18 | The page should refresh automatically when you toggle the features "blueText" or "showPhoto", using a websocket. 19 | 20 | The page also has a link to a "/test" route, behind the toggle "testRoute" that can be toggled on and off. (This requires a refresh to see.) 21 | -------------------------------------------------------------------------------- /example/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 43 | 44 | 45 |

Hello World!

46 |

Run the util.js to try changing things!

47 | 48 |

A test route that can be enabled/disabled

49 | 50 | -------------------------------------------------------------------------------- /example/server.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const region = 'us-west-2'; 4 | const config = { 5 | dynamodb: { 6 | region, 7 | endpoint: 'http://localhost:8000' 8 | }, 9 | sns: {region}, 10 | sqs: {region}, 11 | toggles: { 12 | system: 'aws-feature-toggles-example' // this needs to be unique per environment! 13 | } 14 | }; 15 | const toggles = require('../index')(config); 16 | 17 | const express = require('express'); 18 | const app = express(); 19 | app.use(express.static('example/public')); 20 | // express route example: 21 | // will enable the route when toggled on 22 | function testRoute(req, res, next) { 23 | if (toggles.check('testRoute', 'public')) { 24 | return res.status(200).send('Route enabled'); 25 | } 26 | next(); 27 | } 28 | app.use('/test', testRoute); 29 | const server = require('http').createServer(app); 30 | 31 | // socket.io example: 32 | // use the utility to enable things on the page without a page refresh 33 | const io = require('socket.io')(server); 34 | toggles.init((err) => { 35 | if (err) { 36 | throw err; 37 | } 38 | server.listen(3000); 39 | }); 40 | 41 | io.on('connection', function (client) { 42 | console.log('client connected'); 43 | 44 | // when client requests the status of a feature 45 | // respond with the status 46 | client.on('toggles:check', function (data) { 47 | const featureName = data.featureName; 48 | const target = data.target; 49 | console.log('client sent toggles:check', featureName, target); 50 | if (featureName && target) { 51 | io.emit('toggles:check', { 52 | featureName, 53 | target, 54 | response: toggles.check(featureName, target) 55 | }); 56 | } 57 | }); 58 | client.on('disconnect', function () { 59 | console.log('client disconnected'); 60 | }); 61 | toggles.on('toggles:loaded', function () { 62 | console.log('server got toggles:loaded, emitting to client'); 63 | io.emit('toggles:reload'); 64 | }); 65 | }); 66 | -------------------------------------------------------------------------------- /example/util.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const action = process.argv[2]; 4 | const feature = process.argv[3]; 5 | const target = process.argv[4]; 6 | 7 | function usage() { 8 | console.log('Usage: node examples/util.js [action] [feature] [regex]'); 9 | console.log(' node examples/util.js enable blueText "bob.*"'); 10 | console.log(' node examples/util.js disable blueText "bob.*"'); 11 | console.log(' node examples/util.js enable showPhoto "bob.*"'); 12 | console.log(' node examples/util.js disable showPhoto "bob.*"'); 13 | console.log(' node examples/util.js enable testRoute ".*"'); 14 | console.log(' node examples/util.js disable testRoute ".*"'); 15 | process.exit(); 16 | } 17 | if (!action || !feature || !target) { 18 | usage(); 19 | } else if (action !== 'enable' && action !== 'disable') { 20 | usage(); 21 | } else if (feature !== 'blueText' && feature !== 'showPhoto' && feature !== 'testRoute') { 22 | usage(); 23 | } 24 | 25 | const region = 'us-west-2'; 26 | const config = { 27 | dynamodb: { 28 | region, 29 | endpoint: 'http://localhost:8000' 30 | }, 31 | sns: {region}, 32 | sqs: {region}, 33 | toggles: { 34 | system: 'aws-feature-toggles-example' // this needs to be unique per environment! 35 | } 36 | }; 37 | 38 | console.log(action, feature, target); 39 | const toggles = require('../index')(config); 40 | toggles.init((err) => { 41 | if (err) { 42 | throw err; 43 | } 44 | const currentSetting = toggles.toggles[feature] || []; 45 | const i = currentSetting.indexOf(target); 46 | if (action === 'enable') { 47 | if (i < 0) { 48 | currentSetting.push(target); 49 | } 50 | } else { 51 | currentSetting.splice(i, 1); 52 | } 53 | toggles.put(feature, currentSetting, (err) => { 54 | if (err) { 55 | console.error(err); 56 | } 57 | console.log('Updated', feature, currentSetting); 58 | }); 59 | }); 60 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const pkg = require('./package'); 4 | const debug = require('debug')(pkg.name); 5 | const ToggleDB = require('./models/toggle.js'); 6 | const Messenger = require('./lib/messenger'); 7 | const uuid = require('node-uuid'); 8 | 9 | function Toggles(config) { 10 | if (!(this instanceof Toggles)) { 11 | return new Toggles(config); 12 | } 13 | this.model = new ToggleDB(config); 14 | config.instanceId = uuid.v4(); 15 | this.messenger = new Messenger(config); 16 | this.toggles = {}; 17 | this.instanceId = config.instanceId; 18 | } 19 | 20 | function getRegex(string) { 21 | return new RegExp(string); 22 | } 23 | 24 | Toggles.prototype.init = function (cb) { 25 | debug('init'); 26 | this.messenger.init((err) => { 27 | if (err) { 28 | return cb(err); 29 | } 30 | this.messenger.on('toggles:reload', (messageId, instanceId) => { 31 | debug(`Got msg id: ${messageId} from ${instanceId}, reloading`); 32 | this.load(); 33 | }); 34 | this.messenger.subscribe(); 35 | this.model.init((err) => { 36 | if (err) { 37 | return cb(err); 38 | } 39 | return this.load(cb); 40 | }); 41 | }); 42 | }; 43 | 44 | Toggles.prototype.load = function (cb) { 45 | debug('load'); 46 | this.model.load((err, data) => { 47 | this.toggles = data; 48 | if (cb) { 49 | return cb(); 50 | } 51 | }); 52 | }; 53 | 54 | Toggles.prototype.put = function (feature, targets, cb) { 55 | debug('put'); 56 | this.model.put(feature, targets, (err) => { 57 | if (err) { 58 | return cb(err); 59 | } 60 | this.toggles[feature] = targets; 61 | this.messenger.publish(this.instanceId, cb); 62 | }); 63 | }; 64 | 65 | Toggles.prototype.check = function (feature, target) { 66 | debug('check'); 67 | debug(`toggles: ${JSON.stringify(this.toggles)}`); 68 | if (this.toggles[feature]) { 69 | var status = false; 70 | this.toggles[feature].forEach((t) => { 71 | debug(`Checking ${feature}, ${target}, ${t}`); 72 | var rt = getRegex(t); 73 | if (target.match(rt)) { 74 | status = true; 75 | } 76 | }); 77 | return status; 78 | } 79 | return false; 80 | }; 81 | 82 | module.exports = Toggles; 83 | -------------------------------------------------------------------------------- /lib/messenger.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const util = require('util'); 4 | const EventEmitter = require('events').EventEmitter; 5 | const AWS = require('aws-sdk'); 6 | const pkg = require('../package'); 7 | const debug = require('debug')(`${pkg.name}:messenger`); 8 | const async = require('async'); 9 | const NodeCache = require('node-cache'); 10 | 11 | function Messenger(config) { 12 | this.sns = new AWS.SNS(config.sns); 13 | this.sqs = new AWS.SQS(config.sqs); 14 | this.config = config; 15 | this.cache = new NodeCache({stdTTL: 60, checkperiod: 20}); 16 | this.instanceId = config.instanceId; 17 | EventEmitter.call(this); 18 | } 19 | 20 | util.inherits(Messenger, EventEmitter); 21 | 22 | Messenger.prototype.subscribe = function subscribe() { 23 | const messenger = this; 24 | const receiveMessageParams = { 25 | QueueUrl: this.config.QueueUrl, 26 | MaxNumberOfMessages: 1 27 | }; 28 | 29 | function getMessages() { 30 | messenger.sqs.receiveMessage(receiveMessageParams, receiveMessageCallback); 31 | } 32 | 33 | function receiveMessageCallback(err, data) { 34 | if (data && data.Messages && data.Messages.length > 0) { 35 | data.Messages.forEach((messageObject) => { 36 | const messageBody = JSON.parse(messageObject.Body); 37 | const messageId = messageBody.MessageId; 38 | const instanceId = messageBody.Message; 39 | messenger.cache.get(messageId, (err, value) => { 40 | if (!err && value === undefined && instanceId !== messenger.instanceId) { 41 | debug(`Received non-cached messageId: ${messageId}`); 42 | messenger.cache.set(messageId, instanceId); 43 | messenger.emit('toggles:reload', messageId, instanceId); 44 | } 45 | }); 46 | }); 47 | 48 | } 49 | setTimeout(getMessages, 1000).unref(); 50 | } 51 | getMessages(); 52 | }; 53 | 54 | Messenger.prototype.publish = function publish(message, cb) { 55 | const publishParams = { 56 | TopicArn: this.config.TopicArn, 57 | Message: message 58 | }; 59 | 60 | debug(`Publishing message ${JSON.stringify(publishParams)}`); 61 | 62 | this.sns.publish(publishParams, cb); 63 | }; 64 | 65 | Messenger.prototype.init = function init(cb) { 66 | const messenger = this; 67 | // create sns topic 68 | function createTopic(cb) { 69 | messenger.sns.createTopic({ 70 | Name: messenger.config.toggles.system 71 | }, function (err, result) { 72 | if (err) { 73 | debug(`Error creating Topic ${messenger.config.toggles.system}: ${err}`); 74 | return cb(err); 75 | } 76 | debug(`Create SNS Topic result: ${JSON.stringify(result)}`); 77 | messenger.config.TopicArn = result.TopicArn; 78 | cb(); 79 | }); 80 | } 81 | // create sqs queue 82 | function createQueue(cb) { 83 | messenger.sqs.createQueue({ 84 | QueueName: messenger.config.toggles.system, 85 | Attributes: { 86 | VisibilityTimeout: '0', // allows other systems to get the message immediately 87 | ReceiveMessageWaitTimeSeconds: '20', 88 | MessageRetentionPeriod: '60' 89 | } 90 | }, function (err, result) { 91 | if (err) { 92 | debug(`Error creating Queue: ${err}`); 93 | return cb(err); 94 | } 95 | debug(`Create SQS Queue result: ${JSON.stringify(result)}`); 96 | messenger.config.QueueUrl = result.QueueUrl; 97 | cb(); 98 | }); 99 | } 100 | // get queue attributes 101 | function getQueueAttr(cb) { 102 | messenger.sqs.getQueueAttributes({ 103 | QueueUrl: messenger.config.QueueUrl, 104 | AttributeNames: ['QueueArn'] 105 | }, function (err, result) { 106 | if (err) { 107 | debug(`Error getting queue attributes: ${err}`); 108 | return cb(err); 109 | } 110 | debug(`Get SQS Queue Attributes result: ${JSON.stringify(result)}`); 111 | messenger.config.QueueArn = result.Attributes.QueueArn; 112 | cb(); 113 | }); 114 | 115 | } 116 | // subscribe queue to sns topic 117 | function snsSubscribe(cb) { 118 | messenger.sns.subscribe({ 119 | TopicArn: messenger.config.TopicArn, 120 | Protocol: 'sqs', 121 | Endpoint: messenger.config.QueueArn 122 | }, function (err, result) { 123 | if (err) { 124 | debug(`Error subscribing to topic: ${err}`); 125 | return cb(err); 126 | } 127 | debug(`SNS Subscribe result: ${JSON.stringify(result)}`); 128 | cb(); 129 | }); 130 | 131 | } 132 | // update queue attributes to set policy 133 | function setQueueAttr(cb) { 134 | const queueUrl = messenger.config.QueueUrl; 135 | const topicArn = messenger.config.TopicArn; 136 | const sqsArn = messenger.config.QueueArn; 137 | 138 | const attributes = { 139 | Version: '2008-10-17', 140 | Id: `${sqsArn}/SQSDefaultPolicy`, 141 | Statement: [{ 142 | Sid: `Sid${new Date().getTime()}`, 143 | Effect: 'Allow', 144 | Principal: { 145 | AWS: '*' 146 | }, 147 | Action: 'SQS:SendMessage', 148 | Resource: sqsArn, 149 | Condition: { 150 | ArnEquals: { 151 | 'aws:SourceArn': topicArn 152 | } 153 | } 154 | }] 155 | }; 156 | 157 | messenger.sqs.setQueueAttributes({ 158 | QueueUrl: queueUrl, 159 | Attributes: { 160 | Policy: JSON.stringify(attributes) 161 | } 162 | }, function (err, result) { 163 | if (err) { 164 | debug(`Error setting queue attributes: ${err}`); 165 | return cb(err); 166 | } 167 | debug(`SQS Set Queue Attributes result: ${JSON.stringify(result)}`); 168 | cb(); 169 | }); 170 | 171 | } 172 | 173 | async.series([createTopic, createQueue, getQueueAttr, snsSubscribe, setQueueAttr], cb); 174 | }; 175 | 176 | module.exports = Messenger; 177 | -------------------------------------------------------------------------------- /models/toggle.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const pkg = require('../package'); 4 | const debug = require('debug')(`${pkg.name}:model`); 5 | const AWS = require('aws-sdk'); 6 | 7 | const tableSchema = { 8 | TableName: 'features', 9 | KeySchema: [ 10 | { 11 | AttributeName: 'system', 12 | KeyType: 'HASH' // Partition key 13 | }, 14 | { 15 | AttributeName: 'feature', 16 | KeyType: 'RANGE' // Sort key 17 | } 18 | ], 19 | AttributeDefinitions: [ 20 | { 21 | AttributeName: 'system', 22 | AttributeType: 'S' // String 23 | }, 24 | { 25 | AttributeName: 'feature', 26 | AttributeType: 'S' // String 27 | } 28 | ], 29 | ProvisionedThroughput: { 30 | ReadCapacityUnits: 100, 31 | WriteCapacityUnits: 10 32 | } 33 | }; 34 | 35 | function ToggleDB(config) { 36 | this.config = config; 37 | this.dbClient = new AWS.DynamoDB(config.dynamodb); 38 | this.docClient = new AWS.DynamoDB.DocumentClient(config.dynamodb); 39 | } 40 | 41 | ToggleDB.prototype.createTable = function createTable(cb) { 42 | this.dbClient.createTable(tableSchema, cb); 43 | }; 44 | 45 | ToggleDB.prototype.describeTable = function describeTable(cb) { 46 | this.dbClient.describeTable({TableName: tableSchema.TableName}, cb); 47 | }; 48 | 49 | ToggleDB.prototype.init = function init(cb) { 50 | this.describeTable((err) => { 51 | if (err && err.name === 'ResourceNotFoundException') { 52 | return this.createTable(cb); 53 | } else if (err) { 54 | return cb(err); 55 | } 56 | return cb(); 57 | }); 58 | }; 59 | 60 | ToggleDB.prototype.put = function put(feature, targets, cb) { 61 | const params = { 62 | TableName: tableSchema.TableName, 63 | Item: { 64 | system: this.config.toggles.system, 65 | feature: feature, 66 | targets: targets 67 | } 68 | }; 69 | debug(`put ${JSON.stringify(params)}`); 70 | this.docClient.put(params, cb); 71 | }; 72 | 73 | ToggleDB.prototype.load = function load(cb) { 74 | debug('Loading toggles from dynamoDB'); 75 | this.docClient.scan({TableName: tableSchema.TableName}, (err, data) => { 76 | if (err) { 77 | return cb(err); 78 | } 79 | debug(`data ${JSON.stringify(data)}`); 80 | const response = {}; 81 | data.Items.forEach((item) => { 82 | if (item.system === this.config.toggles.system) { 83 | response[item.feature] = item.targets; 84 | } 85 | }); 86 | cb(null, response); 87 | }); 88 | }; 89 | 90 | module.exports = ToggleDB; 91 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "aws-feature-toggles", 3 | "version": "1.0.0", 4 | "description": "Feature toggles for distributed systems. Requires AWS services.", 5 | "main": "index.js", 6 | "scripts": { 7 | "coverage": "istanbul cover _mocha -- --recursive test/", 8 | "lint": "eslint .", 9 | "test": "_mocha" 10 | }, 11 | "keywords": [ 12 | "feature", 13 | "toggles", 14 | "flags", 15 | "flipper", 16 | "aws" 17 | ], 18 | "author": "Ian Patton ", 19 | "license": "MIT", 20 | "dependencies": { 21 | "async": "^2.1.4", 22 | "aws-sdk": "^2.7.13", 23 | "debug": "^2.4.1", 24 | "node-cache": "^4.1.0", 25 | "node-uuid": "^1.4.7" 26 | }, 27 | "engines": { 28 | "node": ">=4.0.0" 29 | }, 30 | "devDependencies": { 31 | "chai": "^3.5.0", 32 | "eslint": "^3.12.1", 33 | "istanbul": "^0.4.5", 34 | "mocha": "^3.2.0", 35 | "sinon": "^1.17.6" 36 | }, 37 | "directories": { 38 | "test": "test" 39 | }, 40 | "repository": { 41 | "type": "git", 42 | "url": "git+https://github.com/cnnlabs/aws-feature-toggles.git" 43 | }, 44 | "bugs": { 45 | "url": "https://github.com/cnnlabs/aws-feature-toggles/issues" 46 | }, 47 | "homepage": "https://github.com/cnnlabs/aws-feature-toggles#readme" 48 | } 49 | -------------------------------------------------------------------------------- /test/mocha.opts: -------------------------------------------------------------------------------- 1 | --no-timeouts 2 | -------------------------------------------------------------------------------- /test/toggles.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const sinon = require('sinon'); 4 | const chai = require('chai'); 5 | chai.should(); 6 | 7 | const config = {}; 8 | 9 | config.dynamodb = { 10 | region: 'us-west-2', 11 | endpoint: 'http://localhost:8000' 12 | }; 13 | 14 | config.sns = { 15 | region: 'us-west-2', 16 | }; 17 | 18 | config.sqs = { 19 | region: 'us-west-2', 20 | }; 21 | 22 | config.toggles = { 23 | system: 'aws-feature-toggles-test' 24 | }; 25 | 26 | const toggles = require('../index')(config); 27 | 28 | 29 | describe('feature toggles', function () { 30 | 31 | after(function (cb) { 32 | const AWS = require('aws-sdk'); 33 | const dbClient = new AWS.DynamoDB(config.dynamodb); 34 | dbClient.deleteTable({TableName: 'features'}, cb); 35 | }); 36 | 37 | it('should initialize if table does not exist', function (done) { 38 | toggles.init((err) => { 39 | done(err); 40 | }); 41 | }); 42 | 43 | it('should initialize if table does exist', function (done) { 44 | toggles.init((err) => { 45 | done(err); 46 | }); 47 | }); 48 | 49 | it('should create toggles', function (done) { 50 | toggles.put('testFeature', ['sarah.*'], (err) => { 51 | done(err); 52 | }); 53 | }); 54 | 55 | it('should return true if a toggle is enabled', function () { 56 | const enabled = toggles.check('testFeature', 'sarah@gmail.com'); 57 | (enabled).should.be.true; 58 | }); 59 | 60 | it('should return false if a toggle is not enabled', function () { 61 | const enabled = toggles.check('testFeature', 'bob@gmail.com'); 62 | (enabled).should.be.false; 63 | }); 64 | 65 | it('should return false if a toggle does not exist', function () { 66 | const enabled = toggles.check('testFeature2', 'bob@gmail.com'); 67 | (enabled).should.be.false; 68 | }); 69 | 70 | it('should update toggles', function (done) { 71 | toggles.put('testFeature', ['bob.*'], () => { 72 | const enabled = toggles.check('testFeature', 'bob@gmail.com'); 73 | (enabled).should.be.true; 74 | done(); 75 | }); 76 | }); 77 | 78 | it('should load data', function (done) { 79 | toggles.load((err) => { 80 | done(err); 81 | }); 82 | }); 83 | 84 | describe('listening for changes from other systems', function () { 85 | const toggles2 = require('../index')(config); 86 | sinon.spy(toggles2, 'load'); 87 | 88 | it('should process reload messages', function (done) { 89 | toggles2.init(() => { 90 | toggles.put('testFeature3', ['sarah.*'], (err) => { 91 | (toggles2.load.callCount === 2).should.be.true; 92 | done(err); 93 | }); 94 | }); 95 | }); 96 | 97 | it('should continue to process after 20 secs', function (done) { 98 | setTimeout(function () { 99 | toggles.put('testFeature4', ['sarah.*'], (err) => { 100 | setTimeout(function () { 101 | (toggles2.load.callCount >= 4).should.be.true; 102 | done(err); 103 | }, 3 * 1000); 104 | }); 105 | }, 21 * 1000); 106 | }); 107 | }); 108 | }); 109 | --------------------------------------------------------------------------------