├── .gitignore ├── LICENSE ├── README.md ├── example └── index.js ├── package.json ├── src ├── handlers │ ├── delay.js │ ├── error.js │ ├── handlers.js │ └── through.js ├── index.js └── util │ ├── random-element.js │ └── run-on-prop.js └── tests ├── handlers └── errors-spec.js └── index-spec.js /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | 16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 17 | .grunt 18 | 19 | # Compiled binary addons (http://nodejs.org/api/addons.html) 20 | build/Release 21 | 22 | # Dependency directory 23 | # Commenting this out is preferred by some people, see 24 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git- 25 | node_modules 26 | 27 | # Users Environment Variables 28 | .lock-wscript 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Dominic Barker 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | connect-chaos 2 | ============= 3 | 4 | [![Circle CI](https://circleci.com/gh/Dakuan/connect-chaos.svg?style=svg&circle-token=be329dbcfa94b3635df9ae15d1f89133e5b05a95)](https://circleci.com/gh/Dakuan/connect-chaos) 5 | 6 | 7 | 8 | ## Connect / Express middleware that causes chaos 9 | 10 | This is loosely inspired by [chaos monkey](https://github.com/Netflix/SimianArmy/wiki/Chaos-Monkey). The basic idea is that you can drop it into your connect / express app and see how your clients get on when things go wrong. You might want to have it enabled by an environment variable so you can switch it on and off easily. 11 | 12 | ## Installation 13 | 14 | ``` bash 15 | $ npm install connect-chaos 16 | ``` 17 | 18 | ## Usage 19 | 20 | ``` javascript 21 | var chaos = require('connect-chaos'); 22 | 23 | // anything can happen 24 | app.use(chaos()); 25 | 26 | // requests might be delayed by 2000ms (default) 27 | app.use(chaos({ 28 | delay: true 29 | }); 30 | 31 | // requests might be delayed by specified value 32 | app.use(chaos({ 33 | delay: 300 34 | }); 35 | 36 | // requests might return an error code 37 | // Client error codes: 400, 401, 402, 403, 404, 405, 406, 407, 407, 409, 410, 411, 412, 413, 414, 415, 416, 417 38 | // Server Error codes: 500, 501, 502, 503, 504, 505 39 | app.use(chaos({ 40 | error: true 41 | }); 42 | 43 | // requests might return specified code 44 | app.use(chaos({ 45 | error: 401 46 | }); 47 | 48 | // requests might return a code in the array 49 | app.use(chaos({ 50 | error: [404, 500] 51 | })); 52 | 53 | // requests might return a code that matches the regex 54 | app.use(chaos(){ 55 | error: /^40/ 56 | }); 57 | 58 | // requests might return an erro code or be delayed by 6000ms 59 | app.use(chaos({ 60 | error: true, 61 | delay: 6000 62 | }); 63 | ``` 64 | -------------------------------------------------------------------------------- /example/index.js: -------------------------------------------------------------------------------- 1 | var express = require('express'), 2 | app = express(), 3 | chaos = require('../src/index'); 4 | 5 | app.use(chaos()); 6 | 7 | app.get('/', function(req, res, next) { 8 | res.send('CHAOS: hello from example, it looks like this request got through'); 9 | }); 10 | 11 | app.listen(3009, function() { 12 | console.log('booted chos example app on 3009'); 13 | }); 14 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "connect-chaos", 3 | "version": "0.2.0", 4 | "description": "connect / express middleware that causes chaos", 5 | "main": "src/index.js", 6 | "scripts": { 7 | "test": "mocha tests/ --recursive" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/Dakuan/connect-chaos.git" 12 | }, 13 | "keywords": [ 14 | "chaos", 15 | "express", 16 | "chaos monkey", 17 | "testing", 18 | "connect" 19 | ], 20 | "author": { 21 | "name": "Dominic Barker", 22 | "email": "dom.barker808@gmail.com", 23 | "url": "http://www.dombarker.co.uk" 24 | }, 25 | "license": "MIT", 26 | "bugs": { 27 | "url": "https://github.com/Dakuan/connect-chaos/issues" 28 | }, 29 | "homepage": "https://github.com/Dakuan/connect-chaos", 30 | "dependencies": { 31 | "ramda": "0.8.0" 32 | }, 33 | "devDependencies": { 34 | "mocha": "^2.1.0" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/handlers/delay.js: -------------------------------------------------------------------------------- 1 | var R = require('ramda'), 2 | runOnProp = require('../util/run-on-prop'); 3 | 4 | // delays the request a bit 5 | function _delay(time) { 6 | var time = time || 2000; 7 | function _delayHandler(req, res, next) { 8 | console.log('CHAOS: delaying by ' + time); 9 | setTimeout(function() { 10 | next(); 11 | }, time); 12 | } 13 | 14 | return function(req, res, next) { 15 | _delayHandler.apply(null, arguments); 16 | return _delayHandler; 17 | } 18 | } 19 | 20 | module.exports = { 21 | predicate: R.has('delay'), 22 | factory: runOnProp(_delay, 'delay') 23 | }; -------------------------------------------------------------------------------- /src/handlers/error.js: -------------------------------------------------------------------------------- 1 | var R = require('ramda'), 2 | randomElement = require('../util/random-element'), 3 | runOnProp = require('../util/run-on-prop'); 4 | 5 | var clientErrors = R.range(400, 418); 6 | var serverErrors = R.range(500, 506); 7 | var all = R.concat(clientErrors, serverErrors); 8 | 9 | var codeMatchingRegex = function(regex) { 10 | return R.find(function(c) { 11 | return R.func('test', regex, c); 12 | }, all); 13 | }; 14 | 15 | var parseOpt = R.cond( 16 | [R.is(Number), R.I], // if number then that pick that error code 17 | [R.is(Array), randomElement], // if array pick from those 18 | [R.is(RegExp), codeMatchingRegex], // if regex then code matching that regex 19 | [R.alwaysTrue, function() { 20 | return randomElement.call(null, all); 21 | }] // random error code 22 | ); 23 | 24 | // sends an error code 25 | function _error(code) { 26 | 27 | function _errorHandler(req, res, next) { 28 | var toThrow = parseOpt(code); 29 | console.log('CHAOS: throwing ' + toThrow); 30 | res.status(toThrow); 31 | res.end(); 32 | } 33 | 34 | function wrap(req, res, next) { 35 | _errorHandler.apply(null, arguments); 36 | return _errorHandler; 37 | } 38 | return wrap; 39 | } 40 | 41 | 42 | module.exports = { 43 | predicate: R.has('error'), 44 | factory: runOnProp(_error, 'error') 45 | }; 46 | -------------------------------------------------------------------------------- /src/handlers/handlers.js: -------------------------------------------------------------------------------- 1 | module.exports = [ 2 | require('./through'), 3 | require('./error'), 4 | require('./delay') 5 | ]; -------------------------------------------------------------------------------- /src/handlers/through.js: -------------------------------------------------------------------------------- 1 | var R = require('ramda'); 2 | 3 | 4 | // does nothing 5 | function _throughHandler(req, res, next) { 6 | console.log('CHAOS: letting this one go...'); 7 | next(); 8 | } 9 | 10 | function wrap(req, res, next) { 11 | _throughHandler.apply(null, arguments); 12 | return _throughHandler; 13 | } 14 | 15 | module.exports = { 16 | predicate: R.always(true), 17 | factory: R.always(wrap) 18 | }; 19 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | var R = require('ramda'), 2 | randomElemet = require('./util/random-element'), 3 | handlers = require('./handlers/handlers'); 4 | 5 | // checks if value is truthy 6 | function _truthy(val) { 7 | return !!val; 8 | } 9 | 10 | // calls a method on a object with the args 11 | // flipped to accept the object last 12 | var _flipFunc = R.curry(function(method, args, obj) { 13 | return obj[method].apply(null, args); 14 | }); 15 | 16 | // find the handlers that match the predicates 17 | var _handlersForOptions = R.curry(function(handlers, opts) { 18 | return R.compose( 19 | R.map(_flipFunc('factory', R.of(opts))), // build them 20 | R.filter(_flipFunc('predicate', R.of(opts))) // get the matching handlers 21 | )(handlers); 22 | }); 23 | 24 | // full list of built handlers 25 | var allHandlers = R.map(R.func('factory'), handlers); 26 | 27 | // pick the handlers that match the args 28 | var pickFromArgs = R.compose( 29 | randomElemet, 30 | R.ifElse(_truthy, _handlersForOptions(handlers), R.always(allHandlers)) 31 | ); 32 | 33 | var chaos = function(opts) { 34 | console.log('CHAOS: Running in CHAOS MODE. Requests will be delayed, error or worse...'); 35 | return function(req, res, next) { 36 | return pickFromArgs(opts).apply(null, arguments); 37 | } 38 | }; 39 | 40 | module.exports = chaos; 41 | -------------------------------------------------------------------------------- /src/util/random-element.js: -------------------------------------------------------------------------------- 1 | // picks random element from an array 2 | function _randomElement(array) { 3 | return array[Math.floor(Math.random() * array.length)]; 4 | } 5 | 6 | module.exports = _randomElement; -------------------------------------------------------------------------------- /src/util/run-on-prop.js: -------------------------------------------------------------------------------- 1 | var R = require('ramda'), 2 | Maybe = require('ramda/ext/types/Maybe'); 3 | 4 | // returns a function that expects an object with a property containing a config variable 5 | // if the variable is not a number it is ignored 6 | function _runOnProp(fn, prop) { 7 | return R.compose( 8 | fn, // apply the function 9 | R.prop('value'), // unwrap the monad 10 | R.map(R.prop(prop)), // get the prop if the argument is truthy 11 | Maybe // Maybe null 12 | ); 13 | } 14 | 15 | module.exports = _runOnProp; -------------------------------------------------------------------------------- /tests/handlers/errors-spec.js: -------------------------------------------------------------------------------- 1 | var subject = require('../../src/handlers/error'), 2 | R = require('ramda'), 3 | assert = require('assert'); 4 | 5 | function assertStatus(handler, expectedCode) { 6 | handler(null, { 7 | status: function(code) { 8 | assert(code === expectedCode); 9 | }, 10 | end: function() {} 11 | }); 12 | } 13 | 14 | describe("Error handler", function() { 15 | describe("when a valid number is provided ", function() { 16 | it("should use that error code", function() { 17 | var handler = subject.factory({ 18 | error: 420 19 | }); 20 | assertStatus(handler, 420); 21 | }); 22 | }); 23 | 24 | describe("when an array is provided", function() { 25 | it("should use a value from the array", function() { 26 | var handler = subject.factory({ 27 | error: [123] 28 | }); 29 | assertStatus(handler, 123); 30 | }); 31 | }); 32 | 33 | describe("when a regex is provided", function() { 34 | it("should use a value that matches the regex", function() { 35 | var handler = subject.factory({ 36 | error: /^400/ 37 | }); 38 | assertStatus(handler, 400); 39 | }); 40 | }); 41 | 42 | describe("when nothing is provided", function() { 43 | it("should use a valid error code", function() { 44 | var handler = subject.factory(); 45 | handler(null, { 46 | status: function(code) { 47 | assert(code >= 400); 48 | assert(code <= 506); 49 | assert(R.is(Number, code)); 50 | }, 51 | end: function() {} 52 | }); 53 | }); 54 | }); 55 | }); 56 | -------------------------------------------------------------------------------- /tests/index-spec.js: -------------------------------------------------------------------------------- 1 | var chaos = require('../src/index'), 2 | R = require('ramda'), 3 | Maybe = require('ramda/ext/types/Maybe'), 4 | assert = require('assert'); 5 | 6 | // run chaos with n attempts 7 | // then reduce to the unique values 8 | // this should hopefully give us all the handlers 9 | // that are matching the options. 10 | // its random though, so it might not. 11 | // if we have lots of false negative failures on CCI 12 | // we can bump up the number of attempts 13 | var random = R.curry(function(attempts, chaosOptions) { 14 | 15 | var invokeChaos = function(opts) { 16 | return function() { 17 | return chaos(opts)({}, { 18 | status: function() {}, 19 | end: function() {} 20 | }, function() {}); 21 | } 22 | }; 23 | 24 | return R.uniq( 25 | R.map( 26 | R.compose( 27 | R.prop('value'), 28 | R.map(R.prop('name')), 29 | Maybe, 30 | invokeChaos(chaosOptions) 31 | ), 32 | 33 | R.range(0, attempts)) 34 | ); 35 | }); 36 | 37 | var attempt100 = random(100); 38 | 39 | describe("connect-chaos", function() { 40 | describe("when no options are provided", function() { 41 | it("should return a middleware function", function() { 42 | assert(chaos().length === 3); 43 | }); 44 | }); 45 | 46 | describe("when an error config option is provided", function() { 47 | it("might return an error handler", function() { 48 | var handlerNames = attempt100({ 49 | error: true, 50 | }); 51 | assert(R.contains('_errorHandler')(handlerNames)); 52 | }); 53 | it("should return a middleware function", function() { 54 | assert(chaos({ 55 | error: true 56 | }).length === 3); 57 | }); 58 | }); 59 | 60 | describe("when the delay config option is provided", function() { 61 | it("might return a delay handler", function() { 62 | var handlerNames = attempt100({ 63 | delay: true 64 | }); 65 | assert(R.contains('_delayHandler')(handlerNames)); 66 | }); 67 | it("should return a middleware function", function() { 68 | assert(chaos({ 69 | delay: true 70 | }).length === 3); 71 | }); 72 | }); 73 | }); 74 | --------------------------------------------------------------------------------