├── .editorconfig ├── .gitignore ├── .jscsrc ├── .jshintrc ├── .travis.yml ├── Gruntfile.js ├── LICENSE ├── README.md ├── app ├── package │ ├── js │ │ ├── angular-multimocks.js │ │ └── angular-multimocks.min.js │ └── tasks │ │ ├── gulp │ │ └── multimocksGulp.js │ │ ├── multimocks.tpl │ │ ├── multimocksGenerator.js │ │ ├── multimocksGrunt.js │ │ ├── multimocksMultipleFiles.tpl │ │ ├── plugins.js │ │ └── plugins │ │ └── hal.js └── src │ ├── demo │ ├── Gruntfile.js │ ├── index.html │ ├── mockData │ │ ├── cart │ │ │ ├── empty.json │ │ │ ├── outOfStock.json │ │ │ ├── slowResponse.json │ │ │ └── someItems.json │ │ └── mockResources.json │ └── mockOutput.js │ └── js │ ├── multimocks.js │ ├── multimocks.responseDelay.js │ ├── multimocks.responseDelay.spec.js │ └── multimocks.spec.js ├── bower.json ├── karma-unit.conf.js ├── package.json └── tasks ├── gulp └── multimocksGulp.js ├── multimocks.tpl ├── multimocksGenerator.js ├── multimocksGrunt.js ├── multimocksMultipleFiles.tpl ├── plugins.js └── plugins └── hal.js /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: http://EditorConfig.org 2 | 3 | # top-most EditorConfig file 4 | root = true 5 | 6 | # Unix-style newlines with a newline ending every file 7 | [*] 8 | charset = utf-8 9 | end_of_line = lf 10 | 11 | indent_style = space 12 | indent_size = 2 13 | 14 | trim_trailing_whitespace = true 15 | insert_final_newline = true -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /app/build/ 2 | /nbproject/ 3 | /node_modules/ 4 | /coverage/ 5 | .DS_Store 6 | .idea/ 7 | *~ 8 | npm-debug.log 9 | -------------------------------------------------------------------------------- /.jscsrc: -------------------------------------------------------------------------------- 1 | { 2 | "preset": "node-style-guide", 3 | "requireCamelCaseOrUpperCaseIdentifiers": "ignoreProperties", 4 | "disallowTrailingComma": true, 5 | "disallowSpacesInFunction": null, 6 | "disallowMultipleLineBreaks": true, 7 | "requireCapitalizedComments": null, 8 | "requireCapitalizedConstructors": true, 9 | "requireParenthesesAroundIIFE": true, 10 | "requireTrailingComma": null, 11 | "requireSpaceAfterLineComment": null, 12 | "disallowMultipleVarDecl": null, 13 | "maximumLineLength": 80, 14 | "requireSpaceAfterKeywords": [ 15 | "do", 16 | "for", 17 | "if", 18 | "else", 19 | "switch", 20 | "case", 21 | "try", 22 | "catch", 23 | "void", 24 | "while", 25 | "with", 26 | "return", 27 | "typeof", 28 | "function" 29 | ], 30 | "requireLineBreakAfterVariableAssignment" : true, 31 | "validateIndentation": 2 32 | } 33 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "curly": true, 3 | "eqeqeq": true, 4 | "latedef": true, 5 | "noarg": true, 6 | "undef": true, 7 | "unused": true 8 | } 9 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - '0.10' 4 | before_install: 5 | - npm install --global bower grunt-cli 6 | deploy: 7 | provider: npm 8 | email: ukfrontend@wonga.com 9 | api_key: 10 | secure: l2RcuJCd7jgqznrXqQ+bMM+PPVtBP85SlCLZyRYcJ+YfvE1iRjB6Vb1Ll3k4a8NFDLOt1cMO7sEUiLOoP3tFzNr3iBf96tsC826UKqDlu4WPKdhHzjrYJceEAStFaZCMzrrQMGpGqHtkuDGho7FD+Aio50i7XL0GspwzuXxE8pQ= 11 | on: 12 | tags: true 13 | -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | /* global module, require */ 2 | 3 | module.exports = function (grunt) { 4 | grunt.loadNpmTasks('grunt-contrib-clean'); 5 | grunt.loadNpmTasks('grunt-contrib-concat'); 6 | grunt.loadNpmTasks('grunt-contrib-copy'); 7 | grunt.loadNpmTasks('grunt-contrib-jshint'); 8 | grunt.loadNpmTasks('grunt-contrib-uglify'); 9 | grunt.loadNpmTasks('grunt-contrib-watch'); 10 | grunt.loadNpmTasks('grunt-contrib-connect'); 11 | grunt.loadNpmTasks('grunt-jscs'); 12 | grunt.loadNpmTasks('grunt-open'); 13 | grunt.loadNpmTasks('grunt-karma'); 14 | 15 | var os = require('os'); 16 | 17 | grunt.registerTask('build', [ 18 | 'jshint', 19 | 'jscs', 20 | 'clean:build', 21 | 'copy:build' 22 | ]); 23 | grunt.registerTask('test', [ 24 | 'karma:headless_unit' 25 | ]); 26 | grunt.registerTask('test:browser', [ 27 | 'karma:browser_unit' 28 | ]); 29 | grunt.registerTask('test:debug', [ 30 | 'karma:browser_unit_debug' 31 | ]); 32 | grunt.registerTask('package', [ 33 | 'clean:package', 34 | 'concat:package', 35 | 'uglify:package', 36 | 'copy:tasks' 37 | ]); 38 | grunt.registerTask('workflow:dev', [ 39 | 'connect:dev', 40 | 'build', 41 | 'open:dev', 42 | 'watch:dev' 43 | ]); 44 | 45 | grunt.initConfig({ 46 | app: { 47 | name: 'angular-multimocks', 48 | source_dir: 'app/src', 49 | build_dir: 'app/build', 50 | package_dir: 'app/package', 51 | connect_port: grunt.option('connect_port') || 2302, 52 | hostname: os.hostname() 53 | }, 54 | 55 | clean: { 56 | build: '<%= app.build_dir %>', 57 | package: '<%= app.package_dir %>' 58 | }, 59 | 60 | jshint: { 61 | source: [ 62 | '*.js', 63 | '<%= app.source_dir %>/**/*.js', 64 | '!<%= app.source_dir %>/node_modules/**/*.js', 65 | 'tasks/*.js', 66 | 'tasks/**/*.js' 67 | ], 68 | options: { 69 | jshintrc: '.jshintrc', 70 | reporterOutput: '' 71 | } 72 | }, 73 | 74 | jscs: { 75 | source: [ 76 | '*.js', 77 | '<%= app.source_dir %>/**/*.js', 78 | '!<%= app.source_dir %>/node_modules/**/*.js', 79 | 'tasks/*.js', 80 | 'tasks/**/*.js' 81 | ], 82 | options: { 83 | config: '.jscsrc' 84 | } 85 | }, 86 | 87 | copy: { 88 | build: { 89 | files: [ 90 | { 91 | expand: true, 92 | cwd: '<%= app.source_dir %>', 93 | src: ['**', '!css/**'], 94 | dest: '<%= app.build_dir %>' 95 | }, 96 | { 97 | expand: true, 98 | cwd: 'node_modules', 99 | src: [ 100 | 'angular/angular.js', 101 | 'angular-mocks/angular-mocks.js' 102 | ], 103 | dest: '<%= app.build_dir %>/node_modules' 104 | }, 105 | { 106 | expand: true, 107 | src: ['package.json'], 108 | dest: '<%= app.build_dir %>' 109 | } 110 | ] 111 | }, 112 | tasks: { 113 | files: [ 114 | { 115 | expand: true, 116 | cwd: 'tasks', 117 | src: ['**'], 118 | dest: '<%= app.package_dir %>/tasks' 119 | } 120 | ] 121 | } 122 | }, 123 | 124 | karma: { 125 | headless_unit: { 126 | options: { 127 | configFile: 'karma-unit.conf.js', 128 | browsers: ['PhantomJS'] 129 | } 130 | }, 131 | browser_unit: { 132 | options: { 133 | configFile: 'karma-unit.conf.js' 134 | } 135 | }, 136 | browser_unit_debug: { 137 | options: { 138 | configFile: 'karma-unit.conf.js', 139 | singleRun: false, 140 | browsers: ['Chrome'] 141 | } 142 | } 143 | }, 144 | 145 | concat: { 146 | package: { 147 | src: [ 148 | '<%= app.build_dir %>/js/**/*.js', 149 | '!<%= app.build_dir %>/js/**/*.spec.js' 150 | ], 151 | dest: '<%= app.package_dir %>/js/<%= app.name %>.js' 152 | } 153 | }, 154 | 155 | uglify: { 156 | package: { 157 | files: { 158 | '<%= app.package_dir %>/js/<%= app.name %>.min.js': [ 159 | '<%= app.package_dir %>/js/<%= app.name %>.js' 160 | ] 161 | } 162 | } 163 | }, 164 | 165 | connect: { 166 | options: { 167 | hostname: '*' 168 | }, 169 | dev: { 170 | options: { 171 | port: '<%= app.connect_port %>', 172 | base: '<%= app.build_dir %>' 173 | } 174 | } 175 | }, 176 | 177 | open: { 178 | dev: { 179 | url: 'http://<%= app.hostname %>:<%= app.connect_port %>/demo' 180 | } 181 | }, 182 | 183 | watch: { 184 | dev: { 185 | files: ['<%= app.source_dir %>/**/*'], 186 | tasks: ['build', 'test:unit'], 187 | options: { 188 | livereload: true 189 | } 190 | } 191 | } 192 | }); 193 | }; 194 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (C) 2014 Wonga 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NON INFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Angular Multimocks 2 | 3 | 4 | ## Travis Status 5 | [![Build Status](https://travis-ci.org/wongatech/angular-multimocks.svg?branch=master)](https://travis-ci.org/wongatech/angular-multimocks) 6 | 7 | ## Demo 8 | 9 | 10 | Angular Multimocks lets you test how your app behaves with different responses 11 | from an API. 12 | 13 | Angular Multimocks allows you to define sets of mock API responses for different 14 | scenarios as JSON files. A developer of an e-commerce app could set up scenarios 15 | for a new customer, one who is registered and one who has an order outstanding. 16 | 17 | Angular Multimocks allows you to switch between scenarios using a query string 18 | parameter: 19 | ``` 20 | ?scenario=foo 21 | ``` 22 | 23 | You can use Angular Multimocks to quickly test your app works in all situations 24 | while developing or to provide mock data for a suite of automated acceptance 25 | tests. 26 | 27 | ## Example Use Case 28 | 29 | You have an application which calls to `http://example.com/cart` to get a list 30 | of items in the customer's shopping cart. You'd like to be able to easily 31 | switch between different API responses so that you can test the various use 32 | cases. You may want responses for the following: 33 | 34 | | Scenario | URL | 35 | | ------------------------------------- | ------------------------------- | 36 | | Shopping cart is empty | `/cart?scenario=emptyCart` | 37 | | Shopping cart with a quick buy option | `/cart?scenario=quickBuyCart` | 38 | | Shopping cart with out of stock items | `/cart?scenario=outOfStockCart` | 39 | 40 | ## Demo App 41 | 42 | See `app/src/demo/` for a demo app. Inside the demo app, run `grunt` to generate the 43 | mocks, then open `index.html` in your browser. 44 | 45 | ## Usage 46 | 47 | ### NPM 48 | 49 | ```sh 50 | npm install --save angular-multimocks 51 | ``` 52 | 53 | Include `angular-multimocks.js` or `angular-multimocks.min.js` in your 54 | application: 55 | 56 | ```html 57 | 58 | ``` 59 | 60 | ### Dependencies 61 | 62 | Angular Multimocks depends on Angular Mocks so include it in your application. For example: 63 | 64 | ```html 65 | 66 | ``` 67 | 68 | Add the `scenario` module to your application: 69 | 70 | ```javascript 71 | angular 72 | .module('demo', ['scenario']) 73 | // more code here... 74 | ``` 75 | 76 | ## Mock Format 77 | 78 | Resource files look like this: 79 | 80 | ```json 81 | { 82 | "httpMethod": "GET", 83 | "statusCode": 200, 84 | "uri": "/customer/cart", 85 | "response": { 86 | "id": "foo" 87 | } 88 | } 89 | ``` 90 | 91 | The `uri` property defines the URI that is being mocked in your application 92 | and can contain a regex: 93 | 94 | ``` 95 | "uri": "/customer/\\d*/cart" 96 | ``` 97 | 98 | ### Delayed responses 99 | 100 | In some scenarios you may want to simulate a server/network delay. 101 | This is done by intercepting the HTTP response and delaying it. 102 | Mocks accept an optional `responseDelay` property that will delay 103 | the HTTP response for the specified time in milliseconds: 104 | 105 | ``` 106 | "responseDelay": 500 107 | ``` 108 | 109 | The manifest file `mockResources.json` defines the available scenarios and 110 | describes which version of each resource should be used for each scenario. 111 | 112 | ```json 113 | { 114 | "_default": [ 115 | "root/_default.json", 116 | "account/anonymous.json", 117 | "orders/_default.json" 118 | ], 119 | "loggedIn": [ 120 | "account/loggedIn.json" 121 | ] 122 | } 123 | ``` 124 | 125 | All scenarios inherit resources defined in `_default` unless they provide an 126 | override. Think of `_default` as the base class for scenarios. 127 | 128 | The example above defines 2 scenarios `_default` and `loggedIn`. `loggedIn` has 129 | the default versions of the `root` and `orders` resources, but overrides 130 | `account`, using the version in `account/loggedIn.json`. 131 | 132 | #### Global delay override 133 | 134 | You can override all delays in a request by adding an optional parameter to 135 | the query string. 136 | 137 | ``` 138 | global_delay=0 139 | ``` 140 | 141 | ## Generating Mocks 142 | 143 | Angular Multimocks provides `Grunt` and `Gulp` tasks that will compile resources 144 | into an AngularJS module definition. 145 | Adding these tasks to your build process will help to generate mocks after making 146 | changes. 147 | 148 | Install the module using npm: 149 | 150 | ```sh 151 | npm install --save-dev angular-multimocks 152 | ``` 153 | 154 | ### Grunt task 155 | 156 | Add it to your Grunt configuration: 157 | 158 | ```javascript 159 | // load the task 160 | grunt.loadNpmTasks('angular-multimocks'); 161 | 162 | // configuration for scenarios 163 | grunt.initConfig({ 164 | multimocks: { 165 | myApp: { 166 | src: 'mocks', 167 | dest: 'build/multimocks.js', 168 | template: 'myTemplate.tpl' // optional 169 | } 170 | }, 171 | // other config here... 172 | }); 173 | ``` 174 | 175 | ### Gulp task 176 | 177 | ```javascript 178 | // Load the gulp task 179 | var multimocksGulp = require('angular-multimocks/app/package/tasks/gulp/multimocksGulp'); 180 | 181 | // Define multimocks 182 | gulp.task('multimocks', function () { 183 | // Call the multimocks gulp task with apropriate configuration 184 | multimocksGulp({ 185 | src: 'mocks', 186 | dest: 'mocks/multimocks.js' 187 | }); 188 | }); 189 | ``` 190 | 191 | Once either the Gulp or Grunt task is run, `build/multimocks.js` will be generated 192 | containing all your mock data. Include that in your app: 193 | 194 | ```html 195 | 196 | ``` 197 | 198 | ### Output Scenarios In Multiple Files 199 | 200 | If the generated `build/multimocks.js` is too large, you may experience memory 201 | issues when running your application. 202 | 203 | You can choose to build multiple files, one for each scenario by specifying 204 | `multipleFiles: true` and `dest` as a directory. 205 | 206 | Your Grunt configuration should look something like: 207 | 208 | ```javascript 209 | // load the task 210 | grunt.loadNpmTasks('angular-multimocks'); 211 | 212 | // configuration for scenarios 213 | multimocks: { 214 | myApp: { 215 | src: 'mocks', 216 | dest: 'build/multimocks', 217 | multipleFiles: true, 218 | template: 'myTemplate.tpl' // optional 219 | } 220 | }, 221 | ``` 222 | 223 | When the task is run a file will be generated for each scenario. Include all 224 | the generated files in your app: 225 | 226 | ```html 227 | 228 | 229 | 230 | ``` 231 | ### Task options 232 | 233 | * `src` - The directory to load mock files from (required) 234 | * `dest` - The destination file/directory to output compiled mocks (required) 235 | * `multipleFiles` - Generates one file per resource type (default: false) 236 | * `template` - The template to use when generating mocks 237 | * `verbose` - The logging level to use when running the generate task 238 | 239 | ## HAL Plugin 240 | 241 | If your API conforms to [HAL](http://stateless.co/hal_specification.html), 242 | Angular Multimocks can generate links for you to speed development. 243 | 244 | Enable the plugin in your `Gruntfile.js`: 245 | 246 | ```javascript 247 | multimocks: { 248 | myApp: { 249 | src: 'mocks', 250 | dest: 'build/multimocks', 251 | plugins: ['hal'] 252 | } 253 | } 254 | ``` 255 | 256 | Organise your mock response files into a file structure with a directory for 257 | each resource, e.g.: 258 | 259 | ``` 260 | . 261 | ├── account 262 | │   ├── loggedIn.json 263 | │   └── anonymous.json 264 | ├── orders 265 | │   └── _default.json 266 | ├── root 267 | │   └── _default.json 268 | └── mockResources.json 269 | ``` 270 | 271 | Angular Multimocks will add a `_links` object to each response with all the 272 | known resources declared as available links: 273 | 274 | ```json 275 | { 276 | "httpMethod": "GET", 277 | "statusCode": 200, 278 | "response": { 279 | "id": "foo", 280 | "_links": { 281 | "root": { 282 | "rel": "root", 283 | "method": "GET", 284 | "href": "http://example.com/" 285 | }, 286 | "account": { 287 | "rel": "account", 288 | "method": "GET", 289 | "href": "http://example.com/account" 290 | }, 291 | "orders": { 292 | "rel": "orders", 293 | "method": "GET", 294 | "href": "http://example.com/orders" 295 | } 296 | } 297 | } 298 | } 299 | ``` 300 | 301 | A `uri` will be generated for each resource. This value is used for the `href` 302 | field of each object in `_links`. 303 | 304 | ## `multimocksDataProvider` 305 | 306 | Angular Multimocks also declares a provider, `multimocksDataProvider`, which 307 | allows you to set mock data by passing an object to the `setMockData` method. 308 | 309 | `multimocksDataProvider` also gives you the ability to overwrite the default 310 | headers returned by Angular Multimocks. Below we're setting the headers to 311 | specify that the content type is HAL JSON. 312 | 313 | ``` 314 | .config(['mutimocksDataProvider', function (multimocksDataProvider) { 315 | multimocksDataProvider.setHeaders({ 316 | 'Content-Type': 'application/hal+json' 317 | }); 318 | }]); 319 | ``` 320 | 321 | ## Contributing 322 | 323 | We :heart: pull requests! 324 | 325 | To contribute: 326 | 327 | - Fork the repo 328 | - Run `npm install` 329 | - Run `grunt workflow:dev` or `npm run dev` to watch for changes, lint, build and run tests as 330 | you're working 331 | - Write your unit tests for your change 332 | - Test with the demo app 333 | - Run `grunt package` or `npm run package` to update the distribution files 334 | -------------------------------------------------------------------------------- /app/package/js/angular-multimocks.js: -------------------------------------------------------------------------------- 1 | /* global angular */ 2 | 3 | angular 4 | .module('scenario', ['ngMockE2E', 'multimocks.responseDelay']) 5 | 6 | .provider('multimocksData', function () { 7 | var mockData = {}, 8 | mockHeaders = { 9 | 'Content-type': 'application/json' 10 | }, 11 | defaultScenario = '_default'; 12 | 13 | this.setHeaders = function (data) { 14 | mockHeaders = data; 15 | }; 16 | 17 | this.setMockData = function (data) { 18 | mockData = data; 19 | }; 20 | 21 | this.addMockData = function (name, data) { 22 | mockData[name] = data; 23 | }; 24 | 25 | this.setDefaultScenario = function (scenario) { 26 | defaultScenario = scenario; 27 | }; 28 | 29 | this.$get = function $get() { 30 | return { 31 | getMockData: function () { 32 | return mockData; 33 | }, 34 | getDefaultScenario: function () { 35 | return defaultScenario; 36 | }, 37 | getHeaders: function () { 38 | return mockHeaders; 39 | } 40 | }; 41 | }; 42 | }) 43 | 44 | .factory('multimocks', [ 45 | '$q', 46 | '$http', 47 | '$httpBackend', 48 | 'multimocksData', 49 | 'scenarioMocks', 50 | function ($q, $http, $httpBackend, multimocksData, scenarioMocks) { 51 | var setupHttpBackendForMockResource = function (deferred, mock) { 52 | var mockHeaders = multimocksData.getHeaders(), 53 | uriRegExp = new RegExp('^' + mock.uri + '$'); 54 | 55 | // Mock a polling resource. 56 | if (mock.poll) { 57 | var pollCounter = 0, 58 | pollCount = mock.pollCount !== undefined ? mock.pollCount : 2; 59 | 60 | // Respond with a 204 which will then get polled until a 200 is 61 | // returned. 62 | $httpBackend 63 | .when(mock.httpMethod, uriRegExp, mock.requestData) 64 | .respond(function () { 65 | // Call a certain amount of times to simulate polling. 66 | if (pollCounter < pollCount) { 67 | pollCounter++; 68 | return [204, {}, mockHeaders]; 69 | } 70 | return [200, mock.response, mockHeaders]; 71 | }); 72 | } else { 73 | $httpBackend 74 | .when(mock.httpMethod, uriRegExp, mock.requestData) 75 | .respond(mock.statusCode, mock.response, mockHeaders); 76 | } 77 | 78 | // Make this HTTP request now if required otherwise just resolve 79 | // TODO deprecated? 80 | if (mock.callInSetup) { 81 | var req = {method: mock.httpMethod, url: mock.uri}; 82 | $http(req).success(function () { 83 | deferred.resolve(); 84 | }); 85 | } else { 86 | deferred.resolve(); 87 | } 88 | }; 89 | 90 | return { 91 | setup: function (scenarioName) { 92 | var deferred = $q.defer(); 93 | 94 | // Set mock for each item. 95 | var mocks = scenarioMocks.getMocks(scenarioName); 96 | for (var i in mocks) { 97 | setupHttpBackendForMockResource(deferred, mocks[i]); 98 | } 99 | 100 | return deferred.promise; 101 | } 102 | }; 103 | } 104 | ]) 105 | 106 | .factory('currentScenario', [ 107 | '$window', 108 | 'multimocksData', 109 | function ($window, multimocksData) { 110 | 111 | function getScenarioFromPath (path) { 112 | if (path.indexOf('scenario') !== -1) { 113 | var scenarioParams = path 114 | .slice(1) 115 | .split('&') 116 | .map(function (s) { return s.split('='); }) 117 | .filter(function (kv) { return kv[0] === 'scenario'; }); 118 | return scenarioParams[0][1]; 119 | } 120 | return undefined; 121 | } 122 | 123 | return { 124 | getName: function () { 125 | var scenarioFromURL = getScenarioFromPath($window.location.search); 126 | if (scenarioFromURL === undefined) { 127 | return multimocksData.getDefaultScenario(); 128 | } 129 | return scenarioFromURL; 130 | } 131 | }; 132 | } 133 | ]) 134 | 135 | .factory('scenarioMocks', [ 136 | '$log', 137 | 'multimocksData', 138 | 'currentScenario', 139 | 'multimocksLocation', 140 | function ($log, multimocksData, currentScenario, multimocksLocation) { 141 | var mockData = multimocksData.getMockData(); 142 | 143 | function urlMatchesRegex(url, regex) { 144 | var pattern = new RegExp(regex); 145 | return pattern.test(url); 146 | } 147 | 148 | function mergeScenarios(chosenScenario, defaultScenario) { 149 | var scenarioData = [].concat(chosenScenario); 150 | 151 | if (defaultScenario) { 152 | defaultScenario.forEach(function (scenario) { 153 | var isAlreadySet = false; 154 | var defaultUrl = scenario.uri + scenario.httpMethod; 155 | for (var i = 0; i < chosenScenario.length; i++) { 156 | var response = chosenScenario[i]; 157 | var responseUrl = response.uri + response.httpMethod; 158 | isAlreadySet = responseUrl === defaultUrl; 159 | if (isAlreadySet) { 160 | break; 161 | } 162 | } 163 | if (!isAlreadySet) { 164 | scenarioData.push(scenario); 165 | } 166 | }); 167 | } 168 | return scenarioData; 169 | } 170 | 171 | var scenarioMocks = { 172 | getMocks: function (scenarioToLoad) { 173 | var defaultScenario = mockData[multimocksData.getDefaultScenario()]; 174 | 175 | if (scenarioToLoad === multimocksData.getDefaultScenario()) { 176 | return defaultScenario; 177 | } 178 | 179 | if (mockData[scenarioToLoad] !== undefined) { 180 | var chosenScenario = mockData[scenarioToLoad]; 181 | return mergeScenarios(chosenScenario, defaultScenario); 182 | } 183 | 184 | if (scenarioToLoad) { 185 | $log.error('Mocks not found for scenario: ' + scenarioToLoad); 186 | } 187 | }, 188 | getMocksForCurrentScenario: function () { 189 | return scenarioMocks.getMocks(currentScenario.getName()); 190 | }, 191 | getDelayForResponse: function (response) { 192 | var globalDelay = multimocksLocation 193 | .getQueryStringValuesByKey('global_delay'); 194 | if (globalDelay !== undefined) { 195 | return parseInt(globalDelay[0]); 196 | } 197 | var availableMocks = scenarioMocks.getMocksForCurrentScenario(); 198 | 199 | for (var i in availableMocks) { 200 | var mock = availableMocks[i]; 201 | var sameURL = urlMatchesRegex(response.config.url, mock.uri); 202 | var sameMethod = (mock.httpMethod === response.config.method); 203 | if (sameMethod && sameURL) { 204 | return mock.responseDelay || 0; 205 | } 206 | } 207 | return 0; 208 | } 209 | }; 210 | return scenarioMocks; 211 | } 212 | ]) 213 | 214 | /** 215 | * Service to interact with the browser location 216 | */ 217 | .service('multimocksLocation', [ 218 | '$window', 219 | function ($window) { 220 | var multimocksLocation = {}; 221 | 222 | /** 223 | * Returns an array of values for a specified query string parameter. 224 | * 225 | * Handles multivalued keys and encoded characters. 226 | * 227 | * Usage: 228 | * 229 | * If the URL is /?foo=bar 230 | * 231 | * multimocksLocation.getQueryStringValuesByKey('foo') 232 | * 233 | * Will return 234 | * 235 | * ['bar'] 236 | * 237 | * @return Array 238 | * An array of values for the specified key. 239 | */ 240 | multimocksLocation.getQueryStringValuesByKey = function (key) { 241 | var queryDictionary = {}; 242 | $window.location.search 243 | .substr(1) 244 | .split('&') 245 | .forEach(function (item) { 246 | var s = item.split('='), 247 | k = s[0], 248 | v = s[1] && decodeURIComponent(s[1]); 249 | 250 | if (queryDictionary[k]) { 251 | queryDictionary[k].push(v); 252 | } else { 253 | queryDictionary[k] = [v]; 254 | } 255 | }); 256 | return queryDictionary[key]; 257 | }; 258 | 259 | return multimocksLocation; 260 | }]) 261 | 262 | .run([ 263 | 'multimocks', 264 | 'currentScenario', 265 | function (multimocks, currentScenario) { 266 | // load a scenario based on URL string, 267 | // e.g. http://example.com/?scenario=scenario1 268 | multimocks.setup(currentScenario.getName()); 269 | } 270 | ]); 271 | 272 | /* global angular */ 273 | 274 | angular 275 | .module('multimocks.responseDelay', []) 276 | 277 | .factory('responseDelay', [ 278 | '$q', 279 | '$timeout', 280 | 'scenarioMocks', 281 | function ($q, $timeout, scenarioMocks) { 282 | return { 283 | response: function (response) { 284 | var delayedResponse = $q.defer(); 285 | 286 | $timeout(function () { 287 | delayedResponse.resolve(response); 288 | }, scenarioMocks.getDelayForResponse(response)); 289 | 290 | return delayedResponse.promise; 291 | } 292 | }; 293 | } 294 | ]) 295 | 296 | .config([ 297 | '$httpProvider', 298 | function ($httpProvider) { 299 | $httpProvider.interceptors.push('responseDelay'); 300 | } 301 | ]); 302 | -------------------------------------------------------------------------------- /app/package/js/angular-multimocks.min.js: -------------------------------------------------------------------------------- 1 | angular.module("scenario",["ngMockE2E","multimocks.responseDelay"]).provider("multimocksData",function(){var a={},b={"Content-type":"application/json"},c="_default";this.setHeaders=function(a){b=a},this.setMockData=function(b){a=b},this.addMockData=function(b,c){a[b]=c},this.setDefaultScenario=function(a){c=a},this.$get=function(){return{getMockData:function(){return a},getDefaultScenario:function(){return c},getHeaders:function(){return b}}}}).factory("multimocks",["$q","$http","$httpBackend","multimocksData","scenarioMocks",function(a,b,c,d,e){var f=function(a,e){var f=d.getHeaders(),g=new RegExp("^"+e.uri+"$");if(e.poll){var h=0,i=void 0!==e.pollCount?e.pollCount:2;c.when(e.httpMethod,g,e.requestData).respond(function(){return i>h?(h++,[204,{},f]):[200,e.response,f]})}else c.when(e.httpMethod,g,e.requestData).respond(e.statusCode,e.response,f);if(e.callInSetup){var j={method:e.httpMethod,url:e.uri};b(j).success(function(){a.resolve()})}else a.resolve()};return{setup:function(b){var c=a.defer(),d=e.getMocks(b);for(var g in d)f(c,d[g]);return c.promise}}}]).factory("currentScenario",["$window","multimocksData",function(a,b){function c(a){if(-1!==a.indexOf("scenario")){var b=a.slice(1).split("&").map(function(a){return a.split("=")}).filter(function(a){return"scenario"===a[0]});return b[0][1]}return void 0}return{getName:function(){var d=c(a.location.search);return void 0===d?b.getDefaultScenario():d}}}]).factory("scenarioMocks",["$log","multimocksData","currentScenario","multimocksLocation",function(a,b,c,d){function e(a,b){var c=new RegExp(b);return c.test(a)}function f(a,b){var c=[].concat(a);return b&&b.forEach(function(b){for(var d=!1,e=b.uri+b.httpMethod,f=0;f); 11 | /* jshint ignore:end */ 12 | // jscs:enable 13 | }]); 14 | -------------------------------------------------------------------------------- /app/package/tasks/multimocksGenerator.js: -------------------------------------------------------------------------------- 1 | /* global require, module, process */ 2 | 3 | var _ = require('lodash'), 4 | path = require('path'), 5 | fs = require('fs'), 6 | pluginRegistry = require('./plugins'), 7 | mkdirp = require('mkdirp'), 8 | getDirName = path.dirname; 9 | 10 | var pwd = path.dirname(module.filename), 11 | singleFileDefaultTemplate = path.join(pwd, 'multimocks.tpl'), 12 | multipleFilesDefaultTemplate = path.join(pwd, 13 | 'multimocksMultipleFiles.tpl'), 14 | mockManifestFilename = 'mockResources.json'; 15 | 16 | module.exports = function (logger, config) { 17 | 18 | /** 19 | * Read a scenario from a list of resource files, add URIs and merge in 20 | * resources from default scenario. 21 | */ 22 | var readScenario = function (config, mockSrc, defaultScenario, filenames, 23 | scenarioName) { 24 | // read mock data files for this scenario 25 | var scenario = filenames.map(function (filename) { 26 | var filepath = fs.realpathSync(path.join(mockSrc, filename)); 27 | 28 | return { 29 | scenarioName: scenarioName, 30 | filename: filename, 31 | scenario: require(filepath) 32 | }; 33 | }); 34 | 35 | return scenario; 36 | }; 37 | 38 | /** 39 | * Read scenario definitions and return a structure that 40 | * multimockDataProvider.setMockData will understand. 41 | */ 42 | var readMockManifest = function (config, mockSrc) { 43 | var mockManifestPath = path.join(process.cwd(), mockSrc, 44 | mockManifestFilename), 45 | 46 | // read manifest JSON by require'ing it 47 | mockManifest = require(mockManifestPath), 48 | 49 | // read files for default scenario first, so we can merge it into other 50 | // scenarios later 51 | defaultScenario = readScenario(config, mockSrc, [], 52 | mockManifest._default, '_default'); 53 | 54 | // read files for each scenario 55 | return _.mapValues(mockManifest, function (filenames, scenarioName) { 56 | return readScenario(config, mockSrc, defaultScenario, filenames, 57 | scenarioName); 58 | }); 59 | }; 60 | 61 | /** 62 | * Executes each of the plugins configured in the application to 63 | * decorate responses. 64 | * 65 | * @param {object} data 66 | * @param {array} plugins 67 | * @return {object} decoratedData 68 | */ 69 | var runPlugins = function (data, pluginNames) { 70 | logger('runPlugins input', data); 71 | var plugins = pluginNames.map(function (pn) { return pluginRegistry[pn]; }), 72 | applyPlugin = function (oldData, plugin) { return plugin(oldData); }; 73 | // Use reduce to apply all the plugins to the data 74 | var output = plugins.reduce(applyPlugin, data); 75 | logger('runPlugins output', output); 76 | return output; 77 | }; 78 | 79 | /** 80 | * Strip context metadata from scenarios. 81 | */ 82 | var removeContext = function (dataWithContext) { 83 | return _.mapValues(dataWithContext, function (scenario) { 84 | return scenario.map(function (response) { 85 | return response.scenario; 86 | }); 87 | }); 88 | }; 89 | 90 | /** 91 | * Return a javascript object of all scenario data. 92 | * 93 | * @param {string} config 94 | * @param {string} mockSrc 95 | * 96 | * @returns {object} 97 | */ 98 | var readScenarioData = function (config, mockSrc) { 99 | var dataWithContext = readMockManifest(config, mockSrc); 100 | 101 | // log('readScenarioData config', config); 102 | if (config.plugins) { 103 | dataWithContext = runPlugins(dataWithContext, config.plugins); 104 | } 105 | 106 | return removeContext(dataWithContext); 107 | }; 108 | 109 | /** 110 | * Save the file 111 | * 112 | * @param {string} template 113 | * @param {string} path 114 | * @param {string} data 115 | * @param {string} name 116 | */ 117 | var writeScenarioModule = function (templatePath, path, data, name) { 118 | var templateString = fs.readFileSync(templatePath); 119 | 120 | // generate scenarioData.js contents by inserting data into template 121 | var templateData = {scenarioData: data}; 122 | templateData.scenarioDataName = name || ''; 123 | 124 | var output = _.template(templateString)(templateData); 125 | 126 | mkdirp.sync(getDirName(path)); 127 | fs.writeFileSync(path, output); 128 | }; 129 | 130 | /** 131 | * Read mock manifest and JSON files and compile into JS files ready for 132 | * inclusion into an Angular app. 133 | */ 134 | var writeScenarioData = function () { 135 | config.multipleFiles = config.multipleFiles || false; 136 | 137 | var defaultTemplate = singleFileDefaultTemplate; 138 | if (config.multipleFiles) { 139 | defaultTemplate = multipleFilesDefaultTemplate; 140 | } 141 | config.template = config.template || defaultTemplate; 142 | 143 | var mockSrc = _.isArray(config.src) ? _.first(config.src) : config.src; 144 | logger('mock source', mockSrc); 145 | logger('dest', config.dest); 146 | logger('template', config.template); 147 | logger('multipleFiles', config.multipleFiles); 148 | logger('plugins', config.plugins); 149 | 150 | // read all scenario data from manifest/JSON files 151 | var scenarioData = readScenarioData(config, mockSrc); 152 | 153 | logger('scenarioData', scenarioData); 154 | 155 | var scenarioModuleFilename = config.dest, 156 | scenarioString; 157 | 158 | if (!config.multipleFiles) { 159 | // stringify all scenario files into a single Angular module 160 | scenarioString = JSON.stringify(scenarioData); 161 | writeScenarioModule(config.template, scenarioModuleFilename, 162 | scenarioString); 163 | } else { 164 | fs.mkdirSync(config.dest); 165 | 166 | // stringify each scenario file into it's own Angular module 167 | for (var scenarioName in scenarioData) { 168 | if (scenarioData.hasOwnProperty(scenarioName)) { 169 | scenarioModuleFilename = config.dest + '/' + scenarioName + 170 | '.js'; 171 | 172 | scenarioString = JSON.stringify(scenarioData[scenarioName]); 173 | writeScenarioModule(config.template, scenarioModuleFilename, 174 | scenarioString, scenarioName); 175 | } 176 | } 177 | } 178 | }; 179 | 180 | return { 181 | writeScenarioData: writeScenarioData 182 | }; 183 | }; 184 | -------------------------------------------------------------------------------- /app/package/tasks/multimocksGrunt.js: -------------------------------------------------------------------------------- 1 | /* global require, module */ 2 | var _ = require('lodash'), 3 | MultimocksGenerator = require('./multimocksGenerator.js'); 4 | 5 | module.exports = function (grunt) { 6 | 7 | /** 8 | * Register Grunt task to compile mock resources into scenario data file. 9 | */ 10 | grunt.registerMultiTask('multimocks', 11 | 'Generate Angular Multimocks scenario module', 12 | function () { 13 | var config = _.first(this.files); 14 | 15 | var logger = function (message, content) { 16 | if (config.verbose) { 17 | grunt.log.writeln(message, content); 18 | } 19 | }; 20 | var multimocksGenerator = new MultimocksGenerator(logger, config); 21 | 22 | multimocksGenerator.writeScenarioData(); 23 | }); 24 | }; 25 | -------------------------------------------------------------------------------- /app/package/tasks/multimocksMultipleFiles.tpl: -------------------------------------------------------------------------------- 1 | /* global angular, exports, module */ 2 | 3 | (function (root, name, factory) { 4 | if (typeof angular === "object" && angular.module) { 5 | angular 6 | .module("scenario") 7 | .config([ 8 | "multimocksDataProvider", 9 | function (multimocksDataProvider) { 10 | multimocksDataProvider.setDefaultScenario("_default"); 11 | multimocksDataProvider.addMockData(name, factory()); 12 | } 13 | ]); 14 | } else if (typeof exports === "object") { 15 | module.exports = factory(); 16 | } 17 | })(this, "<%= scenarioDataName %>", function () { 18 | /* jshint ignore:start */ 19 | return <%= scenarioData %>; 20 | /* jshint ignore:end */ 21 | } 22 | ); 23 | -------------------------------------------------------------------------------- /app/package/tasks/plugins.js: -------------------------------------------------------------------------------- 1 | /* global module, require */ 2 | 3 | module.exports = { 4 | hal: require('./plugins/hal') 5 | }; 6 | -------------------------------------------------------------------------------- /app/package/tasks/plugins/hal.js: -------------------------------------------------------------------------------- 1 | /* global module, require */ 2 | 3 | var _ = require('lodash'); 4 | 5 | /** 6 | * Generate a list of all available links in all scenarios. 7 | */ 8 | var generateAvailableLinks = function (scenarioData) { 9 | var scenarioLinks = _.map(scenarioData, function (scenario) { 10 | return _.object(_.map(scenario, function (resource) { 11 | // return key-value array for _.object 12 | resource.scenario.rel = resource.filename.split('/')[0]; 13 | return [ 14 | resource.scenario.rel, 15 | { 16 | rel: resource.scenario.rel, 17 | href: '/' + resource.scenario.rel, 18 | method: resource.scenario.httpMethod 19 | } 20 | ]; 21 | })); 22 | }); 23 | return _.reduce(scenarioLinks, _.merge, {}); 24 | }; 25 | 26 | /** 27 | * Add response._links to all resources in a scenario. 28 | */ 29 | var scenarioWithLinks = function (links, scenario) { 30 | return _.map(scenario, function (resource) { 31 | var resourceClone = _.cloneDeep(resource); 32 | if (resourceClone.scenario.response) { 33 | if (resourceClone.scenario.relNames) { 34 | resourceClone.scenario.response._links = _.pick(links, 35 | resourceClone.scenario.relNames); 36 | } else { 37 | resourceClone.scenario.response._links = links; 38 | } 39 | } 40 | return resourceClone; 41 | }); 42 | }; 43 | 44 | /** 45 | * Generate dummy URIs for resources. 46 | */ 47 | var addHalUris = function (resource) { 48 | if (resource.scenario.rel === 'Root') { 49 | resource.scenario.uri = '/'; 50 | } else { 51 | resource.scenario.uri = '/' + resource.scenario.rel; 52 | } 53 | return resource; 54 | }; 55 | 56 | /** 57 | * Add _links to resources in all scenarios. 58 | */ 59 | var decorateWithHalLinks = function (data) { 60 | var links = generateAvailableLinks(data); 61 | return _.mapValues(data, function (scenario) { 62 | return scenarioWithLinks(links, scenario).map(function (resource) { 63 | return addHalUris(resource); 64 | }); 65 | }); 66 | }; 67 | 68 | module.exports = decorateWithHalLinks; 69 | -------------------------------------------------------------------------------- /app/src/demo/Gruntfile.js: -------------------------------------------------------------------------------- 1 | /* globals module */ 2 | 3 | module.exports = function (grunt) { 4 | // Normally you'd load angular-multimocks from NPM: 5 | // 6 | // grunt.loadNpmTasks('angular-multimocks'); 7 | // 8 | grunt.task.loadTasks('../../../tasks'); 9 | 10 | grunt.config.init({ 11 | multimocks: { 12 | demoApp: { 13 | src: 'mockData', 14 | dest: 'mockOutput.js', 15 | multipleFiles: false 16 | } 17 | } 18 | }); 19 | 20 | grunt.registerTask('default', ['multimocks']); 21 | }; 22 | -------------------------------------------------------------------------------- /app/src/demo/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Angular Scenario Demo 6 | 7 | 8 | 9 |
10 |

Angular Scenario Demo

11 | 12 |

Scenarios

13 | 14 | 20 | 21 |

Response

22 | 23 |
{{method}} {{uri}}
24 | 25 |
{{data | json}}
26 |
27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 48 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /app/src/demo/mockData/cart/empty.json: -------------------------------------------------------------------------------- 1 | { 2 | "httpMethod": "GET", 3 | "statusCode": 200, 4 | "uri": "/customer/\\d*/cart", 5 | "response": { 6 | "items": [] 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /app/src/demo/mockData/cart/outOfStock.json: -------------------------------------------------------------------------------- 1 | { 2 | "httpMethod": "GET", 3 | "statusCode": 200, 4 | "uri": "/customer/\\d*/cart", 5 | "response": { 6 | "items": [ 7 | { 8 | "title": "REST in Practise", 9 | "type": "Book", 10 | "desc": "In this insightful book, three SOA experts provide a down-to-earth explanation of REST and demonstrate how you can develop simple and elegant distributed hypermedia systems by applying the Web's guiding principles to common enterprise computing problems. You'll learn techniques for implementing specific Web technologies and patterns to solve the needs of a typical company as it grows from modest beginnings to become a global enterprise.", 11 | "inStock": "0" 12 | } 13 | ] 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /app/src/demo/mockData/cart/slowResponse.json: -------------------------------------------------------------------------------- 1 | { 2 | "httpMethod": "GET", 3 | "statusCode": 200, 4 | "uri": "/customer/\\d*/cart", 5 | "responseDelay": 2000, 6 | "response": { 7 | "items": [ 8 | { 9 | "title": "REST in Practise", 10 | "type": "Book", 11 | "desc": "In this insightful book, three SOA experts provide a down-to-earth explanation of REST and demonstrate how you can develop simple and elegant distributed hypermedia systems by applying the Web's guiding principles to common enterprise computing problems. You'll learn techniques for implementing specific Web technologies and patterns to solve the needs of a typical company as it grows from modest beginnings to become a global enterprise.", 12 | "inStock": "4" 13 | }, 14 | { 15 | "title": "LED Lenser P7.2 Pro Torch", 16 | "type": "Electronics", 17 | "desc": "The LED Lenser P7.2 Professional Torch is a medium-sized handheld torch that has all the best features of the P7, and offers even more in performance, design, durability and efficiency.", 18 | "inStock": "3" 19 | } 20 | ] 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /app/src/demo/mockData/cart/someItems.json: -------------------------------------------------------------------------------- 1 | { 2 | "httpMethod": "GET", 3 | "statusCode": 200, 4 | "uri": "/customer/\\d*/cart", 5 | "response": { 6 | "items": [ 7 | { 8 | "title": "REST in Practise", 9 | "type": "Book", 10 | "desc": "In this insightful book, three SOA experts provide a down-to-earth explanation of REST and demonstrate how you can develop simple and elegant distributed hypermedia systems by applying the Web's guiding principles to common enterprise computing problems. You'll learn techniques for implementing specific Web technologies and patterns to solve the needs of a typical company as it grows from modest beginnings to become a global enterprise.", 11 | "inStock": "4" 12 | }, 13 | { 14 | "title": "LED Lenser P7.2 Pro Torch", 15 | "type": "Electronics", 16 | "desc": "The LED Lenser P7.2 Professional Torch is a medium-sized handheld torch that has all the best features of the P7, and offers even more in performance, design, durability and efficiency.", 17 | "inStock": "3" 18 | } 19 | ] 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /app/src/demo/mockData/mockResources.json: -------------------------------------------------------------------------------- 1 | { 2 | "_default": [ 3 | "cart/empty.json" 4 | ], 5 | "someItems": [ 6 | "cart/someItems.json" 7 | ], 8 | "outOfStock": [ 9 | "cart/outOfStock.json" 10 | ], 11 | "slowResponse": [ 12 | "cart/slowResponse.json" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /app/src/demo/mockOutput.js: -------------------------------------------------------------------------------- 1 | /* global angular */ 2 | 3 | angular 4 | .module('scenario') 5 | 6 | .config(['multimocksDataProvider', function (multimocksDataProvider) { 7 | multimocksDataProvider.setDefaultScenario('_default'); 8 | // jscs:disable 9 | /* jshint ignore:start */ 10 | multimocksDataProvider.setMockData({"_default":[{"httpMethod":"GET","statusCode":200,"uri":"/customer/\\d*/cart","response":{"items":[]}}],"someItems":[{"httpMethod":"GET","statusCode":200,"uri":"/customer/\\d*/cart","response":{"items":[{"title":"REST in Practise","type":"Book","desc":"In this insightful book, three SOA experts provide a down-to-earth explanation of REST and demonstrate how you can develop simple and elegant distributed hypermedia systems by applying the Web's guiding principles to common enterprise computing problems. You'll learn techniques for implementing specific Web technologies and patterns to solve the needs of a typical company as it grows from modest beginnings to become a global enterprise.","inStock":"4"},{"title":"LED Lenser P7.2 Pro Torch","type":"Electronics","desc":"The LED Lenser P7.2 Professional Torch is a medium-sized handheld torch that has all the best features of the P7, and offers even more in performance, design, durability and efficiency.","inStock":"3"}]}},{"httpMethod":"GET","statusCode":200,"uri":"/customer/\\d*/cart","response":{"items":[]}}],"outOfStock":[{"httpMethod":"GET","statusCode":200,"uri":"/customer/\\d*/cart","response":{"items":[{"title":"REST in Practise","type":"Book","desc":"In this insightful book, three SOA experts provide a down-to-earth explanation of REST and demonstrate how you can develop simple and elegant distributed hypermedia systems by applying the Web's guiding principles to common enterprise computing problems. You'll learn techniques for implementing specific Web technologies and patterns to solve the needs of a typical company as it grows from modest beginnings to become a global enterprise.","inStock":"0"}]}},{"httpMethod":"GET","statusCode":200,"uri":"/customer/\\d*/cart","response":{"items":[]}}],"slowResponse":[{"httpMethod":"GET","statusCode":200,"uri":"/customer/\\d*/cart","responseDelay":2000,"response":{"items":[{"title":"REST in Practise","type":"Book","desc":"In this insightful book, three SOA experts provide a down-to-earth explanation of REST and demonstrate how you can develop simple and elegant distributed hypermedia systems by applying the Web's guiding principles to common enterprise computing problems. You'll learn techniques for implementing specific Web technologies and patterns to solve the needs of a typical company as it grows from modest beginnings to become a global enterprise.","inStock":"4"},{"title":"LED Lenser P7.2 Pro Torch","type":"Electronics","desc":"The LED Lenser P7.2 Professional Torch is a medium-sized handheld torch that has all the best features of the P7, and offers even more in performance, design, durability and efficiency.","inStock":"3"}]}},{"httpMethod":"GET","statusCode":200,"uri":"/customer/\\d*/cart","response":{"items":[]}}]}); 11 | /* jshint ignore:end */ 12 | // jscs:enable 13 | }]); 14 | -------------------------------------------------------------------------------- /app/src/js/multimocks.js: -------------------------------------------------------------------------------- 1 | /* global angular */ 2 | 3 | angular 4 | .module('scenario', ['ngMockE2E', 'multimocks.responseDelay']) 5 | 6 | .provider('multimocksData', function () { 7 | var mockData = {}, 8 | mockHeaders = { 9 | 'Content-type': 'application/json' 10 | }, 11 | defaultScenario = '_default'; 12 | 13 | this.setHeaders = function (data) { 14 | mockHeaders = data; 15 | }; 16 | 17 | this.setMockData = function (data) { 18 | mockData = data; 19 | }; 20 | 21 | this.addMockData = function (name, data) { 22 | mockData[name] = data; 23 | }; 24 | 25 | this.setDefaultScenario = function (scenario) { 26 | defaultScenario = scenario; 27 | }; 28 | 29 | this.$get = function $get() { 30 | return { 31 | getMockData: function () { 32 | return mockData; 33 | }, 34 | getDefaultScenario: function () { 35 | return defaultScenario; 36 | }, 37 | getHeaders: function () { 38 | return mockHeaders; 39 | } 40 | }; 41 | }; 42 | }) 43 | 44 | .factory('multimocks', [ 45 | '$q', 46 | '$http', 47 | '$httpBackend', 48 | 'multimocksData', 49 | 'scenarioMocks', 50 | function ($q, $http, $httpBackend, multimocksData, scenarioMocks) { 51 | var setupHttpBackendForMockResource = function (deferred, mock) { 52 | var mockHeaders = multimocksData.getHeaders(), 53 | uriRegExp = new RegExp('^' + mock.uri + '$'); 54 | 55 | // Mock a polling resource. 56 | if (mock.poll) { 57 | var pollCounter = 0, 58 | pollCount = mock.pollCount !== undefined ? mock.pollCount : 2; 59 | 60 | // Respond with a 204 which will then get polled until a 200 is 61 | // returned. 62 | $httpBackend 63 | .when(mock.httpMethod, uriRegExp, mock.requestData) 64 | .respond(function () { 65 | // Call a certain amount of times to simulate polling. 66 | if (pollCounter < pollCount) { 67 | pollCounter++; 68 | return [204, {}, mockHeaders]; 69 | } 70 | return [200, mock.response, mockHeaders]; 71 | }); 72 | } else { 73 | $httpBackend 74 | .when(mock.httpMethod, uriRegExp, mock.requestData) 75 | .respond(mock.statusCode, mock.response, mockHeaders); 76 | } 77 | 78 | // Make this HTTP request now if required otherwise just resolve 79 | // TODO deprecated? 80 | if (mock.callInSetup) { 81 | var req = {method: mock.httpMethod, url: mock.uri}; 82 | $http(req).success(function () { 83 | deferred.resolve(); 84 | }); 85 | } else { 86 | deferred.resolve(); 87 | } 88 | }; 89 | 90 | return { 91 | setup: function (scenarioName) { 92 | var deferred = $q.defer(); 93 | 94 | // Set mock for each item. 95 | var mocks = scenarioMocks.getMocks(scenarioName); 96 | for (var i in mocks) { 97 | setupHttpBackendForMockResource(deferred, mocks[i]); 98 | } 99 | 100 | return deferred.promise; 101 | } 102 | }; 103 | } 104 | ]) 105 | 106 | .factory('currentScenario', [ 107 | '$window', 108 | 'multimocksData', 109 | function ($window, multimocksData) { 110 | 111 | function getScenarioFromPath (path) { 112 | if (path.indexOf('scenario') !== -1) { 113 | var scenarioParams = path 114 | .slice(1) 115 | .split('&') 116 | .map(function (s) { return s.split('='); }) 117 | .filter(function (kv) { return kv[0] === 'scenario'; }); 118 | return scenarioParams[0][1]; 119 | } 120 | return undefined; 121 | } 122 | 123 | return { 124 | getName: function () { 125 | var scenarioFromURL = getScenarioFromPath($window.location.search); 126 | if (scenarioFromURL === undefined) { 127 | return multimocksData.getDefaultScenario(); 128 | } 129 | return scenarioFromURL; 130 | } 131 | }; 132 | } 133 | ]) 134 | 135 | .factory('scenarioMocks', [ 136 | '$log', 137 | 'multimocksData', 138 | 'currentScenario', 139 | 'multimocksLocation', 140 | function ($log, multimocksData, currentScenario, multimocksLocation) { 141 | var mockData = multimocksData.getMockData(); 142 | 143 | function urlMatchesRegex(url, regex) { 144 | var pattern = new RegExp(regex); 145 | return pattern.test(url); 146 | } 147 | 148 | function mergeScenarios(chosenScenario, defaultScenario) { 149 | var scenarioData = [].concat(chosenScenario); 150 | 151 | if (defaultScenario) { 152 | defaultScenario.forEach(function (scenario) { 153 | var isAlreadySet = false; 154 | var defaultUrl = scenario.uri + scenario.httpMethod; 155 | for (var i = 0; i < chosenScenario.length; i++) { 156 | var response = chosenScenario[i]; 157 | var responseUrl = response.uri + response.httpMethod; 158 | isAlreadySet = responseUrl === defaultUrl; 159 | if (isAlreadySet) { 160 | break; 161 | } 162 | } 163 | if (!isAlreadySet) { 164 | scenarioData.push(scenario); 165 | } 166 | }); 167 | } 168 | return scenarioData; 169 | } 170 | 171 | var scenarioMocks = { 172 | getMocks: function (scenarioToLoad) { 173 | var defaultScenario = mockData[multimocksData.getDefaultScenario()]; 174 | 175 | if (scenarioToLoad === multimocksData.getDefaultScenario()) { 176 | return defaultScenario; 177 | } 178 | 179 | if (mockData[scenarioToLoad] !== undefined) { 180 | var chosenScenario = mockData[scenarioToLoad]; 181 | return mergeScenarios(chosenScenario, defaultScenario); 182 | } 183 | 184 | if (scenarioToLoad) { 185 | $log.error('Mocks not found for scenario: ' + scenarioToLoad); 186 | } 187 | }, 188 | getMocksForCurrentScenario: function () { 189 | return scenarioMocks.getMocks(currentScenario.getName()); 190 | }, 191 | getDelayForResponse: function (response) { 192 | var globalDelay = multimocksLocation 193 | .getQueryStringValuesByKey('global_delay'); 194 | if (globalDelay !== undefined) { 195 | return parseInt(globalDelay[0]); 196 | } 197 | var availableMocks = scenarioMocks.getMocksForCurrentScenario(); 198 | 199 | for (var i in availableMocks) { 200 | var mock = availableMocks[i]; 201 | var sameURL = urlMatchesRegex(response.config.url, mock.uri); 202 | var sameMethod = (mock.httpMethod === response.config.method); 203 | if (sameMethod && sameURL) { 204 | return mock.responseDelay || 0; 205 | } 206 | } 207 | return 0; 208 | } 209 | }; 210 | return scenarioMocks; 211 | } 212 | ]) 213 | 214 | /** 215 | * Service to interact with the browser location 216 | */ 217 | .service('multimocksLocation', [ 218 | '$window', 219 | function ($window) { 220 | var multimocksLocation = {}; 221 | 222 | /** 223 | * Returns an array of values for a specified query string parameter. 224 | * 225 | * Handles multivalued keys and encoded characters. 226 | * 227 | * Usage: 228 | * 229 | * If the URL is /?foo=bar 230 | * 231 | * multimocksLocation.getQueryStringValuesByKey('foo') 232 | * 233 | * Will return 234 | * 235 | * ['bar'] 236 | * 237 | * @return Array 238 | * An array of values for the specified key. 239 | */ 240 | multimocksLocation.getQueryStringValuesByKey = function (key) { 241 | var queryDictionary = {}; 242 | $window.location.search 243 | .substr(1) 244 | .split('&') 245 | .forEach(function (item) { 246 | var s = item.split('='), 247 | k = s[0], 248 | v = s[1] && decodeURIComponent(s[1]); 249 | 250 | if (queryDictionary[k]) { 251 | queryDictionary[k].push(v); 252 | } else { 253 | queryDictionary[k] = [v]; 254 | } 255 | }); 256 | return queryDictionary[key]; 257 | }; 258 | 259 | return multimocksLocation; 260 | }]) 261 | 262 | .run([ 263 | 'multimocks', 264 | 'currentScenario', 265 | function (multimocks, currentScenario) { 266 | // load a scenario based on URL string, 267 | // e.g. http://example.com/?scenario=scenario1 268 | multimocks.setup(currentScenario.getName()); 269 | } 270 | ]); 271 | -------------------------------------------------------------------------------- /app/src/js/multimocks.responseDelay.js: -------------------------------------------------------------------------------- 1 | /* global angular */ 2 | 3 | angular 4 | .module('multimocks.responseDelay', []) 5 | 6 | .factory('responseDelay', [ 7 | '$q', 8 | '$timeout', 9 | 'scenarioMocks', 10 | function ($q, $timeout, scenarioMocks) { 11 | return { 12 | response: function (response) { 13 | var delayedResponse = $q.defer(); 14 | 15 | $timeout(function () { 16 | delayedResponse.resolve(response); 17 | }, scenarioMocks.getDelayForResponse(response)); 18 | 19 | return delayedResponse.promise; 20 | } 21 | }; 22 | } 23 | ]) 24 | 25 | .config([ 26 | '$httpProvider', 27 | function ($httpProvider) { 28 | $httpProvider.interceptors.push('responseDelay'); 29 | } 30 | ]); 31 | -------------------------------------------------------------------------------- /app/src/js/multimocks.responseDelay.spec.js: -------------------------------------------------------------------------------- 1 | /* global describe, beforeEach, jasmine, module, inject, it, expect */ 2 | 3 | describe('multimocks.responseDelay', function () { 4 | var responseDelay, 5 | httpProvider, 6 | $q, 7 | $timeout, 8 | scenarioMocks, 9 | mockedPromise; 10 | 11 | beforeEach(function () { 12 | mockedPromise = { 13 | promise: 'mypromise', 14 | resolve: jasmine.createSpy() 15 | }; 16 | module('multimocks.responseDelay', function ($provide, $httpProvider) { 17 | httpProvider = $httpProvider; 18 | 19 | $provide.value('httpProvider', { 20 | interceptors: [] 21 | }); 22 | $provide.value('scenarioMocks', { 23 | getDelayForResponse: jasmine.createSpy() 24 | }); 25 | $provide.value('$q', { 26 | defer: jasmine.createSpy().and.returnValue(mockedPromise) 27 | }); 28 | $provide.value('$timeout', jasmine.createSpy()); 29 | }); 30 | 31 | inject(function (_responseDelay_, _$q_, _$timeout_, _scenarioMocks_) { 32 | responseDelay = _responseDelay_; 33 | $q = _$q_; 34 | $timeout = _$timeout_; 35 | scenarioMocks = _scenarioMocks_; 36 | }); 37 | }); 38 | 39 | describe('config', function () { 40 | it('should add responseDelay to the $httpProvider interceptors', 41 | function () { 42 | // Assert 43 | expect(httpProvider.interceptors).toEqual(['responseDelay']); 44 | }); 45 | }); 46 | 47 | describe('responseDelay', function () { 48 | describe('response', function () { 49 | it('should return a promise', 50 | function () { 51 | // Arrange 52 | scenarioMocks.getDelayForResponse.and.returnValue(); 53 | 54 | // Act 55 | var result = responseDelay.response(); 56 | 57 | // Assert 58 | expect(result).toBe('mypromise'); 59 | }); 60 | 61 | it('should set $timeout with the expected arguments', 62 | function () { 63 | // Arrange 64 | scenarioMocks.getDelayForResponse.and.returnValue(123); 65 | 66 | // Act 67 | responseDelay.response(); 68 | 69 | // Assert 70 | expect($timeout).toHaveBeenCalledWith(jasmine.any(Function), 123); 71 | }); 72 | 73 | it('should call $timeout with a function that resolves promise', 74 | function () { 75 | // Arrange 76 | scenarioMocks.getDelayForResponse.and.returnValue(); 77 | 78 | // Act 79 | responseDelay.response('foo'); 80 | /* 81 | * Because we are passing an anonymous function to $timeout we can't 82 | * assert that mockFn is being passed to $timeout. 83 | * By calling the most recent function we can assert that 84 | * the correct function was called. 85 | */ 86 | $timeout.calls.mostRecent().args[0](); 87 | 88 | // Assert 89 | expect(mockedPromise.resolve).toHaveBeenCalledWith('foo'); 90 | }); 91 | }); 92 | }); 93 | }); 94 | -------------------------------------------------------------------------------- /app/src/js/multimocks.spec.js: -------------------------------------------------------------------------------- 1 | /* global describe, beforeEach, jasmine, module, inject, it, expect */ 2 | 3 | describe('multimocks', function () { 4 | var mockHttpBackend, mockWindow, multimocksDataProvider, multimocksData, 5 | multimocks, scenario1, scenario2, defaultScenario, pollScenario, 6 | delayedResponseScenario, scenarios, mockHeaders, mockUriRegExp, 7 | regexScenario; 8 | 9 | beforeEach(function () { 10 | defaultScenario = [ 11 | { 12 | uri: '/test', 13 | httpMethod: 'GET', 14 | statusCode: 200, 15 | response: { 16 | scenario: 'default' 17 | } 18 | }, 19 | { 20 | uri: '/test', 21 | httpMethod: 'POST', 22 | statusCode: 200, 23 | response: { 24 | scenario: 'default' 25 | } 26 | }, 27 | { 28 | uri: '/test-two', 29 | httpMethod: 'POST', 30 | statusCode: 200, 31 | response: { 32 | scenario: 'default' 33 | } 34 | } 35 | ]; 36 | 37 | scenario1 = [ 38 | { 39 | uri: '/test', 40 | httpMethod: 'GET', 41 | statusCode: 200, 42 | response: { 43 | scenario: 'scenario1' 44 | } 45 | } 46 | ]; 47 | 48 | scenario2 = [ 49 | { 50 | uri: '/test', 51 | httpMethod: 'GET', 52 | statusCode: 200, 53 | response: { 54 | scenario: 'scenario2' 55 | } 56 | } 57 | ]; 58 | 59 | regexScenario = [ 60 | { 61 | uri: '/test/\\d*/foo', 62 | httpMethod: 'GET', 63 | statusCode: 200, 64 | responseDelay: 345, 65 | response: { 66 | scenario: 'regexScenario' 67 | } 68 | } 69 | ]; 70 | 71 | pollScenario = [ 72 | { 73 | uri: '/test', 74 | httpMethod: 'GET', 75 | statusCode: 200, 76 | poll: true, 77 | pollCount: 3, 78 | response: { 79 | scenario: 'poll' 80 | } 81 | } 82 | ]; 83 | 84 | delayedResponseScenario = [ 85 | { 86 | uri: '/delayed', 87 | httpMethod: 'GET', 88 | statusCode: 123, 89 | responseDelay: 9876, 90 | response: { 91 | data: 'delayed' 92 | } 93 | } 94 | ]; 95 | 96 | scenarios = { 97 | scenario1: scenario1, 98 | scenario2: scenario2 99 | }; 100 | 101 | mockHttpBackend = jasmine.createSpyObj('$httpBackend', [ 102 | 'when', 103 | 'respond' 104 | ]); 105 | mockHttpBackend.when.and.returnValue(mockHttpBackend); 106 | 107 | mockHeaders = {foo: 'bar'}; 108 | 109 | mockUriRegExp = new RegExp('^/test$'); 110 | }); 111 | 112 | describe('multimocksDataProvider', function () { 113 | beforeEach(function () { 114 | module( 115 | 'scenario', 116 | function ($provide, _multimocksDataProvider_) { 117 | $provide.value('$httpBackend', mockHttpBackend); 118 | multimocksDataProvider = _multimocksDataProvider_; 119 | } 120 | ); 121 | 122 | inject(function (_multimocksData_, _multimocks_) { 123 | multimocksData = _multimocksData_; 124 | multimocks = _multimocks_; 125 | }); 126 | }); 127 | 128 | it('should allow a client app to set response headers', function () { 129 | // act 130 | multimocksDataProvider.setHeaders(mockHeaders); 131 | 132 | // assert 133 | expect(multimocksData.getHeaders()).toEqual(mockHeaders); 134 | }); 135 | 136 | it('should have json as the default content type', function () { 137 | // assert 138 | expect(multimocksData.getHeaders()).toEqual({ 139 | 'Content-type': 'application/json' 140 | }); 141 | }); 142 | 143 | it('should allow a client app to set mock data', function () { 144 | // act 145 | multimocksDataProvider.setMockData(scenarios); 146 | 147 | // assert 148 | expect(multimocksData.getMockData()).toEqual(scenarios); 149 | }); 150 | 151 | it('should allow a client app to incrementally add mock data', function () { 152 | // act 153 | multimocksDataProvider.addMockData('scenario1', scenario1); 154 | multimocksDataProvider.addMockData('scenario2', scenario2); 155 | 156 | // assert 157 | expect(multimocksData.getMockData()).toEqual(scenarios); 158 | }); 159 | 160 | it('should load the default scenario if specified', function () { 161 | // arrange 162 | multimocksDataProvider.addMockData('_default', scenario2); 163 | multimocksDataProvider.setHeaders(mockHeaders); 164 | 165 | // act 166 | multimocks.setup('_default'); 167 | 168 | // assert 169 | var mockResource = scenario2[0]; 170 | expect(mockHttpBackend.when).toHaveBeenCalledWith( 171 | mockResource.httpMethod, mockUriRegExp, mockResource.requestData); 172 | expect(mockHttpBackend.respond).toHaveBeenCalledWith( 173 | mockResource.statusCode, mockResource.response, mockHeaders); 174 | }); 175 | 176 | it('should allow a client app to set the default scenario', function () { 177 | // arrange 178 | var defaultScenario = 'foo'; 179 | 180 | // act 181 | multimocksDataProvider.setDefaultScenario(defaultScenario); 182 | 183 | // assert 184 | expect(multimocksData.getDefaultScenario()).toEqual(defaultScenario); 185 | }); 186 | }); 187 | 188 | describe('setup', function () { 189 | var setupMultimocks = function (mockData) { 190 | mockWindow = {location: {search: '?scenario=scenario2'}}; 191 | module( 192 | 'scenario', 193 | function ($provide, _multimocksDataProvider_) { 194 | $provide.value('$httpBackend', mockHttpBackend); 195 | $provide.value('$window', mockWindow); 196 | multimocksDataProvider = _multimocksDataProvider_; 197 | multimocksDataProvider.setMockData(mockData); 198 | multimocksDataProvider.setHeaders(mockHeaders); 199 | } 200 | ); 201 | inject(); 202 | }; 203 | 204 | it('should load the scenario specified on the query string', function () { 205 | // arrange 206 | setupMultimocks(scenarios); 207 | 208 | // assert 209 | var mockResource = scenario2[0]; 210 | expect(mockHttpBackend.when).toHaveBeenCalledWith( 211 | mockResource.httpMethod, mockUriRegExp, mockResource.requestData); 212 | expect(mockHttpBackend.respond).toHaveBeenCalledWith( 213 | mockResource.statusCode, mockResource.response, mockHeaders); 214 | }); 215 | 216 | it('should do nothing if the specified scenario isn\'t found', function () { 217 | // arrange - inject empty mock data 218 | setupMultimocks({}); 219 | 220 | // assert 221 | expect(mockHttpBackend.when).not.toHaveBeenCalled(); 222 | expect(mockHttpBackend.respond).not.toHaveBeenCalled(); 223 | }); 224 | 225 | it('should register a function to generate responses for mocks with ' + 226 | 'polling', function () { 227 | // arrange 228 | setupMultimocks({scenario2: pollScenario}); 229 | 230 | // assert 231 | var mockResource = scenario2[0]; 232 | expect(mockHttpBackend.when).toHaveBeenCalledWith( 233 | mockResource.httpMethod, mockUriRegExp, mockResource.requestData); 234 | expect(mockHttpBackend.respond) 235 | .toHaveBeenCalledWith(jasmine.any(Function)); 236 | }); 237 | }); 238 | 239 | describe('currentScenario', function () { 240 | var currentScenario; 241 | 242 | beforeEach(module('scenario', 243 | function ($provide) { 244 | mockWindow = {location: {search: ''}}; 245 | // Setup mocks 246 | $provide.value('$window', mockWindow); 247 | })); 248 | 249 | beforeEach(inject(function (_currentScenario_) { 250 | currentScenario = _currentScenario_; 251 | })); 252 | 253 | describe('getName', function () { 254 | it('should return the scenario name if it is in the path', function () { 255 | // Arrange 256 | mockWindow.location.search = '?scenario=foo'; 257 | 258 | // Act - Assert 259 | expect(currentScenario.getName()).toBe('foo'); 260 | }); 261 | 262 | it('should return default if no scenario name is in the path', 263 | function () { 264 | // Arrange 265 | mockWindow.location.search = ''; 266 | 267 | // Act - Assert 268 | expect(currentScenario.getName()).toBe('_default'); 269 | }); 270 | 271 | it('should return default if other no scenario name is in the path, ' + 272 | 'but other items are', 273 | function () { 274 | // Arrange 275 | mockWindow.location.search = '?other=stuff'; 276 | 277 | // Act - Assert 278 | expect(currentScenario.getName()).toBe('_default'); 279 | }); 280 | }); 281 | }); 282 | 283 | describe('scenarioMocks', function () { 284 | var scenarioMocks, 285 | currentScenario, 286 | $log, 287 | multimocksLocation; 288 | 289 | function setupModule(mockData, defaultScenarioName) { 290 | module('scenario', function ($provide) { 291 | $provide.value('multimocksData', { 292 | getMockData: jasmine.createSpy() 293 | .and.returnValue(mockData), 294 | getDefaultScenario: jasmine.createSpy() 295 | .and.returnValue(defaultScenarioName) 296 | }); 297 | $provide.value('$log', { 298 | error: jasmine.createSpy() 299 | }); 300 | $provide.value('currentScenario', { 301 | getName: jasmine.createSpy() 302 | }); 303 | $provide.value('multimocks', { 304 | setup: jasmine.createSpy() 305 | }); 306 | $provide.value('multimocksLocation', { 307 | getQueryStringValuesByKey: jasmine.createSpy() 308 | }); 309 | }); 310 | 311 | inject(function (_scenarioMocks_, _$log_, _multimocksData_, 312 | _currentScenario_, _multimocksLocation_) { 313 | scenarioMocks = _scenarioMocks_; 314 | multimocksData = _multimocksData_; 315 | currentScenario = _currentScenario_; 316 | $log = _$log_; 317 | multimocksLocation = _multimocksLocation_; 318 | }); 319 | } 320 | 321 | describe('with default scenario', function () { 322 | it('should return mocks for a valid scenario with merged default data', 323 | function () { 324 | setupModule({ 325 | defaultScenario: defaultScenario, 326 | scenario1: scenario1, 327 | scenario2: scenario2 328 | }, 'defaultScenario'); 329 | 330 | var expectedScenario = [].concat(scenario1).concat([ 331 | { 332 | uri: '/test', 333 | httpMethod: 'POST', 334 | statusCode: 200, 335 | response: { 336 | scenario: 'default' 337 | } 338 | }, 339 | { 340 | uri: '/test-two', 341 | httpMethod: 'POST', 342 | statusCode: 200, 343 | response: { 344 | scenario: 'default' 345 | } 346 | } 347 | ]); 348 | // Act 349 | var mocks = scenarioMocks.getMocks('scenario1'); 350 | 351 | // Assert 352 | expect(mocks).toEqual(expectedScenario); 353 | }); 354 | }); 355 | describe('no default scenario', function () { 356 | 357 | beforeEach(function () { 358 | setupModule(scenarios); 359 | }); 360 | 361 | describe('getMocks', function () { 362 | it('should return mocks for a valid scenario', function () { 363 | // Act 364 | var mocks = scenarioMocks.getMocks('scenario1'); 365 | 366 | // Assert 367 | expect(mocks).toEqual(scenario1); 368 | }); 369 | 370 | it('should return undefined for a scenario that doesn\'t exist', 371 | function () { 372 | // Act 373 | var mocks = scenarioMocks.getMocks('badScenario'); 374 | 375 | // Assert 376 | expect(mocks).toBe(undefined); 377 | }); 378 | 379 | it('should log when no mocks can be found for a specified scenario', 380 | function () { 381 | // Act 382 | scenarioMocks.getMocks('notFoundScenario'); 383 | 384 | // Assert 385 | expect($log.error).toHaveBeenCalledWith( 386 | 'Mocks not found for scenario: notFoundScenario'); 387 | }); 388 | }); 389 | 390 | describe('getMocksForCurrentScenario', function () { 391 | it('should get mocks for the current scenario', function () { 392 | // Arrange 393 | scenarioMocks.getMocks = jasmine.createSpy().and 394 | .returnValue({data: 'value'}); 395 | currentScenario.getName.and.returnValue('scenario3'); 396 | 397 | // Act 398 | var mocks = scenarioMocks.getMocksForCurrentScenario(); 399 | 400 | // Assert 401 | expect(scenarioMocks.getMocks).toHaveBeenCalledWith('scenario3'); 402 | expect(mocks).toEqual({data: 'value'}); 403 | }); 404 | }); 405 | 406 | describe('getDelayForResponse', function () { 407 | it('should return 0 when a mock isn\'t set for a response', 408 | function () { 409 | // Arrange 410 | scenarioMocks.getMocksForCurrentScenario = jasmine.createSpy() 411 | .and.returnValue(delayedResponseScenario); 412 | currentScenario.getName.and.returnValue('scenario3'); 413 | var mockedResponse = { 414 | config: { 415 | method: 'UNKNOWN', 416 | url: '/different/path' 417 | } 418 | }; 419 | 420 | // Act 421 | var delay = scenarioMocks.getDelayForResponse(mockedResponse); 422 | 423 | // Assert 424 | expect(delay).toEqual(0); 425 | }); 426 | 427 | it('should return 0 when a mock without a delay is set for a response', 428 | function () { 429 | // Arrange 430 | scenarioMocks.getMocksForCurrentScenario = jasmine.createSpy() 431 | .and.returnValue(scenario1); 432 | currentScenario.getName.and.returnValue('scenario3'); 433 | var mockedResponse = { 434 | config: { 435 | method: 'GET', 436 | url: '/test' 437 | } 438 | }; 439 | 440 | // Act 441 | var delay = scenarioMocks.getDelayForResponse(mockedResponse); 442 | 443 | // Assert 444 | expect(delay).toEqual(0); 445 | }); 446 | 447 | it('should return delay when a mock with a delay is set for a response', 448 | function () { 449 | // Arrange 450 | scenarioMocks.getMocksForCurrentScenario = jasmine.createSpy() 451 | .and.returnValue(delayedResponseScenario); 452 | currentScenario.getName.and.returnValue('delayedResponseScenario'); 453 | var mockedResponse = { 454 | config: { 455 | method: 'GET', 456 | url: '/delayed' 457 | } 458 | }; 459 | 460 | // Act 461 | var delay = scenarioMocks.getDelayForResponse(mockedResponse); 462 | 463 | // Assert 464 | expect(delay).toBe(9876); 465 | }); 466 | 467 | it('should return delay for a mock that has a regex for URL', 468 | function () { 469 | // Arrange 470 | scenarioMocks.getMocksForCurrentScenario = jasmine.createSpy() 471 | .and.returnValue(regexScenario); 472 | currentScenario.getName.and.returnValue('regexScenario'); 473 | var mockedResponse = { 474 | config: { 475 | method: 'GET', 476 | url: '/test/123/foo' 477 | } 478 | }; 479 | 480 | // Act 481 | var delay = scenarioMocks.getDelayForResponse(mockedResponse); 482 | 483 | // Assert 484 | expect(delay).toBe(345); 485 | }); 486 | 487 | it('should return overridden global delay when specified in url', 488 | function () { 489 | // Arrange 490 | multimocksLocation.getQueryStringValuesByKey 491 | .and.returnValue(['123']); 492 | scenarioMocks.getMocksForCurrentScenario = jasmine.createSpy() 493 | .and.returnValue(delayedResponseScenario); 494 | currentScenario.getName.and.returnValue('delayedResponseScenario'); 495 | var mockedResponse = { 496 | config: { 497 | method: 'GET', 498 | url: '/delayed' 499 | } 500 | }; 501 | 502 | // Act 503 | var delay = scenarioMocks.getDelayForResponse(mockedResponse); 504 | 505 | // Assert 506 | expect(delay).toBe(123); 507 | }); 508 | }); 509 | }); 510 | }); 511 | 512 | describe('multimocksLocation', function () { 513 | var multimocksLocation, 514 | $window; 515 | 516 | beforeEach(function () { 517 | module('scenario', function ($provide) { 518 | $provide.value('$window', { 519 | location: { 520 | search: '' 521 | } 522 | }); 523 | }); 524 | 525 | inject(function (_multimocksLocation_, _$window_) { 526 | multimocksLocation = _multimocksLocation_; 527 | $window = _$window_; 528 | }); 529 | }); 530 | 531 | describe('getQueryStringValuesByKey', function () { 532 | it('should return undefined if there are no matching items', function () { 533 | // Arrange 534 | $window.location.search = '?bar=baz'; 535 | 536 | // Act 537 | var result = multimocksLocation.getQueryStringValuesByKey('foo'); 538 | 539 | // Assert 540 | expect(result).toBe(undefined); 541 | }); 542 | 543 | it('should return multiple results if there multiple items', function () { 544 | // Arrange 545 | $window.location.search = '?foo=1&bar=something&foo=2'; 546 | 547 | // Act 548 | var result = multimocksLocation.getQueryStringValuesByKey('foo'); 549 | 550 | // Assert 551 | expect(result).toEqual(['1', '2']); 552 | }); 553 | 554 | it('should return results for URL encoded values', function () { 555 | // Arrange 556 | $window.location.search = '?url=http%3A%2F%2Fw3schools.com'; 557 | 558 | // Act 559 | var result = multimocksLocation.getQueryStringValuesByKey('url'); 560 | 561 | // Assert 562 | expect(result).toEqual(['http://w3schools.com']); 563 | }); 564 | 565 | it('should return an array with undefined for keys without values', 566 | function () { 567 | // Arrange 568 | $window.location.search = '?foo=1&bar'; 569 | 570 | // Act 571 | var result = multimocksLocation.getQueryStringValuesByKey('bar'); 572 | 573 | // Assert 574 | expect(result).toEqual([undefined]); 575 | }); 576 | 577 | }); 578 | }); 579 | 580 | describe('run', function () { 581 | var currentScenario; 582 | 583 | beforeEach(function () { 584 | module('scenario', function ($provide) { 585 | $provide.value('multimocks', { 586 | setup: jasmine.createSpy() 587 | }); 588 | 589 | $provide.value('currentScenario', { 590 | getName: jasmine.createSpy().and.returnValue('myScenarioName') 591 | }); 592 | }); 593 | 594 | inject(function (_multimocks_, _currentScenario_) { 595 | multimocks = _multimocks_; 596 | currentScenario = _currentScenario_; 597 | }); 598 | }); 599 | 600 | it('should set up mocks with the current scenario name', function () { 601 | // Assert 602 | expect(multimocks.setup).toHaveBeenCalledWith('myScenarioName'); 603 | }); 604 | }); 605 | }); 606 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angular-multimocks", 3 | "version": "0.6.10", 4 | "authors": [ 5 | { 6 | "name": "Nabil Boag", 7 | "email": "nabil.boag@wonga.com" 8 | } 9 | ], 10 | "description": "Tools for managing mock data scenarios in AngularJS applications", 11 | "main": "app/package/js/angular-multimocks.js", 12 | "dependencies": {}, 13 | "ignore": [ 14 | "app/src", 15 | "tasks", 16 | ".*", 17 | "Gruntfile.js", 18 | "karma-unit.conf.js", 19 | "package.json", 20 | "LICENSE", 21 | "README.md" 22 | ] 23 | } 24 | -------------------------------------------------------------------------------- /karma-unit.conf.js: -------------------------------------------------------------------------------- 1 | /* global module */ 2 | 3 | // Karma configuration 4 | // http://karma-runner.github.io/0.10/config/configuration-file.html 5 | 6 | module.exports = function (config) { 7 | config.set({ 8 | basePath: 'app/build', 9 | plugins: [ 10 | 'karma-jasmine', 11 | 'karma-coverage', 12 | 'karma-firefox-launcher', 13 | 'karma-phantomjs-launcher', 14 | 'karma-chrome-launcher' 15 | ], 16 | port: 9876, 17 | captureTimeout: 60000, 18 | 19 | frameworks: ['jasmine'], 20 | files: [ 21 | 'node_modules/angular/angular.js', 22 | 'node_modules/angular-mocks/angular-mocks.js', 23 | 'js/**/*.js' 24 | ], 25 | preprocessors: { 26 | '!(node_modules)/**/*.js': 'coverage' 27 | }, 28 | 29 | /** 30 | * How to report, by default. 31 | */ 32 | reporters: ['coverage', 'dots'], 33 | 34 | coverageReporter: { 35 | type: 'html', 36 | dir: '../../coverage/' 37 | }, 38 | 39 | singleRun: true, 40 | browsers: [ 41 | 'Chrome', 42 | 'Firefox' 43 | ] 44 | }); 45 | }; 46 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angular-multimocks", 3 | "version": "0.7.1", 4 | "description": "Tools for managing mock data scenarios in AngularJS applications", 5 | "repository": { 6 | "type": "git", 7 | "url": "git://github.com/wongatech/angular-multimocks" 8 | }, 9 | "scripts": { 10 | "dev": "grunt workflow:dev", 11 | "test": "grunt build test", 12 | "package": "grunt package" 13 | }, 14 | "files": [ 15 | "app/package", 16 | "tasks" 17 | ], 18 | "dependencies": { 19 | "angular": "~1.2.0", 20 | "angular-mocks": "~1.2.0", 21 | "gulp-util": "~3.0.7", 22 | "lodash": "^3.10.1", 23 | "mkdirp": "^0.5.1" 24 | }, 25 | "devDependencies": { 26 | "connect": "~2.9.0", 27 | "grunt": "~0.4.1", 28 | "grunt-cli": "^1.2.0", 29 | "grunt-contrib-clean": "~0.4.1", 30 | "grunt-contrib-concat": "~0.3.0", 31 | "grunt-contrib-connect": "^0.11.2", 32 | "grunt-contrib-copy": "~0.4.1", 33 | "grunt-contrib-jshint": "^1.1.0", 34 | "grunt-contrib-uglify": "~0.2.0", 35 | "grunt-contrib-watch": "~0.5.3", 36 | "grunt-jscs": "^2.1.0", 37 | "grunt-karma": "~2.0.0", 38 | "grunt-open": "^0.2.3", 39 | "jasmine-core": "^2.3.4", 40 | "karma": "^1.3.0", 41 | "karma-chrome-launcher": "^0.1.3", 42 | "karma-coverage": "^0.2.1", 43 | "karma-firefox-launcher": "^0.1.3", 44 | "karma-jasmine": "^1.1.0", 45 | "karma-ng-scenario": "^0.1.0", 46 | "karma-phantomjs-launcher": "^1.0.2" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /tasks/gulp/multimocksGulp.js: -------------------------------------------------------------------------------- 1 | /* global require, module */ 2 | 3 | var gutil = require('gulp-util'), 4 | MultimocksGenerator = require('../multimocksGenerator.js'); 5 | 6 | module.exports = function (config) { 7 | 8 | var logger = function (message, content) { 9 | if (config.verbose) { 10 | gutil.log(message, content); 11 | } 12 | }; 13 | 14 | var multimocksGenerator = new MultimocksGenerator(logger, config); 15 | 16 | multimocksGenerator.writeScenarioData(); 17 | }; 18 | -------------------------------------------------------------------------------- /tasks/multimocks.tpl: -------------------------------------------------------------------------------- 1 | /* global angular */ 2 | 3 | angular 4 | .module('scenario') 5 | 6 | .config(['multimocksDataProvider', function (multimocksDataProvider) { 7 | multimocksDataProvider.setDefaultScenario('_default'); 8 | // jscs:disable 9 | /* jshint ignore:start */ 10 | multimocksDataProvider.setMockData(<%= scenarioData %>); 11 | /* jshint ignore:end */ 12 | // jscs:enable 13 | }]); 14 | -------------------------------------------------------------------------------- /tasks/multimocksGenerator.js: -------------------------------------------------------------------------------- 1 | /* global require, module, process */ 2 | 3 | var _ = require('lodash'), 4 | path = require('path'), 5 | fs = require('fs'), 6 | pluginRegistry = require('./plugins'), 7 | mkdirp = require('mkdirp'), 8 | getDirName = path.dirname; 9 | 10 | var pwd = path.dirname(module.filename), 11 | singleFileDefaultTemplate = path.join(pwd, 'multimocks.tpl'), 12 | multipleFilesDefaultTemplate = path.join(pwd, 13 | 'multimocksMultipleFiles.tpl'), 14 | mockManifestFilename = 'mockResources.json'; 15 | 16 | module.exports = function (logger, config) { 17 | 18 | /** 19 | * Read a scenario from a list of resource files, add URIs and merge in 20 | * resources from default scenario. 21 | */ 22 | var readScenario = function (config, mockSrc, defaultScenario, filenames, 23 | scenarioName) { 24 | // read mock data files for this scenario 25 | var scenario = filenames.map(function (filename) { 26 | var filepath = fs.realpathSync(path.join(mockSrc, filename)); 27 | 28 | return { 29 | scenarioName: scenarioName, 30 | filename: filename, 31 | scenario: require(filepath) 32 | }; 33 | }); 34 | 35 | return scenario; 36 | }; 37 | 38 | /** 39 | * Read scenario definitions and return a structure that 40 | * multimockDataProvider.setMockData will understand. 41 | */ 42 | var readMockManifest = function (config, mockSrc) { 43 | var mockManifestPath = path.join(process.cwd(), mockSrc, 44 | mockManifestFilename), 45 | 46 | // read manifest JSON by require'ing it 47 | mockManifest = require(mockManifestPath), 48 | 49 | // read files for default scenario first, so we can merge it into other 50 | // scenarios later 51 | defaultScenario = readScenario(config, mockSrc, [], 52 | mockManifest._default, '_default'); 53 | 54 | // read files for each scenario 55 | return _.mapValues(mockManifest, function (filenames, scenarioName) { 56 | return readScenario(config, mockSrc, defaultScenario, filenames, 57 | scenarioName); 58 | }); 59 | }; 60 | 61 | /** 62 | * Executes each of the plugins configured in the application to 63 | * decorate responses. 64 | * 65 | * @param {object} data 66 | * @param {array} plugins 67 | * @return {object} decoratedData 68 | */ 69 | var runPlugins = function (data, pluginNames) { 70 | logger('runPlugins input', data); 71 | var plugins = pluginNames.map(function (pn) { return pluginRegistry[pn]; }), 72 | applyPlugin = function (oldData, plugin) { return plugin(oldData); }; 73 | // Use reduce to apply all the plugins to the data 74 | var output = plugins.reduce(applyPlugin, data); 75 | logger('runPlugins output', output); 76 | return output; 77 | }; 78 | 79 | /** 80 | * Strip context metadata from scenarios. 81 | */ 82 | var removeContext = function (dataWithContext) { 83 | return _.mapValues(dataWithContext, function (scenario) { 84 | return scenario.map(function (response) { 85 | return response.scenario; 86 | }); 87 | }); 88 | }; 89 | 90 | /** 91 | * Return a javascript object of all scenario data. 92 | * 93 | * @param {string} config 94 | * @param {string} mockSrc 95 | * 96 | * @returns {object} 97 | */ 98 | var readScenarioData = function (config, mockSrc) { 99 | var dataWithContext = readMockManifest(config, mockSrc); 100 | 101 | // log('readScenarioData config', config); 102 | if (config.plugins) { 103 | dataWithContext = runPlugins(dataWithContext, config.plugins); 104 | } 105 | 106 | return removeContext(dataWithContext); 107 | }; 108 | 109 | /** 110 | * Save the file 111 | * 112 | * @param {string} template 113 | * @param {string} path 114 | * @param {string} data 115 | * @param {string} name 116 | */ 117 | var writeScenarioModule = function (templatePath, path, data, name) { 118 | var templateString = fs.readFileSync(templatePath); 119 | 120 | // generate scenarioData.js contents by inserting data into template 121 | var templateData = {scenarioData: data}; 122 | templateData.scenarioDataName = name || ''; 123 | 124 | var output = _.template(templateString)(templateData); 125 | 126 | mkdirp.sync(getDirName(path)); 127 | fs.writeFileSync(path, output); 128 | }; 129 | 130 | /** 131 | * Read mock manifest and JSON files and compile into JS files ready for 132 | * inclusion into an Angular app. 133 | */ 134 | var writeScenarioData = function () { 135 | config.multipleFiles = config.multipleFiles || false; 136 | 137 | var defaultTemplate = singleFileDefaultTemplate; 138 | if (config.multipleFiles) { 139 | defaultTemplate = multipleFilesDefaultTemplate; 140 | } 141 | config.template = config.template || defaultTemplate; 142 | 143 | var mockSrc = _.isArray(config.src) ? _.first(config.src) : config.src; 144 | logger('mock source', mockSrc); 145 | logger('dest', config.dest); 146 | logger('template', config.template); 147 | logger('multipleFiles', config.multipleFiles); 148 | logger('plugins', config.plugins); 149 | 150 | // read all scenario data from manifest/JSON files 151 | var scenarioData = readScenarioData(config, mockSrc); 152 | 153 | logger('scenarioData', scenarioData); 154 | 155 | var scenarioModuleFilename = config.dest, 156 | scenarioString; 157 | 158 | if (!config.multipleFiles) { 159 | // stringify all scenario files into a single Angular module 160 | scenarioString = JSON.stringify(scenarioData); 161 | writeScenarioModule(config.template, scenarioModuleFilename, 162 | scenarioString); 163 | } else { 164 | fs.mkdirSync(config.dest); 165 | 166 | // stringify each scenario file into it's own Angular module 167 | for (var scenarioName in scenarioData) { 168 | if (scenarioData.hasOwnProperty(scenarioName)) { 169 | scenarioModuleFilename = config.dest + '/' + scenarioName + 170 | '.js'; 171 | 172 | scenarioString = JSON.stringify(scenarioData[scenarioName]); 173 | writeScenarioModule(config.template, scenarioModuleFilename, 174 | scenarioString, scenarioName); 175 | } 176 | } 177 | } 178 | }; 179 | 180 | return { 181 | writeScenarioData: writeScenarioData 182 | }; 183 | }; 184 | -------------------------------------------------------------------------------- /tasks/multimocksGrunt.js: -------------------------------------------------------------------------------- 1 | /* global require, module */ 2 | var _ = require('lodash'), 3 | MultimocksGenerator = require('./multimocksGenerator.js'); 4 | 5 | module.exports = function (grunt) { 6 | 7 | /** 8 | * Register Grunt task to compile mock resources into scenario data file. 9 | */ 10 | grunt.registerMultiTask('multimocks', 11 | 'Generate Angular Multimocks scenario module', 12 | function () { 13 | var config = _.first(this.files); 14 | 15 | var logger = function (message, content) { 16 | if (config.verbose) { 17 | grunt.log.writeln(message, content); 18 | } 19 | }; 20 | var multimocksGenerator = new MultimocksGenerator(logger, config); 21 | 22 | multimocksGenerator.writeScenarioData(); 23 | }); 24 | }; 25 | -------------------------------------------------------------------------------- /tasks/multimocksMultipleFiles.tpl: -------------------------------------------------------------------------------- 1 | /* global angular, exports, module */ 2 | 3 | (function (root, name, factory) { 4 | if (typeof angular === "object" && angular.module) { 5 | angular 6 | .module("scenario") 7 | .config([ 8 | "multimocksDataProvider", 9 | function (multimocksDataProvider) { 10 | multimocksDataProvider.setDefaultScenario("_default"); 11 | multimocksDataProvider.addMockData(name, factory()); 12 | } 13 | ]); 14 | } else if (typeof exports === "object") { 15 | module.exports = factory(); 16 | } 17 | })(this, "<%= scenarioDataName %>", function () { 18 | /* jshint ignore:start */ 19 | return <%= scenarioData %>; 20 | /* jshint ignore:end */ 21 | } 22 | ); 23 | -------------------------------------------------------------------------------- /tasks/plugins.js: -------------------------------------------------------------------------------- 1 | /* global module, require */ 2 | 3 | module.exports = { 4 | hal: require('./plugins/hal') 5 | }; 6 | -------------------------------------------------------------------------------- /tasks/plugins/hal.js: -------------------------------------------------------------------------------- 1 | /* global module, require */ 2 | 3 | var _ = require('lodash'); 4 | 5 | /** 6 | * Generate a list of all available links in all scenarios. 7 | */ 8 | var generateAvailableLinks = function (scenarioData) { 9 | var scenarioLinks = _.map(scenarioData, function (scenario) { 10 | return _.object(_.map(scenario, function (resource) { 11 | // return key-value array for _.object 12 | resource.scenario.rel = resource.filename.split('/')[0]; 13 | return [ 14 | resource.scenario.rel, 15 | { 16 | rel: resource.scenario.rel, 17 | href: '/' + resource.scenario.rel, 18 | method: resource.scenario.httpMethod 19 | } 20 | ]; 21 | })); 22 | }); 23 | return _.reduce(scenarioLinks, _.merge, {}); 24 | }; 25 | 26 | /** 27 | * Add response._links to all resources in a scenario. 28 | */ 29 | var scenarioWithLinks = function (links, scenario) { 30 | return _.map(scenario, function (resource) { 31 | var resourceClone = _.cloneDeep(resource); 32 | if (resourceClone.scenario.response) { 33 | if (resourceClone.scenario.relNames) { 34 | resourceClone.scenario.response._links = _.pick(links, 35 | resourceClone.scenario.relNames); 36 | } else { 37 | resourceClone.scenario.response._links = links; 38 | } 39 | } 40 | return resourceClone; 41 | }); 42 | }; 43 | 44 | /** 45 | * Generate dummy URIs for resources. 46 | */ 47 | var addHalUris = function (resource) { 48 | if (resource.scenario.rel === 'Root') { 49 | resource.scenario.uri = '/'; 50 | } else { 51 | resource.scenario.uri = '/' + resource.scenario.rel; 52 | } 53 | return resource; 54 | }; 55 | 56 | /** 57 | * Add _links to resources in all scenarios. 58 | */ 59 | var decorateWithHalLinks = function (data) { 60 | var links = generateAvailableLinks(data); 61 | return _.mapValues(data, function (scenario) { 62 | return scenarioWithLinks(links, scenario).map(function (resource) { 63 | return addHalUris(resource); 64 | }); 65 | }); 66 | }; 67 | 68 | module.exports = decorateWithHalLinks; 69 | --------------------------------------------------------------------------------