├── .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 | [](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 |
--------------------------------------------------------------------------------