├── .gitignore ├── Gruntfile.js ├── LICENSE ├── README.md ├── app └── scripts │ └── ng-http-circuitbreaker.js ├── dist ├── ng-http-circuitbreaker.js └── ng-http-circuitbreaker.min.js ├── lib └── angular-1.2.0-rc.2 │ ├── angular-loader.js │ ├── angular-loader.min.js │ ├── angular-loader.min.js.map │ ├── angular-mocks.js │ ├── angular-resource.js │ ├── angular-resource.min.js │ ├── angular-resource.min.js.map │ ├── angular-scenario.js │ ├── angular.js │ ├── angular.min.js │ ├── angular.min.js.map │ ├── errors.json │ ├── version.json │ └── version.txt ├── package.json └── test ├── karma-unit.conf.js └── unit └── ng-http-circuitbreakerSpec.js /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .agignore 3 | node_modules 4 | bower_components 5 | /selenium 6 | /coverage 7 | /app/assets 8 | .idea 9 | -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | module.exports = function(grunt) { 2 | 3 | grunt.loadNpmTasks('grunt-karma'); 4 | grunt.loadNpmTasks('grunt-contrib-jshint'); 5 | grunt.loadNpmTasks('grunt-contrib-uglify'); 6 | grunt.loadNpmTasks('grunt-contrib-copy'); 7 | grunt.initConfig({ 8 | pkg: grunt.file.readJSON('package.json'), 9 | uglify: { 10 | dist: { 11 | files: { 12 | 'dist/<%= pkg.name %>.min.js': ['app/scripts/ng-http-circuitbreaker.js'] 13 | } 14 | } 15 | }, 16 | copy: { 17 | main: { 18 | files: [ 19 | {src: ['app/scripts/ng-http-circuitbreaker.js'], dest: 'dist/<%=pkg.name %>.js' }, 20 | ] 21 | } 22 | }, 23 | jshint: { 24 | options: { 25 | smarttabs: true, 26 | curly: true, 27 | eqeqeq: true, 28 | immed: true, 29 | newcap: true, 30 | noarg: true, 31 | sub: true, 32 | eqnull: true, 33 | unused: true, 34 | browser: true, 35 | validthis: true, 36 | strict: true, 37 | latedef: true, 38 | globals: { 39 | angular: true 40 | } 41 | }, 42 | source: { 43 | src: ['app/scripts/ng-http-circuitbreaker.js'] 44 | } 45 | }, 46 | karma: { 47 | unit: { 48 | configFile: './test/karma-unit.conf.js', 49 | autoWatch: false, 50 | singleRun: true 51 | }, 52 | unit_auto: { 53 | configFile: './test/karma-unit.conf.js', 54 | autoWatch: true, 55 | singleRun: false 56 | }, 57 | unit_coverage: { 58 | configFile: './test/karma-unit.conf.js', 59 | autoWatch: false, 60 | singleRun: true, 61 | reporters: ['progress', 'coverage'], 62 | preprocessors: { 63 | 'app/scripts/*.js': ['coverage'] 64 | }, 65 | coverageReporter: { 66 | type : 'html', 67 | dir : 'coverage/' 68 | } 69 | }, 70 | } 71 | }); 72 | 73 | //single run tests 74 | grunt.registerTask('test', ['jshint','karma:unit']); 75 | //coverage testing 76 | grunt.registerTask('test:coverage', ['jshint','karma:unit_coverage']); 77 | //autotest and watch tests 78 | grunt.registerTask('autotest', ['jshint','karma:unit_auto']); 79 | 80 | 81 | grunt.registerTask('build', ['jshint','karma:unit','copy:main','uglify']); 82 | 83 | //defaults 84 | grunt.registerTask('default', ['test']); 85 | 86 | }; 87 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013 mikepugh 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ng-http-circuitbreaker 2 | ====================== 3 | 4 | ngHttpCircuitBreaker is an attempt to model the circuit-breaker pattern for angular services and $http calls. For a great explanation of the benefits of the circuit-breaker pattern, see http://techblog.netflix.com/2011/12/making-netflix-api-more-resilient.html. The ng-http-circuitbreaker 5 | is created as an $http interceptor and therefore [AngularJS v1.1.5+ is required](http://angularjs.org/). 6 | 7 | Please note that while a client-side angular circuit breaker should help reduce load on your backend servers in the event of system failures, it is not a replacement for server side circuit breakers / fail-over designs. The point of the client side circuit breaker is to lessen the demand for doomed requests against your server, hopefully giving the server time to recover. 8 | 9 | The ngHttpCircuitBreaker can support one or more endpoints, using regular expressions to define each circuit. For example, if your application calls two different sets of APIs, you can selectively choose which one(s) you want to protect with the circuit breaker. 10 | 11 | For each circuit you can specify: 12 | 13 | 1. endpoint regular expression - the regex to test the $http.config.url 14 | 2. failure limit - the number of errors you'll accept before you want the circuit breaker to trip into the OPEN state 15 | 3. response sla - the time in ms to allow the $http service to wait for a response before timing out (timeouts are an important part of the fail fast mindset) 16 | 4. half open time - the time in ms until an open circuit should transition to half-open 17 | 5. status codes to ignore - an array of HTTP failure status codes that should be ignored (such as 401 - Authorization Required) 18 | 19 | Your client application must be designed to gracefully handle error responses - once tripped the circuit breaker will reject all requests immediately until the half-open time has elapsed. At that point it will allow a single request to pass through to the backend server - while that request is waiting for a response the circuit will transition back to the OPEN state and reject all requests. If the single request that went through during the half open state responds successfully, the circuit will immediately move to the CLOSED state and allow all requests to go through again. 20 | 21 | ## Example Usage 22 | 23 | Include the ng-http-circuitbreaker dependency in your module, configure the circuits and add it to the $httpProvider's interceptor list. 24 | ```javascript 25 | var yourModule = angular.module('someApp', ['ng-http-circuitbreaker']) 26 | .config(['$httpProvider','ngHttpCircuitBreakerConfigProvider', function($httpProvider, ngHttpCircuitBreakerConfigProvider) { 27 | ngHttpCircuitBreakerConfigProvider 28 | .circuit({endPointRegEx: /^\/api\//i, failureLimit: 5, responseSLA: 500, timeUntilHalfOpen: 5000, statusCodesToIgnore: [401,403,409]}) 29 | .circuit({endPointRegEx: /^\/ext\//i, failureLimit: 8, responseSLA: 750, timeUntilHalfOpen: 5000, statusCodesToIgnore: [401]}); 30 | 31 | $httpProvider.interceptors.push('ngHttpCircuitBreaker'); 32 | }]); 33 | 34 | ``` 35 | 36 | #### Most circuit configuration parameters are optional 37 | The only required parameter to the circuit configuration is the endPointRegEx which is any valid [regular expression](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions) that matches your service route(s). The other parameters provide the following defaults: 38 | 39 | * failureLimit: 5 40 | * responseSLA: 500ms 41 | * timeUntilHalfOpen: 5000ms 42 | * statusCodesToIgnore: [401,403,409] 43 | 44 | -------------------------------------------------------------------------------- /app/scripts/ng-http-circuitbreaker.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by Mike Pugh on 10/30/13. 3 | * 4 | * ngHttpCircuitBreaker is an attempt to model the circuit-breaker pattern for angular services and $http calls. For a 5 | * great explanation of the benefits of the circuit-breaker pattern, 6 | * see http://techblog.netflix.com/2011/12/making-netflix-api-more-resilient.html. 7 | * 8 | * Please note that while a client-side angular circuit breaker should help reduce load on your backend servers in the 9 | * event of system failures, it is not a replacement for server side circuit breakers / fail-over designs. The point of 10 | * the client side circuit breaker is to lessen the demand for doomed requests against your server, hopefully giving the 11 | * server time to recover. 12 | * 13 | * The ngHttpCircuitBreaker can support one or more endpoints, using regular expressions to define each circuit. For 14 | * example, if your application calls two different sets of APIs, you can selectively choose which one(s) you want to 15 | * protect with the circuit breaker. 16 | * 17 | * For each circuit you can specify: 18 | * 1) endpoint regular expression - the regex to test the $http.config.url 19 | * 2) failure limit - the number of errors you'll accept before you want the circuit breaker to trip into the OPEN state 20 | * 3) response sla - the time in ms to allow the $http service to wait for a response before timing out (timeouts are an important part of the fail fast mindset) 21 | * 4) half open time - the time in ms until an open circuit should transition to half-open 22 | * 5) status codes to ignore - an array of HTTP failure status codes that should be ignored (such as 401 - Authorization Required) 23 | * 24 | * Your client application must be designed to gracefully handle error responses - once tripped the circuit breaker will 25 | * reject all requests immediately until the half-open time has elapsed. At that point it will allow a single request to pass through 26 | * to the backend server - while that request is waiting for a response the circuit will transition back to the OPEN state and reject all requests. 27 | * If the single request that went through during the half open state responds successfully, the circuit will immediately move to the CLOSED state 28 | * and allow all requests to go through again. 29 | * 30 | * 31 | * 32 | * The MIT License (MIT) 33 | 34 | Copyright (c) 2013 - Mike Pugh 35 | 36 | Permission is hereby granted, free of charge, to any person obtaining a copy 37 | of this software and associated documentation files (the "Software"), to deal 38 | in the Software without restriction, including without limitation the rights 39 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 40 | copies of the Software, and to permit persons to whom the Software is 41 | furnished to do so, subject to the following conditions: 42 | 43 | The above copyright notice and this permission notice shall be included in 44 | all copies or substantial portions of the Software. 45 | 46 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 47 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 48 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 49 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 50 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 51 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 52 | THE SOFTWARE. 53 | * 54 | */ 55 | 56 | (function() { 57 | "use strict"; 58 | var module = angular.module('ng-http-circuitbreaker', []); 59 | // Circuit State constants 60 | var STATE_CLOSED = 0, 61 | STATE_HALF_OPEN = 1, 62 | STATE_OPEN = 2; 63 | 64 | function ngHttpCircuitBreakerProvider() { 65 | var config = { 66 | circuits: [] 67 | }; 68 | /** 69 | * The ngHttpCircuitBreakerConfigProvider provides a circuit method which can be chained to define the HTTP routes that should be encapsulated as circuits behind the circuit breaker http transform. 70 | * 71 | * @param circuitConfig.endPointRegEx - (REQUIRED) A regular expression to match the URL of the $http request config 72 | * @param circuitConfig.failureLimit - (OPTIONAL: Default = 5) The number of error responses you'll accept before you want to OPEN the circuit. 73 | * @param circuitConfig.responseSLA - (OPTIONAL: Default = 500) The maximum response time (in ms) deemed acceptable before forcing an active request to timeout. Set to 0 if you do not want an SLA timeout injected into your circuit's calls. 74 | * @param circuitConfig.timeUntilHalfOpen - (OPTIONAL: Default = 5000) The time (in ms) an OPEN circuit should wait until moving to the HALF-OPEN state. 75 | * @param circuitConfig.statusCodesToIgnore - (OPTIONAL: Default = [401, 403, 409]) An array of HTTP Status Codes to ignore. 76 | * @returns {ngHttpCircuitBreakerProvider} 77 | */ 78 | this.circuit = function circuit(circuitConfig) { 79 | if(!circuitConfig) { 80 | throw {source: 'ngHttpCircuitBreakerConfig', message: 'Circuit Config required'}; 81 | } 82 | 83 | if(!circuitConfig.endPointRegEx) { 84 | throw {source: 'ngHttpCircuitBreakerConfig', message: 'Circuit Endpoint Regular Expression required'}; 85 | } 86 | 87 | if(!circuitConfig.endPointRegEx.test) { 88 | throw {source: 'ngHttpCircuitBreakerConfig', message: 'Circuit Parameter endPointRegEx must be a regular expression'}; 89 | } 90 | 91 | for(var i = 0; i < config.circuits.length; i++) { 92 | if(config.circuits[i].endPointRegEx.toString().toUpperCase() === circuitConfig.endPointRegEx.toString().toUpperCase()) { 93 | throw {source: 'ngHttpCircuitBreakerConfig', message: 'Duplicate endpoint regular expression found'}; 94 | } 95 | } 96 | 97 | // default to 5 98 | if(isNaN(circuitConfig.failureLimit)) { 99 | circuitConfig.failureLimit = 5; 100 | } 101 | 102 | if(circuitConfig.failureLimit <= 0) { 103 | throw {source: 'ngHttpCircuitBreakerConfig', message: 'Invalid failure limit - must be positive, non-zero value'}; 104 | } 105 | 106 | // default to 500 107 | if(isNaN(circuitConfig.responseSLA)) { 108 | circuitConfig.responseSLA = 500; 109 | } 110 | 111 | if(circuitConfig.responseSLA < 0) { 112 | throw {source: 'ngHttpCircuitBreakerConfig', message: 'Invalid Response SLA - must be non-negative. Set to 0 if you do not want a response sla to be set.'}; 113 | } 114 | 115 | // default to 5 seconds 116 | if(isNaN(circuitConfig.timeUntilHalfOpen)) { 117 | circuitConfig.timeUntilHalfOpen = 5000; 118 | } 119 | 120 | if(circuitConfig.timeUntilHalfOpen <= 0) { 121 | throw {source: 'ngHttpCircuitBreakerConfig', message: 'Invalid Circuit timeUntilHalfOpen - must be a positive non-zero integer value'}; 122 | } 123 | 124 | // Inject some default status codes to ignore if none are specified by the caller. Ignore auth & validation error codes. 125 | if(!circuitConfig.statusCodesToIgnore) { 126 | circuitConfig.statusCodesToIgnore = [401,403,409]; 127 | } 128 | 129 | if(!Array.isArray(circuitConfig.statusCodesToIgnore)) { 130 | throw {source: 'ngHttpCircuitBreakerConfig', message: 'Invalid statusCodesToIgnore - expecting array of integer values representing HTTP response codes'}; 131 | } 132 | 133 | config.circuits.push({ 134 | endPointRegEx: circuitConfig.endPointRegEx, 135 | failureLimit: circuitConfig.failureLimit, 136 | responseSLA: circuitConfig.responseSLA, 137 | timeUntilHalfOpen: circuitConfig.timeUntilHalfOpen, 138 | statusCodesToIgnore: circuitConfig.statusCodesToIgnore, 139 | STATE: STATE_CLOSED, // closed state 140 | timerSet: false, 141 | failureCount: 0 // initial failure count 142 | }); 143 | return this; 144 | }; 145 | this.$get = function $get() { 146 | return config; 147 | }; 148 | } 149 | 150 | module.provider('ngHttpCircuitBreakerConfig', ngHttpCircuitBreakerProvider); 151 | 152 | module.factory('ngHttpCircuitBreaker', ['ngHttpCircuitBreakerConfig', '$q', '$timeout', '$log', function(cktConfig, $q, $timeout, $log) { 153 | return { 154 | request: function request(config) { 155 | for (var i = 0; i < cktConfig.circuits.length; i++) { 156 | if (cktConfig.circuits[i].endPointRegEx.test(config.url)) { 157 | $log.info('Circuit Breaker protecting ' + config.url); 158 | var circuit = cktConfig.circuits[i]; 159 | // To save on lookup times on response processing, add the circuit index to the config object which will be included within the response 160 | config.cktbkr = { 161 | circuit: i 162 | }; 163 | 164 | if (circuit.STATE === STATE_CLOSED || circuit.STATE === STATE_HALF_OPEN) { 165 | 166 | // Inject the SLA timeout only if it is a number and is greater than 0 167 | if(cktConfig.circuits[i].responseSLA > 0) { 168 | config.timeout = cktConfig.circuits[i].responseSLA; 169 | } 170 | 171 | // We only want to allow one attempt, so set state back to OPEN - if this call succeeds it will set state to CLOSED 172 | if(circuit.STATE === STATE_HALF_OPEN) { 173 | $log.warn('Circuit breaker is HALF-OPEN - allowing request for ' + config.url + ' through'); 174 | circuit.STATE = STATE_OPEN; 175 | } 176 | // Don't look for more circuits 177 | break; 178 | } else if (circuit.STATE === STATE_OPEN) { 179 | // open state, reject everything 180 | // todo: Do something here to indicate the failure is circuit breaker related ?? 181 | $log.error('Circuit breaker for ' + config.url + ' is OPEN - short circuiting request'); 182 | return $q.reject(config); 183 | } 184 | } 185 | } 186 | return config || $q.when(config); 187 | }, 188 | response: function (response) { 189 | if(response.config.cktbkr) { 190 | var circuit = cktConfig.circuits[response.config.cktbkr.circuit]; 191 | if (circuit.STATE === STATE_OPEN) { 192 | $log.info('Circuit breaker detected successful call to ' + response.config.url + ' - closing circuit'); 193 | // circuit is currently open, which means this is the one request that made it through during the half-open transition 194 | // since it is successful, set the state to CLOSED and reset failure count 195 | circuit.STATE = STATE_CLOSED; 196 | circuit.failureCount = 0; 197 | } else if(circuit.failureCount > 0) { 198 | // only decrement the failure count if it's greater than zero 199 | circuit.failureCount -= 1; 200 | } 201 | } 202 | return response || $q.when(response); 203 | }, 204 | responseError: function responseError(response) { 205 | //console.log(response); 206 | // Only process responses where the config has been extended with the cktbkr object 207 | if (response.config && response.config.cktbkr) { 208 | var circuit = cktConfig.circuits[response.config.cktbkr.circuit]; 209 | // determine if the status code should be ignored 210 | if(circuit.statusCodesToIgnore.indexOf(response.status) !== -1) { 211 | $log.info('Circuit breaker ignoring status code ' + response.status + ' for circuit ' + response.config.url); 212 | return $q.reject(response); 213 | } 214 | 215 | // The status code is in the failure range (4xx, 5xx) and isn't an ignored status code 216 | // so increment the failure count and check against the failure limit 217 | circuit.failureCount += 1; 218 | if (circuit.failureCount >= circuit.failureLimit) { 219 | $log.error('Circuit breaker limit reached for circuit ' + circuit.endPointRegEx.toString() + ' with failed request to ' + response.config.url + ' with code ' + response.status); 220 | // Breached failure limit, set the circuit to OPEN state 221 | circuit.STATE = STATE_OPEN; 222 | // Create a timeout that will set the circuit to half open based upon the half open time specified by the circuit 223 | // we only want to have one timer active per circuit though so check timerSet flag 224 | if(!circuit.timerSet) { 225 | $timeout(function () { 226 | $log.warn('Circuit breaker switching to HALF-OPEN status for circuit ' + circuit.endPointRegEx.toString()); 227 | circuit.STATE = STATE_HALF_OPEN; 228 | circuit.timerSet = false; // Allow a new timer to be set if needed 229 | }, circuit.timeUntilHalfOpen); 230 | } 231 | circuit.timerSet = true; 232 | } 233 | } 234 | // return the response back to the client for handling 235 | return $q.reject(response); 236 | } 237 | }; 238 | }]); 239 | }).call(this); -------------------------------------------------------------------------------- /dist/ng-http-circuitbreaker.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by Mike Pugh on 10/30/13. 3 | * 4 | * ngHttpCircuitBreaker is an attempt to model the circuit-breaker pattern for angular services and $http calls. For a 5 | * great explanation of the benefits of the circuit-breaker pattern, 6 | * see http://techblog.netflix.com/2011/12/making-netflix-api-more-resilient.html. 7 | * 8 | * Please note that while a client-side angular circuit breaker should help reduce load on your backend servers in the 9 | * event of system failures, it is not a replacement for server side circuit breakers / fail-over designs. The point of 10 | * the client side circuit breaker is to lessen the demand for doomed requests against your server, hopefully giving the 11 | * server time to recover. 12 | * 13 | * The ngHttpCircuitBreaker can support one or more endpoints, using regular expressions to define each circuit. For 14 | * example, if your application calls two different sets of APIs, you can selectively choose which one(s) you want to 15 | * protect with the circuit breaker. 16 | * 17 | * For each circuit you can specify: 18 | * 1) endpoint regular expression - the regex to test the $http.config.url 19 | * 2) failure limit - the number of errors you'll accept before you want the circuit breaker to trip into the OPEN state 20 | * 3) response sla - the time in ms to allow the $http service to wait for a response before timing out (timeouts are an important part of the fail fast mindset) 21 | * 4) half open time - the time in ms until an open circuit should transition to half-open 22 | * 5) status codes to ignore - an array of HTTP failure status codes that should be ignored (such as 401 - Authorization Required) 23 | * 24 | * Your client application must be designed to gracefully handle error responses - once tripped the circuit breaker will 25 | * reject all requests immediately until the half-open time has elapsed. At that point it will allow a single request to pass through 26 | * to the backend server - while that request is waiting for a response the circuit will transition back to the OPEN state and reject all requests. 27 | * If the single request that went through during the half open state responds successfully, the circuit will immediately move to the CLOSED state 28 | * and allow all requests to go through again. 29 | * 30 | * 31 | * 32 | * The MIT License (MIT) 33 | 34 | Copyright (c) 2013 - Mike Pugh 35 | 36 | Permission is hereby granted, free of charge, to any person obtaining a copy 37 | of this software and associated documentation files (the "Software"), to deal 38 | in the Software without restriction, including without limitation the rights 39 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 40 | copies of the Software, and to permit persons to whom the Software is 41 | furnished to do so, subject to the following conditions: 42 | 43 | The above copyright notice and this permission notice shall be included in 44 | all copies or substantial portions of the Software. 45 | 46 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 47 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 48 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 49 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 50 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 51 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 52 | THE SOFTWARE. 53 | * 54 | */ 55 | 56 | (function() { 57 | "use strict"; 58 | var module = angular.module('ng-http-circuitbreaker', []); 59 | // Circuit State constants 60 | var STATE_CLOSED = 0, 61 | STATE_HALF_OPEN = 1, 62 | STATE_OPEN = 2; 63 | 64 | function ngHttpCircuitBreakerProvider() { 65 | var config = { 66 | circuits: [] 67 | }; 68 | /** 69 | * The ngHttpCircuitBreakerConfigProvider provides a circuit method which can be chained to define the HTTP routes that should be encapsulated as circuits behind the circuit breaker http transform. 70 | * 71 | * @param circuitConfig.endPointRegEx - (REQUIRED) A regular expression to match the URL of the $http request config 72 | * @param circuitConfig.failureLimit - (OPTIONAL: Default = 5) The number of error responses you'll accept before you want to OPEN the circuit. 73 | * @param circuitConfig.responseSLA - (OPTIONAL: Default = 500) The maximum response time (in ms) deemed acceptable before forcing an active request to timeout. Set to 0 if you do not want an SLA timeout injected into your circuit's calls. 74 | * @param circuitConfig.timeUntilHalfOpen - (OPTIONAL: Default = 5000) The time (in ms) an OPEN circuit should wait until moving to the HALF-OPEN state. 75 | * @param circuitConfig.statusCodesToIgnore - (OPTIONAL: Default = [401, 403, 409]) An array of HTTP Status Codes to ignore. 76 | * @returns {ngHttpCircuitBreakerProvider} 77 | */ 78 | this.circuit = function circuit(circuitConfig) { 79 | if(!circuitConfig) { 80 | throw {source: 'ngHttpCircuitBreakerConfig', message: 'Circuit Config required'}; 81 | } 82 | 83 | if(!circuitConfig.endPointRegEx) { 84 | throw {source: 'ngHttpCircuitBreakerConfig', message: 'Circuit Endpoint Regular Expression required'}; 85 | } 86 | 87 | if(!circuitConfig.endPointRegEx.test) { 88 | throw {source: 'ngHttpCircuitBreakerConfig', message: 'Circuit Parameter endPointRegEx must be a regular expression'}; 89 | } 90 | 91 | for(var i = 0; i < config.circuits.length; i++) { 92 | if(config.circuits[i].endPointRegEx.toString().toUpperCase() === circuitConfig.endPointRegEx.toString().toUpperCase()) { 93 | throw {source: 'ngHttpCircuitBreakerConfig', message: 'Duplicate endpoint regular expression found'}; 94 | } 95 | } 96 | 97 | // default to 5 98 | if(isNaN(circuitConfig.failureLimit)) { 99 | circuitConfig.failureLimit = 5; 100 | } 101 | 102 | if(circuitConfig.failureLimit <= 0) { 103 | throw {source: 'ngHttpCircuitBreakerConfig', message: 'Invalid failure limit - must be positive, non-zero value'}; 104 | } 105 | 106 | // default to 500 107 | if(isNaN(circuitConfig.responseSLA)) { 108 | circuitConfig.responseSLA = 500; 109 | } 110 | 111 | if(circuitConfig.responseSLA < 0) { 112 | throw {source: 'ngHttpCircuitBreakerConfig', message: 'Invalid Response SLA - must be non-negative. Set to 0 if you do not want a response sla to be set.'}; 113 | } 114 | 115 | // default to 5 seconds 116 | if(isNaN(circuitConfig.timeUntilHalfOpen)) { 117 | circuitConfig.timeUntilHalfOpen = 5000; 118 | } 119 | 120 | if(circuitConfig.timeUntilHalfOpen <= 0) { 121 | throw {source: 'ngHttpCircuitBreakerConfig', message: 'Invalid Circuit timeUntilHalfOpen - must be a positive non-zero integer value'}; 122 | } 123 | 124 | // Inject some default status codes to ignore if none are specified by the caller. Ignore auth & validation error codes. 125 | if(!circuitConfig.statusCodesToIgnore) { 126 | circuitConfig.statusCodesToIgnore = [401,403,409]; 127 | } 128 | 129 | if(!Array.isArray(circuitConfig.statusCodesToIgnore)) { 130 | throw {source: 'ngHttpCircuitBreakerConfig', message: 'Invalid statusCodesToIgnore - expecting array of integer values representing HTTP response codes'}; 131 | } 132 | 133 | config.circuits.push({ 134 | endPointRegEx: circuitConfig.endPointRegEx, 135 | failureLimit: circuitConfig.failureLimit, 136 | responseSLA: circuitConfig.responseSLA, 137 | timeUntilHalfOpen: circuitConfig.timeUntilHalfOpen, 138 | statusCodesToIgnore: circuitConfig.statusCodesToIgnore, 139 | STATE: STATE_CLOSED, // closed state 140 | timerSet: false, 141 | failureCount: 0 // initial failure count 142 | }); 143 | return this; 144 | }; 145 | this.$get = function $get() { 146 | return config; 147 | }; 148 | } 149 | 150 | module.provider('ngHttpCircuitBreakerConfig', ngHttpCircuitBreakerProvider); 151 | 152 | module.factory('ngHttpCircuitBreaker', ['ngHttpCircuitBreakerConfig', '$q', '$timeout', '$log', function(cktConfig, $q, $timeout, $log) { 153 | return { 154 | request: function request(config) { 155 | for (var i = 0; i < cktConfig.circuits.length; i++) { 156 | if (cktConfig.circuits[i].endPointRegEx.test(config.url)) { 157 | $log.info('Circuit Breaker protecting ' + config.url); 158 | var circuit = cktConfig.circuits[i]; 159 | // To save on lookup times on response processing, add the circuit index to the config object which will be included within the response 160 | config.cktbkr = { 161 | circuit: i 162 | }; 163 | 164 | if (circuit.STATE === STATE_CLOSED || circuit.STATE === STATE_HALF_OPEN) { 165 | 166 | // Inject the SLA timeout only if it is a number and is greater than 0 167 | if(cktConfig.circuits[i].responseSLA > 0) { 168 | config.timeout = cktConfig.circuits[i].responseSLA; 169 | } 170 | 171 | // We only want to allow one attempt, so set state back to OPEN - if this call succeeds it will set state to CLOSED 172 | if(circuit.STATE === STATE_HALF_OPEN) { 173 | $log.warn('Circuit breaker is HALF-OPEN - allowing request for ' + config.url + ' through'); 174 | circuit.STATE = STATE_OPEN; 175 | } 176 | // Don't look for more circuits 177 | break; 178 | } else if (circuit.STATE === STATE_OPEN) { 179 | // open state, reject everything 180 | // todo: Do something here to indicate the failure is circuit breaker related ?? 181 | $log.error('Circuit breaker for ' + config.url + ' is OPEN - short circuiting request'); 182 | return $q.reject(config); 183 | } 184 | } 185 | } 186 | return config || $q.when(config); 187 | }, 188 | response: function (response) { 189 | if(response.config.cktbkr) { 190 | var circuit = cktConfig.circuits[response.config.cktbkr.circuit]; 191 | if (circuit.STATE === STATE_OPEN) { 192 | $log.info('Circuit breaker detected successful call to ' + response.config.url + ' - closing circuit'); 193 | // circuit is currently open, which means this is the one request that made it through during the half-open transition 194 | // since it is successful, set the state to CLOSED and reset failure count 195 | circuit.STATE = STATE_CLOSED; 196 | circuit.failureCount = 0; 197 | } else if(circuit.failureCount > 0) { 198 | // only decrement the failure count if it's greater than zero 199 | circuit.failureCount -= 1; 200 | } 201 | } 202 | return response || $q.when(response); 203 | }, 204 | responseError: function responseError(response) { 205 | //console.log(response); 206 | // Only process responses where the config has been extended with the cktbkr object 207 | if (response.config && response.config.cktbkr) { 208 | var circuit = cktConfig.circuits[response.config.cktbkr.circuit]; 209 | // determine if the status code should be ignored 210 | if(circuit.statusCodesToIgnore.indexOf(response.status) !== -1) { 211 | $log.info('Circuit breaker ignoring status code ' + response.status + ' for circuit ' + response.config.url); 212 | return $q.reject(response); 213 | } 214 | 215 | // The status code is in the failure range (4xx, 5xx) and isn't an ignored status code 216 | // so increment the failure count and check against the failure limit 217 | circuit.failureCount += 1; 218 | if (circuit.failureCount >= circuit.failureLimit) { 219 | $log.error('Circuit breaker limit reached for circuit ' + circuit.endPointRegEx.toString() + ' with failed request to ' + response.config.url + ' with code ' + response.status); 220 | // Breached failure limit, set the circuit to OPEN state 221 | circuit.STATE = STATE_OPEN; 222 | // Create a timeout that will set the circuit to half open based upon the half open time specified by the circuit 223 | // we only want to have one timer active per circuit though so check timerSet flag 224 | if(!circuit.timerSet) { 225 | $timeout(function () { 226 | $log.warn('Circuit breaker switching to HALF-OPEN status for circuit ' + circuit.endPointRegEx.toString()); 227 | circuit.STATE = STATE_HALF_OPEN; 228 | circuit.timerSet = false; // Allow a new timer to be set if needed 229 | }, circuit.timeUntilHalfOpen); 230 | } 231 | circuit.timerSet = true; 232 | } 233 | } 234 | // return the response back to the client for handling 235 | return $q.reject(response); 236 | } 237 | }; 238 | }]); 239 | }).call(this); -------------------------------------------------------------------------------- /dist/ng-http-circuitbreaker.min.js: -------------------------------------------------------------------------------- 1 | (function(){"use strict";function a(){var a={circuits:[]};this.circuit=function(b){if(!b)throw{source:"ngHttpCircuitBreakerConfig",message:"Circuit Config required"};if(!b.endPointRegEx)throw{source:"ngHttpCircuitBreakerConfig",message:"Circuit Endpoint Regular Expression required"};if(!b.endPointRegEx.test)throw{source:"ngHttpCircuitBreakerConfig",message:"Circuit Parameter endPointRegEx must be a regular expression"};for(var d=0;d0&&(f.timeout=a.circuits[h].responseSLA),i.STATE===d&&(g.warn("Circuit breaker is HALF-OPEN - allowing request for "+f.url+" through"),i.STATE=e);break}if(i.STATE===e)return g.error("Circuit breaker for "+f.url+" is OPEN - short circuiting request"),b.reject(f)}return f||b.when(f)},response:function(d){if(d.config.cktbkr){var f=a.circuits[d.config.cktbkr.circuit];f.STATE===e?(g.info("Circuit breaker detected successful call to "+d.config.url+" - closing circuit"),f.STATE=c,f.failureCount=0):f.failureCount>0&&(f.failureCount-=1)}return d||b.when(d)},responseError:function(c){if(c.config&&c.config.cktbkr){var h=a.circuits[c.config.cktbkr.circuit];if(-1!==h.statusCodesToIgnore.indexOf(c.status))return g.info("Circuit breaker ignoring status code "+c.status+" for circuit "+c.config.url),b.reject(c);h.failureCount+=1,h.failureCount>=h.failureLimit&&(g.error("Circuit breaker limit reached for circuit "+h.endPointRegEx.toString()+" with failed request to "+c.config.url+" with code "+c.status),h.STATE=e,h.timerSet||f(function(){g.warn("Circuit breaker switching to HALF-OPEN status for circuit "+h.endPointRegEx.toString()),h.STATE=d,h.timerSet=!1},h.timeUntilHalfOpen),h.timerSet=!0)}return b.reject(c)}}}])}).call(this); -------------------------------------------------------------------------------- /lib/angular-1.2.0-rc.2/angular-loader.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license AngularJS v1.2.0-rc.2 3 | * (c) 2010-2012 Google, Inc. http://angularjs.org 4 | * License: MIT 5 | */ 6 | 7 | ( 8 | 9 | /** 10 | * @ngdoc interface 11 | * @name angular.Module 12 | * @description 13 | * 14 | * Interface for configuring angular {@link angular.module modules}. 15 | */ 16 | 17 | function setupModuleLoader(window) { 18 | 19 | function ensure(obj, name, factory) { 20 | return obj[name] || (obj[name] = factory()); 21 | } 22 | 23 | return ensure(ensure(window, 'angular', Object), 'module', function() { 24 | /** @type {Object.} */ 25 | var modules = {}; 26 | 27 | /** 28 | * @ngdoc function 29 | * @name angular.module 30 | * @description 31 | * 32 | * The `angular.module` is a global place for creating, registering and retrieving Angular modules. 33 | * All modules (angular core or 3rd party) that should be available to an application must be 34 | * registered using this mechanism. 35 | * 36 | * When passed two or more arguments, a new module is created. If passed only one argument, an 37 | * existing module (the name passed as the first argument to `module`) is retrieved. 38 | * 39 | * 40 | * # Module 41 | * 42 | * A module is a collection of services, directives, filters, and configuration information. 43 | * `angular.module` is used to configure the {@link AUTO.$injector $injector}. 44 | * 45 | *
 46 |      * // Create a new module
 47 |      * var myModule = angular.module('myModule', []);
 48 |      *
 49 |      * // register a new service
 50 |      * myModule.value('appName', 'MyCoolApp');
 51 |      *
 52 |      * // configure existing services inside initialization blocks.
 53 |      * myModule.config(function($locationProvider) {'use strict';
 54 |      *   // Configure existing providers
 55 |      *   $locationProvider.hashPrefix('!');
 56 |      * });
 57 |      * 
58 | * 59 | * Then you can create an injector and load your modules like this: 60 | * 61 | *
 62 |      * var injector = angular.injector(['ng', 'MyModule'])
 63 |      * 
64 | * 65 | * However it's more likely that you'll just use 66 | * {@link ng.directive:ngApp ngApp} or 67 | * {@link angular.bootstrap} to simplify this process for you. 68 | * 69 | * @param {!string} name The name of the module to create or retrieve. 70 | * @param {Array.=} requires If specified then new module is being created. If unspecified then the 71 | * the module is being retrieved for further configuration. 72 | * @param {Function} configFn Optional configuration function for the module. Same as 73 | * {@link angular.Module#config Module#config()}. 74 | * @returns {module} new module with the {@link angular.Module} api. 75 | */ 76 | return function module(name, requires, configFn) { 77 | if (requires && modules.hasOwnProperty(name)) { 78 | modules[name] = null; 79 | } 80 | return ensure(modules, name, function() { 81 | if (!requires) { 82 | throw minErr('$injector')('nomod', "Module '{0}' is not available! You either misspelled the module name " + 83 | "or forgot to load it. If registering a module ensure that you specify the dependencies as the second " + 84 | "argument.", name); 85 | } 86 | 87 | /** @type {!Array.>} */ 88 | var invokeQueue = []; 89 | 90 | /** @type {!Array.} */ 91 | var runBlocks = []; 92 | 93 | var config = invokeLater('$injector', 'invoke'); 94 | 95 | /** @type {angular.Module} */ 96 | var moduleInstance = { 97 | // Private state 98 | _invokeQueue: invokeQueue, 99 | _runBlocks: runBlocks, 100 | 101 | /** 102 | * @ngdoc property 103 | * @name angular.Module#requires 104 | * @propertyOf angular.Module 105 | * @returns {Array.} List of module names which must be loaded before this module. 106 | * @description 107 | * Holds the list of modules which the injector will load before the current module is loaded. 108 | */ 109 | requires: requires, 110 | 111 | /** 112 | * @ngdoc property 113 | * @name angular.Module#name 114 | * @propertyOf angular.Module 115 | * @returns {string} Name of the module. 116 | * @description 117 | */ 118 | name: name, 119 | 120 | 121 | /** 122 | * @ngdoc method 123 | * @name angular.Module#provider 124 | * @methodOf angular.Module 125 | * @param {string} name service name 126 | * @param {Function} providerType Construction function for creating new instance of the service. 127 | * @description 128 | * See {@link AUTO.$provide#provider $provide.provider()}. 129 | */ 130 | provider: invokeLater('$provide', 'provider'), 131 | 132 | /** 133 | * @ngdoc method 134 | * @name angular.Module#factory 135 | * @methodOf angular.Module 136 | * @param {string} name service name 137 | * @param {Function} providerFunction Function for creating new instance of the service. 138 | * @description 139 | * See {@link AUTO.$provide#factory $provide.factory()}. 140 | */ 141 | factory: invokeLater('$provide', 'factory'), 142 | 143 | /** 144 | * @ngdoc method 145 | * @name angular.Module#service 146 | * @methodOf angular.Module 147 | * @param {string} name service name 148 | * @param {Function} constructor A constructor function that will be instantiated. 149 | * @description 150 | * See {@link AUTO.$provide#service $provide.service()}. 151 | */ 152 | service: invokeLater('$provide', 'service'), 153 | 154 | /** 155 | * @ngdoc method 156 | * @name angular.Module#value 157 | * @methodOf angular.Module 158 | * @param {string} name service name 159 | * @param {*} object Service instance object. 160 | * @description 161 | * See {@link AUTO.$provide#value $provide.value()}. 162 | */ 163 | value: invokeLater('$provide', 'value'), 164 | 165 | /** 166 | * @ngdoc method 167 | * @name angular.Module#constant 168 | * @methodOf angular.Module 169 | * @param {string} name constant name 170 | * @param {*} object Constant value. 171 | * @description 172 | * Because the constant are fixed, they get applied before other provide methods. 173 | * See {@link AUTO.$provide#constant $provide.constant()}. 174 | */ 175 | constant: invokeLater('$provide', 'constant', 'unshift'), 176 | 177 | /** 178 | * @ngdoc method 179 | * @name angular.Module#animation 180 | * @methodOf angular.Module 181 | * @param {string} name animation name 182 | * @param {Function} animationFactory Factory function for creating new instance of an animation. 183 | * @description 184 | * 185 | * **NOTE**: animations are take effect only if the **ngAnimate** module is loaded. 186 | * 187 | * 188 | * Defines an animation hook that can be later used with {@link ngAnimate.$animate $animate} service and 189 | * directives that use this service. 190 | * 191 | *
192 |            * module.animation('.animation-name', function($inject1, $inject2) {
193 |            *   return {
194 |            *     eventName : function(element, done) {
195 |            *       //code to run the animation
196 |            *       //once complete, then run done()
197 |            *       return function cancellationFunction(element) {
198 |            *         //code to cancel the animation
199 |            *       }
200 |            *     }
201 |            *   }
202 |            * })
203 |            * 
204 | * 205 | * See {@link ngAnimate.$animateProvider#register $animateProvider.register()} and 206 | * {@link ngAnimate ngAnimate module} for more information. 207 | */ 208 | animation: invokeLater('$animateProvider', 'register'), 209 | 210 | /** 211 | * @ngdoc method 212 | * @name angular.Module#filter 213 | * @methodOf angular.Module 214 | * @param {string} name Filter name. 215 | * @param {Function} filterFactory Factory function for creating new instance of filter. 216 | * @description 217 | * See {@link ng.$filterProvider#register $filterProvider.register()}. 218 | */ 219 | filter: invokeLater('$filterProvider', 'register'), 220 | 221 | /** 222 | * @ngdoc method 223 | * @name angular.Module#controller 224 | * @methodOf angular.Module 225 | * @param {string} name Controller name. 226 | * @param {Function} constructor Controller constructor function. 227 | * @description 228 | * See {@link ng.$controllerProvider#register $controllerProvider.register()}. 229 | */ 230 | controller: invokeLater('$controllerProvider', 'register'), 231 | 232 | /** 233 | * @ngdoc method 234 | * @name angular.Module#directive 235 | * @methodOf angular.Module 236 | * @param {string} name directive name 237 | * @param {Function} directiveFactory Factory function for creating new instance of 238 | * directives. 239 | * @description 240 | * See {@link ng.$compileProvider#directive $compileProvider.directive()}. 241 | */ 242 | directive: invokeLater('$compileProvider', 'directive'), 243 | 244 | /** 245 | * @ngdoc method 246 | * @name angular.Module#config 247 | * @methodOf angular.Module 248 | * @param {Function} configFn Execute this function on module load. Useful for service 249 | * configuration. 250 | * @description 251 | * Use this method to register work which needs to be performed on module loading. 252 | */ 253 | config: config, 254 | 255 | /** 256 | * @ngdoc method 257 | * @name angular.Module#run 258 | * @methodOf angular.Module 259 | * @param {Function} initializationFn Execute this function after injector creation. 260 | * Useful for application initialization. 261 | * @description 262 | * Use this method to register work which should be performed when the injector is done 263 | * loading all modules. 264 | */ 265 | run: function(block) { 266 | runBlocks.push(block); 267 | return this; 268 | } 269 | }; 270 | 271 | if (configFn) { 272 | config(configFn); 273 | } 274 | 275 | return moduleInstance; 276 | 277 | /** 278 | * @param {string} provider 279 | * @param {string} method 280 | * @param {String=} insertMethod 281 | * @returns {angular.Module} 282 | */ 283 | function invokeLater(provider, method, insertMethod) { 284 | return function() { 285 | invokeQueue[insertMethod || 'push']([provider, method, arguments]); 286 | return moduleInstance; 287 | } 288 | } 289 | }); 290 | }; 291 | }); 292 | 293 | } 294 | 295 | )(window); 296 | 297 | /** 298 | * Closure compiler type information 299 | * 300 | * @typedef { { 301 | * requires: !Array., 302 | * invokeQueue: !Array.>, 303 | * 304 | * service: function(string, Function):angular.Module, 305 | * factory: function(string, Function):angular.Module, 306 | * value: function(string, *):angular.Module, 307 | * 308 | * filter: function(string, Function):angular.Module, 309 | * 310 | * init: function(Function):angular.Module 311 | * } } 312 | */ 313 | angular.Module; 314 | 315 | -------------------------------------------------------------------------------- /lib/angular-1.2.0-rc.2/angular-loader.min.js: -------------------------------------------------------------------------------- 1 | /* 2 | AngularJS v1.2.0-rc.2 3 | (c) 2010-2012 Google, Inc. http://angularjs.org 4 | License: MIT 5 | */ 6 | (function(k){'use strict';function d(c,b,e){return c[b]||(c[b]=e())}return d(d(k,"angular",Object),"module",function(){var c={};return function(b,e,f){e&&c.hasOwnProperty(b)&&(c[b]=null);return d(c,b,function(){function a(a,b,d){return function(){c[d||"push"]([a,b,arguments]);return g}}if(!e)throw minErr("$injector")("nomod",b);var c=[],d=[],h=a("$injector","invoke"),g={_invokeQueue:c,_runBlocks:d,requires:e,name:b,provider:a("$provide","provider"),factory:a("$provide","factory"),service:a("$provide", 7 | "service"),value:a("$provide","value"),constant:a("$provide","constant","unshift"),animation:a("$animateProvider","register"),filter:a("$filterProvider","register"),controller:a("$controllerProvider","register"),directive:a("$compileProvider","directive"),config:h,run:function(a){d.push(a);return this}};f&&h(f);return g})}})})(window); 8 | /* 9 | //@ sourceMappingURL=angular-loader.min.js.map 10 | */ 11 | -------------------------------------------------------------------------------- /lib/angular-1.2.0-rc.2/angular-loader.min.js.map: -------------------------------------------------------------------------------- 1 | { 2 | "version":3, 3 | "file":"angular-loader.min.js", 4 | "lineCount":7, 5 | "mappings":"A;;;;;aAgBAA,SAA0B,CAACC,CAAD,CAAS,CAEjCC,QAASA,EAAM,CAACC,CAAD,CAAMC,CAAN,CAAYC,CAAZ,CAAqB,CAClC,MAAOF,EAAA,CAAIC,CAAJ,CAAP,GAAqBD,CAAA,CAAIC,CAAJ,CAArB,CAAiCC,CAAA,EAAjC,CADkC,CAIpC,MAAOH,EAAA,CAAOA,CAAA,CAAOD,CAAP,CAAe,SAAf,CAA0BK,MAA1B,CAAP,CAA0C,QAA1C,CAAoD,QAAQ,EAAG,CAEpE,IAAIC,EAAU,EAmDd,OAAOC,SAAe,CAACJ,CAAD,CAAOK,CAAP,CAAiBC,CAAjB,CAA2B,CAC3CD,CAAJ,EAAgBF,CAAAI,eAAA,CAAuBP,CAAvB,CAAhB,GACEG,CAAA,CAAQH,CAAR,CADF,CACkB,IADlB,CAGA,OAAOF,EAAA,CAAOK,CAAP,CAAgBH,CAAhB,CAAsB,QAAQ,EAAG,CA2MtCQ,QAASA,EAAW,CAACC,CAAD,CAAWC,CAAX,CAAmBC,CAAnB,CAAiC,CACnD,MAAO,SAAQ,EAAG,CAChBC,CAAA,CAAYD,CAAZ,EAA4B,MAA5B,CAAA,CAAoC,CAACF,CAAD,CAAWC,CAAX,CAAmBG,SAAnB,CAApC,CACA,OAAOC,EAFS,CADiC,CA1MrD,GAAI,CAACT,CAAL,CACE,KAAMU,OAAA,CAAO,WAAP,CAAA,CAAoB,OAApB,CAEWf,CAFX,CAAN,CAMF,IAAIY,EAAc,EAAlB,CAGII,EAAY,EAHhB,CAKIC,EAAST,CAAA,CAAY,WAAZ,CAAyB,QAAzB,CALb,CAQIM,EAAiB,cAELF,CAFK,YAGPI,CAHO,UAaTX,CAbS,MAsBbL,CAtBa,UAkCTQ,CAAA,CAAY,UAAZ,CAAwB,UAAxB,CAlCS,SA6CVA,CAAA,CAAY,UAAZ,CAAwB,SAAxB,CA7CU,SAwDVA,CAAA,CAAY,UAAZ;AAAwB,SAAxB,CAxDU,OAmEZA,CAAA,CAAY,UAAZ,CAAwB,OAAxB,CAnEY,UA+ETA,CAAA,CAAY,UAAZ,CAAwB,UAAxB,CAAoC,SAApC,CA/ES,WAgHRA,CAAA,CAAY,kBAAZ,CAAgC,UAAhC,CAhHQ,QA2HXA,CAAA,CAAY,iBAAZ,CAA+B,UAA/B,CA3HW,YAsIPA,CAAA,CAAY,qBAAZ,CAAmC,UAAnC,CAtIO,WAkJRA,CAAA,CAAY,kBAAZ,CAAgC,WAAhC,CAlJQ,QA6JXS,CA7JW,KAyKdC,QAAQ,CAACC,CAAD,CAAQ,CACnBH,CAAAI,KAAA,CAAeD,CAAf,CACA,OAAO,KAFY,CAzKF,CA+KjBb,EAAJ,EACEW,CAAA,CAAOX,CAAP,CAGF,OAAQQ,EAnM8B,CAAjC,CAJwC,CArDmB,CAA/D,CAN0B,CAAnClB,CAAA,CAsREC,MAtRF;", 6 | "sources":["angular-loader.js"], 7 | "names":["setupModuleLoader","window","ensure","obj","name","factory","Object","modules","module","requires","configFn","hasOwnProperty","invokeLater","provider","method","insertMethod","invokeQueue","arguments","moduleInstance","minErr","runBlocks","config","run","block","push"] 8 | } 9 | -------------------------------------------------------------------------------- /lib/angular-1.2.0-rc.2/angular-mocks.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license AngularJS v1.2.0-rc.2 3 | * (c) 2010-2012 Google, Inc. http://angularjs.org 4 | * License: MIT 5 | * 6 | * TODO(vojta): wrap whole file into closure during build 7 | */ 8 | 9 | /** 10 | * @ngdoc overview 11 | * @name angular.mock 12 | * @description 13 | * 14 | * Namespace from 'angular-mocks.js' which contains testing related code. 15 | */ 16 | angular.mock = {}; 17 | 18 | /** 19 | * ! This is a private undocumented service ! 20 | * 21 | * @name ngMock.$browser 22 | * 23 | * @description 24 | * This service is a mock implementation of {@link ng.$browser}. It provides fake 25 | * implementation for commonly used browser apis that are hard to test, e.g. setTimeout, xhr, 26 | * cookies, etc... 27 | * 28 | * The api of this service is the same as that of the real {@link ng.$browser $browser}, except 29 | * that there are several helper methods available which can be used in tests. 30 | */ 31 | angular.mock.$BrowserProvider = function() { 32 | this.$get = function() { 33 | return new angular.mock.$Browser(); 34 | }; 35 | }; 36 | 37 | angular.mock.$Browser = function() { 38 | var self = this; 39 | 40 | this.isMock = true; 41 | self.$$url = "http://server/"; 42 | self.$$lastUrl = self.$$url; // used by url polling fn 43 | self.pollFns = []; 44 | 45 | // TODO(vojta): remove this temporary api 46 | self.$$completeOutstandingRequest = angular.noop; 47 | self.$$incOutstandingRequestCount = angular.noop; 48 | 49 | 50 | // register url polling fn 51 | 52 | self.onUrlChange = function(listener) { 53 | self.pollFns.push( 54 | function() { 55 | if (self.$$lastUrl != self.$$url) { 56 | self.$$lastUrl = self.$$url; 57 | listener(self.$$url); 58 | } 59 | } 60 | ); 61 | 62 | return listener; 63 | }; 64 | 65 | self.cookieHash = {}; 66 | self.lastCookieHash = {}; 67 | self.deferredFns = []; 68 | self.deferredNextId = 0; 69 | 70 | self.defer = function(fn, delay) { 71 | delay = delay || 0; 72 | self.deferredFns.push({time:(self.defer.now + delay), fn:fn, id: self.deferredNextId}); 73 | self.deferredFns.sort(function(a,b){ return a.time - b.time;}); 74 | return self.deferredNextId++; 75 | }; 76 | 77 | 78 | self.defer.now = 0; 79 | 80 | 81 | self.defer.cancel = function(deferId) { 82 | var fnIndex; 83 | 84 | angular.forEach(self.deferredFns, function(fn, index) { 85 | if (fn.id === deferId) fnIndex = index; 86 | }); 87 | 88 | if (fnIndex !== undefined) { 89 | self.deferredFns.splice(fnIndex, 1); 90 | return true; 91 | } 92 | 93 | return false; 94 | }; 95 | 96 | 97 | /** 98 | * @name ngMock.$browser#defer.flush 99 | * @methodOf ngMock.$browser 100 | * 101 | * @description 102 | * Flushes all pending requests and executes the defer callbacks. 103 | * 104 | * @param {number=} number of milliseconds to flush. See {@link #defer.now} 105 | */ 106 | self.defer.flush = function(delay) { 107 | if (angular.isDefined(delay)) { 108 | self.defer.now += delay; 109 | } else { 110 | if (self.deferredFns.length) { 111 | self.defer.now = self.deferredFns[self.deferredFns.length-1].time; 112 | } else { 113 | throw Error('No deferred tasks to be flushed'); 114 | } 115 | } 116 | 117 | while (self.deferredFns.length && self.deferredFns[0].time <= self.defer.now) { 118 | self.deferredFns.shift().fn(); 119 | } 120 | }; 121 | 122 | /** 123 | * @name ngMock.$browser#defer.flushNext 124 | * @methodOf ngMock.$browser 125 | * 126 | * @description 127 | * Flushes next pending request and compares it to the provided delay 128 | * 129 | * @param {number=} expectedDelay the delay value that will be asserted against the delay of the next timeout function 130 | */ 131 | self.defer.flushNext = function(expectedDelay) { 132 | var tick = self.deferredFns.shift(); 133 | expect(tick.time).toEqual(expectedDelay); 134 | tick.fn(); 135 | }; 136 | 137 | /** 138 | * @name ngMock.$browser#defer.now 139 | * @propertyOf ngMock.$browser 140 | * 141 | * @description 142 | * Current milliseconds mock time. 143 | */ 144 | 145 | self.$$baseHref = ''; 146 | self.baseHref = function() { 147 | return this.$$baseHref; 148 | }; 149 | }; 150 | angular.mock.$Browser.prototype = { 151 | 152 | /** 153 | * @name ngMock.$browser#poll 154 | * @methodOf ngMock.$browser 155 | * 156 | * @description 157 | * run all fns in pollFns 158 | */ 159 | poll: function poll() { 160 | angular.forEach(this.pollFns, function(pollFn){ 161 | pollFn(); 162 | }); 163 | }, 164 | 165 | addPollFn: function(pollFn) { 166 | this.pollFns.push(pollFn); 167 | return pollFn; 168 | }, 169 | 170 | url: function(url, replace) { 171 | if (url) { 172 | this.$$url = url; 173 | return this; 174 | } 175 | 176 | return this.$$url; 177 | }, 178 | 179 | cookies: function(name, value) { 180 | if (name) { 181 | if (value == undefined) { 182 | delete this.cookieHash[name]; 183 | } else { 184 | if (angular.isString(value) && //strings only 185 | value.length <= 4096) { //strict cookie storage limits 186 | this.cookieHash[name] = value; 187 | } 188 | } 189 | } else { 190 | if (!angular.equals(this.cookieHash, this.lastCookieHash)) { 191 | this.lastCookieHash = angular.copy(this.cookieHash); 192 | this.cookieHash = angular.copy(this.cookieHash); 193 | } 194 | return this.cookieHash; 195 | } 196 | }, 197 | 198 | notifyWhenNoOutstandingRequests: function(fn) { 199 | fn(); 200 | } 201 | }; 202 | 203 | 204 | /** 205 | * @ngdoc object 206 | * @name ngMock.$exceptionHandlerProvider 207 | * 208 | * @description 209 | * Configures the mock implementation of {@link ng.$exceptionHandler} to rethrow or to log errors passed 210 | * into the `$exceptionHandler`. 211 | */ 212 | 213 | /** 214 | * @ngdoc object 215 | * @name ngMock.$exceptionHandler 216 | * 217 | * @description 218 | * Mock implementation of {@link ng.$exceptionHandler} that rethrows or logs errors passed 219 | * into it. See {@link ngMock.$exceptionHandlerProvider $exceptionHandlerProvider} for configuration 220 | * information. 221 | * 222 | * 223 | *
 224 |  *   describe('$exceptionHandlerProvider', function() {
 225 |  *
 226 |  *     it('should capture log messages and exceptions', function() {
 227 |  *
 228 |  *       module(function($exceptionHandlerProvider) {
 229 |  *         $exceptionHandlerProvider.mode('log');
 230 |  *       });
 231 |  *
 232 |  *       inject(function($log, $exceptionHandler, $timeout) {
 233 |  *         $timeout(function() { $log.log(1); });
 234 |  *         $timeout(function() { $log.log(2); throw 'banana peel'; });
 235 |  *         $timeout(function() { $log.log(3); });
 236 |  *         expect($exceptionHandler.errors).toEqual([]);
 237 |  *         expect($log.assertEmpty());
 238 |  *         $timeout.flush();
 239 |  *         expect($exceptionHandler.errors).toEqual(['banana peel']);
 240 |  *         expect($log.log.logs).toEqual([[1], [2], [3]]);
 241 |  *       });
 242 |  *     });
 243 |  *   });
 244 |  * 
245 | */ 246 | 247 | angular.mock.$ExceptionHandlerProvider = function() { 248 | var handler; 249 | 250 | /** 251 | * @ngdoc method 252 | * @name ngMock.$exceptionHandlerProvider#mode 253 | * @methodOf ngMock.$exceptionHandlerProvider 254 | * 255 | * @description 256 | * Sets the logging mode. 257 | * 258 | * @param {string} mode Mode of operation, defaults to `rethrow`. 259 | * 260 | * - `rethrow`: If any errors are passed into the handler in tests, it typically 261 | * means that there is a bug in the application or test, so this mock will 262 | * make these tests fail. 263 | * - `log`: Sometimes it is desirable to test that an error is thrown, for this case the `log` mode stores an 264 | * array of errors in `$exceptionHandler.errors`, to allow later assertion of them. 265 | * See {@link ngMock.$log#assertEmpty assertEmpty()} and 266 | * {@link ngMock.$log#reset reset()} 267 | */ 268 | this.mode = function(mode) { 269 | switch(mode) { 270 | case 'rethrow': 271 | handler = function(e) { 272 | throw e; 273 | }; 274 | break; 275 | case 'log': 276 | var errors = []; 277 | 278 | handler = function(e) { 279 | if (arguments.length == 1) { 280 | errors.push(e); 281 | } else { 282 | errors.push([].slice.call(arguments, 0)); 283 | } 284 | }; 285 | 286 | handler.errors = errors; 287 | break; 288 | default: 289 | throw Error("Unknown mode '" + mode + "', only 'log'/'rethrow' modes are allowed!"); 290 | } 291 | }; 292 | 293 | this.$get = function() { 294 | return handler; 295 | }; 296 | 297 | this.mode('rethrow'); 298 | }; 299 | 300 | 301 | /** 302 | * @ngdoc service 303 | * @name ngMock.$log 304 | * 305 | * @description 306 | * Mock implementation of {@link ng.$log} that gathers all logged messages in arrays 307 | * (one array per logging level). These arrays are exposed as `logs` property of each of the 308 | * level-specific log function, e.g. for level `error` the array is exposed as `$log.error.logs`. 309 | * 310 | */ 311 | angular.mock.$LogProvider = function() { 312 | var debug = true; 313 | 314 | function concat(array1, array2, index) { 315 | return array1.concat(Array.prototype.slice.call(array2, index)); 316 | } 317 | 318 | this.debugEnabled = function(flag) { 319 | if (angular.isDefined(flag)) { 320 | debug = flag; 321 | return this; 322 | } else { 323 | return debug; 324 | } 325 | }; 326 | 327 | this.$get = function () { 328 | var $log = { 329 | log: function() { $log.log.logs.push(concat([], arguments, 0)); }, 330 | warn: function() { $log.warn.logs.push(concat([], arguments, 0)); }, 331 | info: function() { $log.info.logs.push(concat([], arguments, 0)); }, 332 | error: function() { $log.error.logs.push(concat([], arguments, 0)); }, 333 | debug: function() { 334 | if (debug) { 335 | $log.debug.logs.push(concat([], arguments, 0)); 336 | } 337 | } 338 | }; 339 | 340 | /** 341 | * @ngdoc method 342 | * @name ngMock.$log#reset 343 | * @methodOf ngMock.$log 344 | * 345 | * @description 346 | * Reset all of the logging arrays to empty. 347 | */ 348 | $log.reset = function () { 349 | /** 350 | * @ngdoc property 351 | * @name ngMock.$log#log.logs 352 | * @propertyOf ngMock.$log 353 | * 354 | * @description 355 | * Array of messages logged using {@link ngMock.$log#log}. 356 | * 357 | * @example 358 | *
 359 |        * $log.log('Some Log');
 360 |        * var first = $log.log.logs.unshift();
 361 |        * 
362 | */ 363 | $log.log.logs = []; 364 | /** 365 | * @ngdoc property 366 | * @name ngMock.$log#info.logs 367 | * @propertyOf ngMock.$log 368 | * 369 | * @description 370 | * Array of messages logged using {@link ngMock.$log#info}. 371 | * 372 | * @example 373 | *
 374 |        * $log.info('Some Info');
 375 |        * var first = $log.info.logs.unshift();
 376 |        * 
377 | */ 378 | $log.info.logs = []; 379 | /** 380 | * @ngdoc property 381 | * @name ngMock.$log#warn.logs 382 | * @propertyOf ngMock.$log 383 | * 384 | * @description 385 | * Array of messages logged using {@link ngMock.$log#warn}. 386 | * 387 | * @example 388 | *
 389 |        * $log.warn('Some Warning');
 390 |        * var first = $log.warn.logs.unshift();
 391 |        * 
392 | */ 393 | $log.warn.logs = []; 394 | /** 395 | * @ngdoc property 396 | * @name ngMock.$log#error.logs 397 | * @propertyOf ngMock.$log 398 | * 399 | * @description 400 | * Array of messages logged using {@link ngMock.$log#error}. 401 | * 402 | * @example 403 | *
 404 |        * $log.log('Some Error');
 405 |        * var first = $log.error.logs.unshift();
 406 |        * 
407 | */ 408 | $log.error.logs = []; 409 | /** 410 | * @ngdoc property 411 | * @name ngMock.$log#debug.logs 412 | * @propertyOf ngMock.$log 413 | * 414 | * @description 415 | * Array of messages logged using {@link ngMock.$log#debug}. 416 | * 417 | * @example 418 | *
 419 |        * $log.debug('Some Error');
 420 |        * var first = $log.debug.logs.unshift();
 421 |        * 
422 | */ 423 | $log.debug.logs = [] 424 | }; 425 | 426 | /** 427 | * @ngdoc method 428 | * @name ngMock.$log#assertEmpty 429 | * @methodOf ngMock.$log 430 | * 431 | * @description 432 | * Assert that the all of the logging methods have no logged messages. If messages present, an exception is thrown. 433 | */ 434 | $log.assertEmpty = function() { 435 | var errors = []; 436 | angular.forEach(['error', 'warn', 'info', 'log', 'debug'], function(logLevel) { 437 | angular.forEach($log[logLevel].logs, function(log) { 438 | angular.forEach(log, function (logItem) { 439 | errors.push('MOCK $log (' + logLevel + '): ' + String(logItem) + '\n' + (logItem.stack || '')); 440 | }); 441 | }); 442 | }); 443 | if (errors.length) { 444 | errors.unshift("Expected $log to be empty! Either a message was logged unexpectedly, or an expected " + 445 | "log message was not checked and removed:"); 446 | errors.push(''); 447 | throw new Error(errors.join('\n---------\n')); 448 | } 449 | }; 450 | 451 | $log.reset(); 452 | return $log; 453 | }; 454 | }; 455 | 456 | 457 | (function() { 458 | var R_ISO8061_STR = /^(\d{4})-?(\d\d)-?(\d\d)(?:T(\d\d)(?:\:?(\d\d)(?:\:?(\d\d)(?:\.(\d{3}))?)?)?(Z|([+-])(\d\d):?(\d\d)))?$/; 459 | 460 | function jsonStringToDate(string) { 461 | var match; 462 | if (match = string.match(R_ISO8061_STR)) { 463 | var date = new Date(0), 464 | tzHour = 0, 465 | tzMin = 0; 466 | if (match[9]) { 467 | tzHour = int(match[9] + match[10]); 468 | tzMin = int(match[9] + match[11]); 469 | } 470 | date.setUTCFullYear(int(match[1]), int(match[2]) - 1, int(match[3])); 471 | date.setUTCHours(int(match[4]||0) - tzHour, int(match[5]||0) - tzMin, int(match[6]||0), int(match[7]||0)); 472 | return date; 473 | } 474 | return string; 475 | } 476 | 477 | function int(str) { 478 | return parseInt(str, 10); 479 | } 480 | 481 | function padNumber(num, digits, trim) { 482 | var neg = ''; 483 | if (num < 0) { 484 | neg = '-'; 485 | num = -num; 486 | } 487 | num = '' + num; 488 | while(num.length < digits) num = '0' + num; 489 | if (trim) 490 | num = num.substr(num.length - digits); 491 | return neg + num; 492 | } 493 | 494 | 495 | /** 496 | * @ngdoc object 497 | * @name angular.mock.TzDate 498 | * @description 499 | * 500 | * *NOTE*: this is not an injectable instance, just a globally available mock class of `Date`. 501 | * 502 | * Mock of the Date type which has its timezone specified via constructor arg. 503 | * 504 | * The main purpose is to create Date-like instances with timezone fixed to the specified timezone 505 | * offset, so that we can test code that depends on local timezone settings without dependency on 506 | * the time zone settings of the machine where the code is running. 507 | * 508 | * @param {number} offset Offset of the *desired* timezone in hours (fractions will be honored) 509 | * @param {(number|string)} timestamp Timestamp representing the desired time in *UTC* 510 | * 511 | * @example 512 | * !!!! WARNING !!!!! 513 | * This is not a complete Date object so only methods that were implemented can be called safely. 514 | * To make matters worse, TzDate instances inherit stuff from Date via a prototype. 515 | * 516 | * We do our best to intercept calls to "unimplemented" methods, but since the list of methods is 517 | * incomplete we might be missing some non-standard methods. This can result in errors like: 518 | * "Date.prototype.foo called on incompatible Object". 519 | * 520 | *
 521 |    * var newYearInBratislava = new TzDate(-1, '2009-12-31T23:00:00Z');
 522 |    * newYearInBratislava.getTimezoneOffset() => -60;
 523 |    * newYearInBratislava.getFullYear() => 2010;
 524 |    * newYearInBratislava.getMonth() => 0;
 525 |    * newYearInBratislava.getDate() => 1;
 526 |    * newYearInBratislava.getHours() => 0;
 527 |    * newYearInBratislava.getMinutes() => 0;
 528 |    * newYearInBratislava.getSeconds() => 0;
 529 |    * 
530 | * 531 | */ 532 | angular.mock.TzDate = function (offset, timestamp) { 533 | var self = new Date(0); 534 | if (angular.isString(timestamp)) { 535 | var tsStr = timestamp; 536 | 537 | self.origDate = jsonStringToDate(timestamp); 538 | 539 | timestamp = self.origDate.getTime(); 540 | if (isNaN(timestamp)) 541 | throw { 542 | name: "Illegal Argument", 543 | message: "Arg '" + tsStr + "' passed into TzDate constructor is not a valid date string" 544 | }; 545 | } else { 546 | self.origDate = new Date(timestamp); 547 | } 548 | 549 | var localOffset = new Date(timestamp).getTimezoneOffset(); 550 | self.offsetDiff = localOffset*60*1000 - offset*1000*60*60; 551 | self.date = new Date(timestamp + self.offsetDiff); 552 | 553 | self.getTime = function() { 554 | return self.date.getTime() - self.offsetDiff; 555 | }; 556 | 557 | self.toLocaleDateString = function() { 558 | return self.date.toLocaleDateString(); 559 | }; 560 | 561 | self.getFullYear = function() { 562 | return self.date.getFullYear(); 563 | }; 564 | 565 | self.getMonth = function() { 566 | return self.date.getMonth(); 567 | }; 568 | 569 | self.getDate = function() { 570 | return self.date.getDate(); 571 | }; 572 | 573 | self.getHours = function() { 574 | return self.date.getHours(); 575 | }; 576 | 577 | self.getMinutes = function() { 578 | return self.date.getMinutes(); 579 | }; 580 | 581 | self.getSeconds = function() { 582 | return self.date.getSeconds(); 583 | }; 584 | 585 | self.getMilliseconds = function() { 586 | return self.date.getMilliseconds(); 587 | }; 588 | 589 | self.getTimezoneOffset = function() { 590 | return offset * 60; 591 | }; 592 | 593 | self.getUTCFullYear = function() { 594 | return self.origDate.getUTCFullYear(); 595 | }; 596 | 597 | self.getUTCMonth = function() { 598 | return self.origDate.getUTCMonth(); 599 | }; 600 | 601 | self.getUTCDate = function() { 602 | return self.origDate.getUTCDate(); 603 | }; 604 | 605 | self.getUTCHours = function() { 606 | return self.origDate.getUTCHours(); 607 | }; 608 | 609 | self.getUTCMinutes = function() { 610 | return self.origDate.getUTCMinutes(); 611 | }; 612 | 613 | self.getUTCSeconds = function() { 614 | return self.origDate.getUTCSeconds(); 615 | }; 616 | 617 | self.getUTCMilliseconds = function() { 618 | return self.origDate.getUTCMilliseconds(); 619 | }; 620 | 621 | self.getDay = function() { 622 | return self.date.getDay(); 623 | }; 624 | 625 | // provide this method only on browsers that already have it 626 | if (self.toISOString) { 627 | self.toISOString = function() { 628 | return padNumber(self.origDate.getUTCFullYear(), 4) + '-' + 629 | padNumber(self.origDate.getUTCMonth() + 1, 2) + '-' + 630 | padNumber(self.origDate.getUTCDate(), 2) + 'T' + 631 | padNumber(self.origDate.getUTCHours(), 2) + ':' + 632 | padNumber(self.origDate.getUTCMinutes(), 2) + ':' + 633 | padNumber(self.origDate.getUTCSeconds(), 2) + '.' + 634 | padNumber(self.origDate.getUTCMilliseconds(), 3) + 'Z' 635 | } 636 | } 637 | 638 | //hide all methods not implemented in this mock that the Date prototype exposes 639 | var unimplementedMethods = ['getUTCDay', 640 | 'getYear', 'setDate', 'setFullYear', 'setHours', 'setMilliseconds', 641 | 'setMinutes', 'setMonth', 'setSeconds', 'setTime', 'setUTCDate', 'setUTCFullYear', 642 | 'setUTCHours', 'setUTCMilliseconds', 'setUTCMinutes', 'setUTCMonth', 'setUTCSeconds', 643 | 'setYear', 'toDateString', 'toGMTString', 'toJSON', 'toLocaleFormat', 'toLocaleString', 644 | 'toLocaleTimeString', 'toSource', 'toString', 'toTimeString', 'toUTCString', 'valueOf']; 645 | 646 | angular.forEach(unimplementedMethods, function(methodName) { 647 | self[methodName] = function() { 648 | throw Error("Method '" + methodName + "' is not implemented in the TzDate mock"); 649 | }; 650 | }); 651 | 652 | return self; 653 | }; 654 | 655 | //make "tzDateInstance instanceof Date" return true 656 | angular.mock.TzDate.prototype = Date.prototype; 657 | })(); 658 | 659 | angular.mock.animate = angular.module('mock.animate', ['ng']) 660 | 661 | .config(['$provide', function($provide) { 662 | 663 | $provide.decorator('$animate', function($delegate) { 664 | var animate = { 665 | queue : [], 666 | enabled : $delegate.enabled, 667 | flushNext : function(name) { 668 | var tick = animate.queue.shift(); 669 | expect(tick.method).toBe(name); 670 | tick.fn(); 671 | return tick; 672 | } 673 | }; 674 | 675 | forEach(['enter','leave','move','addClass','removeClass'], function(method) { 676 | animate[method] = function() { 677 | var params = arguments; 678 | animate.queue.push({ 679 | method : method, 680 | params : params, 681 | element : angular.isElement(params[0]) && params[0], 682 | parent : angular.isElement(params[1]) && params[1], 683 | after : angular.isElement(params[2]) && params[2], 684 | fn : function() { 685 | $delegate[method].apply($delegate, params); 686 | } 687 | }); 688 | }; 689 | }); 690 | 691 | return animate; 692 | }); 693 | 694 | }]); 695 | 696 | 697 | /** 698 | * @ngdoc function 699 | * @name angular.mock.dump 700 | * @description 701 | * 702 | * *NOTE*: this is not an injectable instance, just a globally available function. 703 | * 704 | * Method for serializing common angular objects (scope, elements, etc..) into strings, useful for debugging. 705 | * 706 | * This method is also available on window, where it can be used to display objects on debug console. 707 | * 708 | * @param {*} object - any object to turn into string. 709 | * @return {string} a serialized string of the argument 710 | */ 711 | angular.mock.dump = function(object) { 712 | return serialize(object); 713 | 714 | function serialize(object) { 715 | var out; 716 | 717 | if (angular.isElement(object)) { 718 | object = angular.element(object); 719 | out = angular.element('
'); 720 | angular.forEach(object, function(element) { 721 | out.append(angular.element(element).clone()); 722 | }); 723 | out = out.html(); 724 | } else if (angular.isArray(object)) { 725 | out = []; 726 | angular.forEach(object, function(o) { 727 | out.push(serialize(o)); 728 | }); 729 | out = '[ ' + out.join(', ') + ' ]'; 730 | } else if (angular.isObject(object)) { 731 | if (angular.isFunction(object.$eval) && angular.isFunction(object.$apply)) { 732 | out = serializeScope(object); 733 | } else if (object instanceof Error) { 734 | out = object.stack || ('' + object.name + ': ' + object.message); 735 | } else { 736 | // TODO(i): this prevents methods to be logged, we should have a better way to serialize objects 737 | out = angular.toJson(object, true); 738 | } 739 | } else { 740 | out = String(object); 741 | } 742 | 743 | return out; 744 | } 745 | 746 | function serializeScope(scope, offset) { 747 | offset = offset || ' '; 748 | var log = [offset + 'Scope(' + scope.$id + '): {']; 749 | for ( var key in scope ) { 750 | if (scope.hasOwnProperty(key) && !key.match(/^(\$|this)/)) { 751 | log.push(' ' + key + ': ' + angular.toJson(scope[key])); 752 | } 753 | } 754 | var child = scope.$$childHead; 755 | while(child) { 756 | log.push(serializeScope(child, offset + ' ')); 757 | child = child.$$nextSibling; 758 | } 759 | log.push('}'); 760 | return log.join('\n' + offset); 761 | } 762 | }; 763 | 764 | /** 765 | * @ngdoc object 766 | * @name ngMock.$httpBackend 767 | * @description 768 | * Fake HTTP backend implementation suitable for unit testing applications that use the 769 | * {@link ng.$http $http service}. 770 | * 771 | * *Note*: For fake HTTP backend implementation suitable for end-to-end testing or backend-less 772 | * development please see {@link ngMockE2E.$httpBackend e2e $httpBackend mock}. 773 | * 774 | * During unit testing, we want our unit tests to run quickly and have no external dependencies so 775 | * we don’t want to send {@link https://developer.mozilla.org/en/xmlhttprequest XHR} or 776 | * {@link http://en.wikipedia.org/wiki/JSONP JSONP} requests to a real server. All we really need is 777 | * to verify whether a certain request has been sent or not, or alternatively just let the 778 | * application make requests, respond with pre-trained responses and assert that the end result is 779 | * what we expect it to be. 780 | * 781 | * This mock implementation can be used to respond with static or dynamic responses via the 782 | * `expect` and `when` apis and their shortcuts (`expectGET`, `whenPOST`, etc). 783 | * 784 | * When an Angular application needs some data from a server, it calls the $http service, which 785 | * sends the request to a real server using $httpBackend service. With dependency injection, it is 786 | * easy to inject $httpBackend mock (which has the same API as $httpBackend) and use it to verify 787 | * the requests and respond with some testing data without sending a request to real server. 788 | * 789 | * There are two ways to specify what test data should be returned as http responses by the mock 790 | * backend when the code under test makes http requests: 791 | * 792 | * - `$httpBackend.expect` - specifies a request expectation 793 | * - `$httpBackend.when` - specifies a backend definition 794 | * 795 | * 796 | * # Request Expectations vs Backend Definitions 797 | * 798 | * Request expectations provide a way to make assertions about requests made by the application and 799 | * to define responses for those requests. The test will fail if the expected requests are not made 800 | * or they are made in the wrong order. 801 | * 802 | * Backend definitions allow you to define a fake backend for your application which doesn't assert 803 | * if a particular request was made or not, it just returns a trained response if a request is made. 804 | * The test will pass whether or not the request gets made during testing. 805 | * 806 | * 807 | * 808 | * 809 | * 810 | * 811 | * 812 | * 813 | * 814 | * 815 | * 816 | * 817 | * 818 | * 819 | * 820 | * 821 | * 822 | * 823 | * 824 | * 825 | * 826 | * 827 | * 828 | * 829 | * 830 | * 831 | * 832 | * 833 | * 834 | * 835 | * 836 | * 837 | * 838 | * 839 | *
Request expectationsBackend definitions
Syntax.expect(...).respond(...).when(...).respond(...)
Typical usagestrict unit testsloose (black-box) unit testing
Fulfills multiple requestsNOYES
Order of requests mattersYESNO
Request requiredYESNO
Response requiredoptional (see below)YES
840 | * 841 | * In cases where both backend definitions and request expectations are specified during unit 842 | * testing, the request expectations are evaluated first. 843 | * 844 | * If a request expectation has no response specified, the algorithm will search your backend 845 | * definitions for an appropriate response. 846 | * 847 | * If a request didn't match any expectation or if the expectation doesn't have the response 848 | * defined, the backend definitions are evaluated in sequential order to see if any of them match 849 | * the request. The response from the first matched definition is returned. 850 | * 851 | * 852 | * # Flushing HTTP requests 853 | * 854 | * The $httpBackend used in production, always responds to requests with responses asynchronously. 855 | * If we preserved this behavior in unit testing, we'd have to create async unit tests, which are 856 | * hard to write, follow and maintain. At the same time the testing mock, can't respond 857 | * synchronously because that would change the execution of the code under test. For this reason the 858 | * mock $httpBackend has a `flush()` method, which allows the test to explicitly flush pending 859 | * requests and thus preserving the async api of the backend, while allowing the test to execute 860 | * synchronously. 861 | * 862 | * 863 | * # Unit testing with mock $httpBackend 864 | * The following code shows how to setup and use the mock backend in unit testing a controller. 865 | * First we create the controller under test 866 | * 867 |
 868 |   // The controller code
 869 |   function MyController($scope, $http) {
 870 |     var authToken;
 871 | 
 872 |     $http.get('/auth.py').success(function(data, status, headers) {
 873 |       authToken = headers('A-Token');
 874 |       $scope.user = data;
 875 |     });
 876 | 
 877 |     $scope.saveMessage = function(message) {
 878 |       var headers = { 'Authorization': authToken };
 879 |       $scope.status = 'Saving...';
 880 | 
 881 |       $http.post('/add-msg.py', message, { headers: headers } ).success(function(response) {
 882 |         $scope.status = '';
 883 |       }).error(function() {
 884 |         $scope.status = 'ERROR!';
 885 |       });
 886 |     };
 887 |   }
 888 |   
889 | * 890 | * Now we setup the mock backend and create the test specs. 891 | * 892 |
 893 |     // testing controller
 894 |     describe('MyController', function() {
 895 |        var $httpBackend, $rootScope, createController;
 896 | 
 897 |        beforeEach(inject(function($injector) {
 898 |          // Set up the mock http service responses
 899 |          $httpBackend = $injector.get('$httpBackend');
 900 |          // backend definition common for all tests
 901 |          $httpBackend.when('GET', '/auth.py').respond({userId: 'userX'}, {'A-Token': 'xxx'});
 902 | 
 903 |          // Get hold of a scope (i.e. the root scope)
 904 |          $rootScope = $injector.get('$rootScope');
 905 |          // The $controller service is used to create instances of controllers
 906 |          var $controller = $injector.get('$controller');
 907 | 
 908 |          createController = function() {
 909 |            return $controller('MyController', {'$scope' : $rootScope });
 910 |          };
 911 |        }));
 912 | 
 913 | 
 914 |        afterEach(function() {
 915 |          $httpBackend.verifyNoOutstandingExpectation();
 916 |          $httpBackend.verifyNoOutstandingRequest();
 917 |        });
 918 | 
 919 | 
 920 |        it('should fetch authentication token', function() {
 921 |          $httpBackend.expectGET('/auth.py');
 922 |          var controller = createController();
 923 |          $httpBackend.flush();
 924 |        });
 925 | 
 926 | 
 927 |        it('should send msg to server', function() {
 928 |          var controller = createController();
 929 |          $httpBackend.flush();
 930 | 
 931 |          // now you don’t care about the authentication, but
 932 |          // the controller will still send the request and
 933 |          // $httpBackend will respond without you having to
 934 |          // specify the expectation and response for this request
 935 | 
 936 |          $httpBackend.expectPOST('/add-msg.py', 'message content').respond(201, '');
 937 |          $rootScope.saveMessage('message content');
 938 |          expect($rootScope.status).toBe('Saving...');
 939 |          $httpBackend.flush();
 940 |          expect($rootScope.status).toBe('');
 941 |        });
 942 | 
 943 | 
 944 |        it('should send auth header', function() {
 945 |          var controller = createController();
 946 |          $httpBackend.flush();
 947 | 
 948 |          $httpBackend.expectPOST('/add-msg.py', undefined, function(headers) {
 949 |            // check if the header was send, if it wasn't the expectation won't
 950 |            // match the request and the test will fail
 951 |            return headers['Authorization'] == 'xxx';
 952 |          }).respond(201, '');
 953 | 
 954 |          $rootScope.saveMessage('whatever');
 955 |          $httpBackend.flush();
 956 |        });
 957 |     });
 958 |    
959 | */ 960 | angular.mock.$HttpBackendProvider = function() { 961 | this.$get = ['$rootScope', createHttpBackendMock]; 962 | }; 963 | 964 | /** 965 | * General factory function for $httpBackend mock. 966 | * Returns instance for unit testing (when no arguments specified): 967 | * - passing through is disabled 968 | * - auto flushing is disabled 969 | * 970 | * Returns instance for e2e testing (when `$delegate` and `$browser` specified): 971 | * - passing through (delegating request to real backend) is enabled 972 | * - auto flushing is enabled 973 | * 974 | * @param {Object=} $delegate Real $httpBackend instance (allow passing through if specified) 975 | * @param {Object=} $browser Auto-flushing enabled if specified 976 | * @return {Object} Instance of $httpBackend mock 977 | */ 978 | function createHttpBackendMock($rootScope, $delegate, $browser) { 979 | var definitions = [], 980 | expectations = [], 981 | responses = [], 982 | responsesPush = angular.bind(responses, responses.push); 983 | 984 | function createResponse(status, data, headers) { 985 | if (angular.isFunction(status)) return status; 986 | 987 | return function() { 988 | return angular.isNumber(status) 989 | ? [status, data, headers] 990 | : [200, status, data]; 991 | }; 992 | } 993 | 994 | // TODO(vojta): change params to: method, url, data, headers, callback 995 | function $httpBackend(method, url, data, callback, headers, timeout, withCredentials) { 996 | var xhr = new MockXhr(), 997 | expectation = expectations[0], 998 | wasExpected = false; 999 | 1000 | function prettyPrint(data) { 1001 | return (angular.isString(data) || angular.isFunction(data) || data instanceof RegExp) 1002 | ? data 1003 | : angular.toJson(data); 1004 | } 1005 | 1006 | function wrapResponse(wrapped) { 1007 | if (!$browser && timeout && timeout.then) timeout.then(handleTimeout); 1008 | 1009 | return handleResponse; 1010 | 1011 | function handleResponse() { 1012 | var response = wrapped.response(method, url, data, headers); 1013 | xhr.$$respHeaders = response[2]; 1014 | callback(response[0], response[1], xhr.getAllResponseHeaders()); 1015 | } 1016 | 1017 | function handleTimeout() { 1018 | for (var i = 0, ii = responses.length; i < ii; i++) { 1019 | if (responses[i] === handleResponse) { 1020 | responses.splice(i, 1); 1021 | callback(-1, undefined, ''); 1022 | break; 1023 | } 1024 | } 1025 | } 1026 | } 1027 | 1028 | if (expectation && expectation.match(method, url)) { 1029 | if (!expectation.matchData(data)) 1030 | throw new Error('Expected ' + expectation + ' with different data\n' + 1031 | 'EXPECTED: ' + prettyPrint(expectation.data) + '\nGOT: ' + data); 1032 | 1033 | if (!expectation.matchHeaders(headers)) 1034 | throw new Error('Expected ' + expectation + ' with different headers\n' + 1035 | 'EXPECTED: ' + prettyPrint(expectation.headers) + '\nGOT: ' + prettyPrint(headers)); 1036 | 1037 | expectations.shift(); 1038 | 1039 | if (expectation.response) { 1040 | responses.push(wrapResponse(expectation)); 1041 | return; 1042 | } 1043 | wasExpected = true; 1044 | } 1045 | 1046 | var i = -1, definition; 1047 | while ((definition = definitions[++i])) { 1048 | if (definition.match(method, url, data, headers || {})) { 1049 | if (definition.response) { 1050 | // if $browser specified, we do auto flush all requests 1051 | ($browser ? $browser.defer : responsesPush)(wrapResponse(definition)); 1052 | } else if (definition.passThrough) { 1053 | $delegate(method, url, data, callback, headers, timeout, withCredentials); 1054 | } else throw Error('No response defined !'); 1055 | return; 1056 | } 1057 | } 1058 | throw wasExpected ? 1059 | new Error('No response defined !') : 1060 | new Error('Unexpected request: ' + method + ' ' + url + '\n' + 1061 | (expectation ? 'Expected ' + expectation : 'No more request expected')); 1062 | } 1063 | 1064 | /** 1065 | * @ngdoc method 1066 | * @name ngMock.$httpBackend#when 1067 | * @methodOf ngMock.$httpBackend 1068 | * @description 1069 | * Creates a new backend definition. 1070 | * 1071 | * @param {string} method HTTP method. 1072 | * @param {string|RegExp} url HTTP url. 1073 | * @param {(string|RegExp|function(string))=} data HTTP request body or function that receives 1074 | * data string and returns true if the data is as expected. 1075 | * @param {(Object|function(Object))=} headers HTTP headers or function that receives http header 1076 | * object and returns true if the headers match the current definition. 1077 | * @returns {requestHandler} Returns an object with `respond` method that control how a matched 1078 | * request is handled. 1079 | * 1080 | * - respond – `{function([status,] data[, headers])|function(function(method, url, data, headers)}` 1081 | * – The respond method takes a set of static data to be returned or a function that can return 1082 | * an array containing response status (number), response data (string) and response headers 1083 | * (Object). 1084 | */ 1085 | $httpBackend.when = function(method, url, data, headers) { 1086 | var definition = new MockHttpExpectation(method, url, data, headers), 1087 | chain = { 1088 | respond: function(status, data, headers) { 1089 | definition.response = createResponse(status, data, headers); 1090 | } 1091 | }; 1092 | 1093 | if ($browser) { 1094 | chain.passThrough = function() { 1095 | definition.passThrough = true; 1096 | }; 1097 | } 1098 | 1099 | definitions.push(definition); 1100 | return chain; 1101 | }; 1102 | 1103 | /** 1104 | * @ngdoc method 1105 | * @name ngMock.$httpBackend#whenGET 1106 | * @methodOf ngMock.$httpBackend 1107 | * @description 1108 | * Creates a new backend definition for GET requests. For more info see `when()`. 1109 | * 1110 | * @param {string|RegExp} url HTTP url. 1111 | * @param {(Object|function(Object))=} headers HTTP headers. 1112 | * @returns {requestHandler} Returns an object with `respond` method that control how a matched 1113 | * request is handled. 1114 | */ 1115 | 1116 | /** 1117 | * @ngdoc method 1118 | * @name ngMock.$httpBackend#whenHEAD 1119 | * @methodOf ngMock.$httpBackend 1120 | * @description 1121 | * Creates a new backend definition for HEAD requests. For more info see `when()`. 1122 | * 1123 | * @param {string|RegExp} url HTTP url. 1124 | * @param {(Object|function(Object))=} headers HTTP headers. 1125 | * @returns {requestHandler} Returns an object with `respond` method that control how a matched 1126 | * request is handled. 1127 | */ 1128 | 1129 | /** 1130 | * @ngdoc method 1131 | * @name ngMock.$httpBackend#whenDELETE 1132 | * @methodOf ngMock.$httpBackend 1133 | * @description 1134 | * Creates a new backend definition for DELETE requests. For more info see `when()`. 1135 | * 1136 | * @param {string|RegExp} url HTTP url. 1137 | * @param {(Object|function(Object))=} headers HTTP headers. 1138 | * @returns {requestHandler} Returns an object with `respond` method that control how a matched 1139 | * request is handled. 1140 | */ 1141 | 1142 | /** 1143 | * @ngdoc method 1144 | * @name ngMock.$httpBackend#whenPOST 1145 | * @methodOf ngMock.$httpBackend 1146 | * @description 1147 | * Creates a new backend definition for POST requests. For more info see `when()`. 1148 | * 1149 | * @param {string|RegExp} url HTTP url. 1150 | * @param {(string|RegExp|function(string))=} data HTTP request body or function that receives 1151 | * data string and returns true if the data is as expected. 1152 | * @param {(Object|function(Object))=} headers HTTP headers. 1153 | * @returns {requestHandler} Returns an object with `respond` method that control how a matched 1154 | * request is handled. 1155 | */ 1156 | 1157 | /** 1158 | * @ngdoc method 1159 | * @name ngMock.$httpBackend#whenPUT 1160 | * @methodOf ngMock.$httpBackend 1161 | * @description 1162 | * Creates a new backend definition for PUT requests. For more info see `when()`. 1163 | * 1164 | * @param {string|RegExp} url HTTP url. 1165 | * @param {(string|RegExp|function(string))=} data HTTP request body or function that receives 1166 | * data string and returns true if the data is as expected. 1167 | * @param {(Object|function(Object))=} headers HTTP headers. 1168 | * @returns {requestHandler} Returns an object with `respond` method that control how a matched 1169 | * request is handled. 1170 | */ 1171 | 1172 | /** 1173 | * @ngdoc method 1174 | * @name ngMock.$httpBackend#whenJSONP 1175 | * @methodOf ngMock.$httpBackend 1176 | * @description 1177 | * Creates a new backend definition for JSONP requests. For more info see `when()`. 1178 | * 1179 | * @param {string|RegExp} url HTTP url. 1180 | * @returns {requestHandler} Returns an object with `respond` method that control how a matched 1181 | * request is handled. 1182 | */ 1183 | createShortMethods('when'); 1184 | 1185 | 1186 | /** 1187 | * @ngdoc method 1188 | * @name ngMock.$httpBackend#expect 1189 | * @methodOf ngMock.$httpBackend 1190 | * @description 1191 | * Creates a new request expectation. 1192 | * 1193 | * @param {string} method HTTP method. 1194 | * @param {string|RegExp} url HTTP url. 1195 | * @param {(string|RegExp|function(string)|Object)=} data HTTP request body or function that 1196 | * receives data string and returns true if the data is as expected, or Object if request body 1197 | * is in JSON format. 1198 | * @param {(Object|function(Object))=} headers HTTP headers or function that receives http header 1199 | * object and returns true if the headers match the current expectation. 1200 | * @returns {requestHandler} Returns an object with `respond` method that control how a matched 1201 | * request is handled. 1202 | * 1203 | * - respond – `{function([status,] data[, headers])|function(function(method, url, data, headers)}` 1204 | * – The respond method takes a set of static data to be returned or a function that can return 1205 | * an array containing response status (number), response data (string) and response headers 1206 | * (Object). 1207 | */ 1208 | $httpBackend.expect = function(method, url, data, headers) { 1209 | var expectation = new MockHttpExpectation(method, url, data, headers); 1210 | expectations.push(expectation); 1211 | return { 1212 | respond: function(status, data, headers) { 1213 | expectation.response = createResponse(status, data, headers); 1214 | } 1215 | }; 1216 | }; 1217 | 1218 | 1219 | /** 1220 | * @ngdoc method 1221 | * @name ngMock.$httpBackend#expectGET 1222 | * @methodOf ngMock.$httpBackend 1223 | * @description 1224 | * Creates a new request expectation for GET requests. For more info see `expect()`. 1225 | * 1226 | * @param {string|RegExp} url HTTP url. 1227 | * @param {Object=} headers HTTP headers. 1228 | * @returns {requestHandler} Returns an object with `respond` method that control how a matched 1229 | * request is handled. See #expect for more info. 1230 | */ 1231 | 1232 | /** 1233 | * @ngdoc method 1234 | * @name ngMock.$httpBackend#expectHEAD 1235 | * @methodOf ngMock.$httpBackend 1236 | * @description 1237 | * Creates a new request expectation for HEAD requests. For more info see `expect()`. 1238 | * 1239 | * @param {string|RegExp} url HTTP url. 1240 | * @param {Object=} headers HTTP headers. 1241 | * @returns {requestHandler} Returns an object with `respond` method that control how a matched 1242 | * request is handled. 1243 | */ 1244 | 1245 | /** 1246 | * @ngdoc method 1247 | * @name ngMock.$httpBackend#expectDELETE 1248 | * @methodOf ngMock.$httpBackend 1249 | * @description 1250 | * Creates a new request expectation for DELETE requests. For more info see `expect()`. 1251 | * 1252 | * @param {string|RegExp} url HTTP url. 1253 | * @param {Object=} headers HTTP headers. 1254 | * @returns {requestHandler} Returns an object with `respond` method that control how a matched 1255 | * request is handled. 1256 | */ 1257 | 1258 | /** 1259 | * @ngdoc method 1260 | * @name ngMock.$httpBackend#expectPOST 1261 | * @methodOf ngMock.$httpBackend 1262 | * @description 1263 | * Creates a new request expectation for POST requests. For more info see `expect()`. 1264 | * 1265 | * @param {string|RegExp} url HTTP url. 1266 | * @param {(string|RegExp|function(string)|Object)=} data HTTP request body or function that 1267 | * receives data string and returns true if the data is as expected, or Object if request body 1268 | * is in JSON format. 1269 | * @param {Object=} headers HTTP headers. 1270 | * @returns {requestHandler} Returns an object with `respond` method that control how a matched 1271 | * request is handled. 1272 | */ 1273 | 1274 | /** 1275 | * @ngdoc method 1276 | * @name ngMock.$httpBackend#expectPUT 1277 | * @methodOf ngMock.$httpBackend 1278 | * @description 1279 | * Creates a new request expectation for PUT requests. For more info see `expect()`. 1280 | * 1281 | * @param {string|RegExp} url HTTP url. 1282 | * @param {(string|RegExp|function(string)|Object)=} data HTTP request body or function that 1283 | * receives data string and returns true if the data is as expected, or Object if request body 1284 | * is in JSON format. 1285 | * @param {Object=} headers HTTP headers. 1286 | * @returns {requestHandler} Returns an object with `respond` method that control how a matched 1287 | * request is handled. 1288 | */ 1289 | 1290 | /** 1291 | * @ngdoc method 1292 | * @name ngMock.$httpBackend#expectPATCH 1293 | * @methodOf ngMock.$httpBackend 1294 | * @description 1295 | * Creates a new request expectation for PATCH requests. For more info see `expect()`. 1296 | * 1297 | * @param {string|RegExp} url HTTP url. 1298 | * @param {(string|RegExp|function(string)|Object)=} data HTTP request body or function that 1299 | * receives data string and returns true if the data is as expected, or Object if request body 1300 | * is in JSON format. 1301 | * @param {Object=} headers HTTP headers. 1302 | * @returns {requestHandler} Returns an object with `respond` method that control how a matched 1303 | * request is handled. 1304 | */ 1305 | 1306 | /** 1307 | * @ngdoc method 1308 | * @name ngMock.$httpBackend#expectJSONP 1309 | * @methodOf ngMock.$httpBackend 1310 | * @description 1311 | * Creates a new request expectation for JSONP requests. For more info see `expect()`. 1312 | * 1313 | * @param {string|RegExp} url HTTP url. 1314 | * @returns {requestHandler} Returns an object with `respond` method that control how a matched 1315 | * request is handled. 1316 | */ 1317 | createShortMethods('expect'); 1318 | 1319 | 1320 | /** 1321 | * @ngdoc method 1322 | * @name ngMock.$httpBackend#flush 1323 | * @methodOf ngMock.$httpBackend 1324 | * @description 1325 | * Flushes all pending requests using the trained responses. 1326 | * 1327 | * @param {number=} count Number of responses to flush (in the order they arrived). If undefined, 1328 | * all pending requests will be flushed. If there are no pending requests when the flush method 1329 | * is called an exception is thrown (as this typically a sign of programming error). 1330 | */ 1331 | $httpBackend.flush = function(count) { 1332 | $rootScope.$digest(); 1333 | if (!responses.length) throw Error('No pending request to flush !'); 1334 | 1335 | if (angular.isDefined(count)) { 1336 | while (count--) { 1337 | if (!responses.length) throw Error('No more pending request to flush !'); 1338 | responses.shift()(); 1339 | } 1340 | } else { 1341 | while (responses.length) { 1342 | responses.shift()(); 1343 | } 1344 | } 1345 | $httpBackend.verifyNoOutstandingExpectation(); 1346 | }; 1347 | 1348 | 1349 | /** 1350 | * @ngdoc method 1351 | * @name ngMock.$httpBackend#verifyNoOutstandingExpectation 1352 | * @methodOf ngMock.$httpBackend 1353 | * @description 1354 | * Verifies that all of the requests defined via the `expect` api were made. If any of the 1355 | * requests were not made, verifyNoOutstandingExpectation throws an exception. 1356 | * 1357 | * Typically, you would call this method following each test case that asserts requests using an 1358 | * "afterEach" clause. 1359 | * 1360 | *
1361 |    *   afterEach($httpBackend.verifyNoOutstandingExpectation);
1362 |    * 
1363 | */ 1364 | $httpBackend.verifyNoOutstandingExpectation = function() { 1365 | $rootScope.$digest(); 1366 | if (expectations.length) { 1367 | throw new Error('Unsatisfied requests: ' + expectations.join(', ')); 1368 | } 1369 | }; 1370 | 1371 | 1372 | /** 1373 | * @ngdoc method 1374 | * @name ngMock.$httpBackend#verifyNoOutstandingRequest 1375 | * @methodOf ngMock.$httpBackend 1376 | * @description 1377 | * Verifies that there are no outstanding requests that need to be flushed. 1378 | * 1379 | * Typically, you would call this method following each test case that asserts requests using an 1380 | * "afterEach" clause. 1381 | * 1382 | *
1383 |    *   afterEach($httpBackend.verifyNoOutstandingRequest);
1384 |    * 
1385 | */ 1386 | $httpBackend.verifyNoOutstandingRequest = function() { 1387 | if (responses.length) { 1388 | throw Error('Unflushed requests: ' + responses.length); 1389 | } 1390 | }; 1391 | 1392 | 1393 | /** 1394 | * @ngdoc method 1395 | * @name ngMock.$httpBackend#resetExpectations 1396 | * @methodOf ngMock.$httpBackend 1397 | * @description 1398 | * Resets all request expectations, but preserves all backend definitions. Typically, you would 1399 | * call resetExpectations during a multiple-phase test when you want to reuse the same instance of 1400 | * $httpBackend mock. 1401 | */ 1402 | $httpBackend.resetExpectations = function() { 1403 | expectations.length = 0; 1404 | responses.length = 0; 1405 | }; 1406 | 1407 | return $httpBackend; 1408 | 1409 | 1410 | function createShortMethods(prefix) { 1411 | angular.forEach(['GET', 'DELETE', 'JSONP'], function(method) { 1412 | $httpBackend[prefix + method] = function(url, headers) { 1413 | return $httpBackend[prefix](method, url, undefined, headers) 1414 | } 1415 | }); 1416 | 1417 | angular.forEach(['PUT', 'POST', 'PATCH'], function(method) { 1418 | $httpBackend[prefix + method] = function(url, data, headers) { 1419 | return $httpBackend[prefix](method, url, data, headers) 1420 | } 1421 | }); 1422 | } 1423 | } 1424 | 1425 | function MockHttpExpectation(method, url, data, headers) { 1426 | 1427 | this.data = data; 1428 | this.headers = headers; 1429 | 1430 | this.match = function(m, u, d, h) { 1431 | if (method != m) return false; 1432 | if (!this.matchUrl(u)) return false; 1433 | if (angular.isDefined(d) && !this.matchData(d)) return false; 1434 | if (angular.isDefined(h) && !this.matchHeaders(h)) return false; 1435 | return true; 1436 | }; 1437 | 1438 | this.matchUrl = function(u) { 1439 | if (!url) return true; 1440 | if (angular.isFunction(url.test)) return url.test(u); 1441 | return url == u; 1442 | }; 1443 | 1444 | this.matchHeaders = function(h) { 1445 | if (angular.isUndefined(headers)) return true; 1446 | if (angular.isFunction(headers)) return headers(h); 1447 | return angular.equals(headers, h); 1448 | }; 1449 | 1450 | this.matchData = function(d) { 1451 | if (angular.isUndefined(data)) return true; 1452 | if (data && angular.isFunction(data.test)) return data.test(d); 1453 | if (data && angular.isFunction(data)) return data(d); 1454 | if (data && !angular.isString(data)) return angular.toJson(data) == d; 1455 | return data == d; 1456 | }; 1457 | 1458 | this.toString = function() { 1459 | return method + ' ' + url; 1460 | }; 1461 | } 1462 | 1463 | function MockXhr() { 1464 | 1465 | // hack for testing $http, $httpBackend 1466 | MockXhr.$$lastInstance = this; 1467 | 1468 | this.open = function(method, url, async) { 1469 | this.$$method = method; 1470 | this.$$url = url; 1471 | this.$$async = async; 1472 | this.$$reqHeaders = {}; 1473 | this.$$respHeaders = {}; 1474 | }; 1475 | 1476 | this.send = function(data) { 1477 | this.$$data = data; 1478 | }; 1479 | 1480 | this.setRequestHeader = function(key, value) { 1481 | this.$$reqHeaders[key] = value; 1482 | }; 1483 | 1484 | this.getResponseHeader = function(name) { 1485 | // the lookup must be case insensitive, that's why we try two quick lookups and full scan at last 1486 | var header = this.$$respHeaders[name]; 1487 | if (header) return header; 1488 | 1489 | name = angular.lowercase(name); 1490 | header = this.$$respHeaders[name]; 1491 | if (header) return header; 1492 | 1493 | header = undefined; 1494 | angular.forEach(this.$$respHeaders, function(headerVal, headerName) { 1495 | if (!header && angular.lowercase(headerName) == name) header = headerVal; 1496 | }); 1497 | return header; 1498 | }; 1499 | 1500 | this.getAllResponseHeaders = function() { 1501 | var lines = []; 1502 | 1503 | angular.forEach(this.$$respHeaders, function(value, key) { 1504 | lines.push(key + ': ' + value); 1505 | }); 1506 | return lines.join('\n'); 1507 | }; 1508 | 1509 | this.abort = angular.noop; 1510 | } 1511 | 1512 | 1513 | /** 1514 | * @ngdoc function 1515 | * @name ngMock.$timeout 1516 | * @description 1517 | * 1518 | * This service is just a simple decorator for {@link ng.$timeout $timeout} service 1519 | * that adds a "flush" and "verifyNoPendingTasks" methods. 1520 | */ 1521 | 1522 | angular.mock.$TimeoutDecorator = function($delegate, $browser) { 1523 | 1524 | /** 1525 | * @ngdoc method 1526 | * @name ngMock.$timeout#flush 1527 | * @methodOf ngMock.$timeout 1528 | * @description 1529 | * 1530 | * Flushes the queue of pending tasks. 1531 | * 1532 | * @param {number=} delay maximum timeout amount to flush up until 1533 | */ 1534 | $delegate.flush = function(delay) { 1535 | $browser.defer.flush(delay); 1536 | }; 1537 | 1538 | /** 1539 | * @ngdoc method 1540 | * @name ngMock.$timeout#flushNext 1541 | * @methodOf ngMock.$timeout 1542 | * @description 1543 | * 1544 | * Flushes the next timeout in the queue and compares it to the provided delay 1545 | * 1546 | * @param {number=} expectedDelay the delay value that will be asserted against the delay of the next timeout function 1547 | */ 1548 | $delegate.flushNext = function(expectedDelay) { 1549 | $browser.defer.flushNext(expectedDelay); 1550 | }; 1551 | 1552 | /** 1553 | * @ngdoc method 1554 | * @name ngMock.$timeout#verifyNoPendingTasks 1555 | * @methodOf ngMock.$timeout 1556 | * @description 1557 | * 1558 | * Verifies that there are no pending tasks that need to be flushed. 1559 | */ 1560 | $delegate.verifyNoPendingTasks = function() { 1561 | if ($browser.deferredFns.length) { 1562 | throw new Error('Deferred tasks to flush (' + $browser.deferredFns.length + '): ' + 1563 | formatPendingTasksAsString($browser.deferredFns)); 1564 | } 1565 | }; 1566 | 1567 | function formatPendingTasksAsString(tasks) { 1568 | var result = []; 1569 | angular.forEach(tasks, function(task) { 1570 | result.push('{id: ' + task.id + ', ' + 'time: ' + task.time + '}'); 1571 | }); 1572 | 1573 | return result.join(', '); 1574 | } 1575 | 1576 | return $delegate; 1577 | }; 1578 | 1579 | /** 1580 | * 1581 | */ 1582 | angular.mock.$RootElementProvider = function() { 1583 | this.$get = function() { 1584 | return angular.element('
'); 1585 | } 1586 | }; 1587 | 1588 | /** 1589 | * @ngdoc overview 1590 | * @name ngMock 1591 | * @description 1592 | * 1593 | * The `ngMock` is an angular module which is used with `ng` module and adds unit-test configuration as well as useful 1594 | * mocks to the {@link AUTO.$injector $injector}. 1595 | */ 1596 | angular.module('ngMock', ['ng']).provider({ 1597 | $browser: angular.mock.$BrowserProvider, 1598 | $exceptionHandler: angular.mock.$ExceptionHandlerProvider, 1599 | $log: angular.mock.$LogProvider, 1600 | $httpBackend: angular.mock.$HttpBackendProvider, 1601 | $rootElement: angular.mock.$RootElementProvider 1602 | }).config(function($provide) { 1603 | $provide.decorator('$timeout', angular.mock.$TimeoutDecorator); 1604 | }); 1605 | 1606 | /** 1607 | * @ngdoc overview 1608 | * @name ngMockE2E 1609 | * @description 1610 | * 1611 | * The `ngMockE2E` is an angular module which contains mocks suitable for end-to-end testing. 1612 | * Currently there is only one mock present in this module - 1613 | * the {@link ngMockE2E.$httpBackend e2e $httpBackend} mock. 1614 | */ 1615 | angular.module('ngMockE2E', ['ng']).config(function($provide) { 1616 | $provide.decorator('$httpBackend', angular.mock.e2e.$httpBackendDecorator); 1617 | }); 1618 | 1619 | /** 1620 | * @ngdoc object 1621 | * @name ngMockE2E.$httpBackend 1622 | * @description 1623 | * Fake HTTP backend implementation suitable for end-to-end testing or backend-less development of 1624 | * applications that use the {@link ng.$http $http service}. 1625 | * 1626 | * *Note*: For fake http backend implementation suitable for unit testing please see 1627 | * {@link ngMock.$httpBackend unit-testing $httpBackend mock}. 1628 | * 1629 | * This implementation can be used to respond with static or dynamic responses via the `when` api 1630 | * and its shortcuts (`whenGET`, `whenPOST`, etc) and optionally pass through requests to the 1631 | * real $httpBackend for specific requests (e.g. to interact with certain remote apis or to fetch 1632 | * templates from a webserver). 1633 | * 1634 | * As opposed to unit-testing, in an end-to-end testing scenario or in scenario when an application 1635 | * is being developed with the real backend api replaced with a mock, it is often desirable for 1636 | * certain category of requests to bypass the mock and issue a real http request (e.g. to fetch 1637 | * templates or static files from the webserver). To configure the backend with this behavior 1638 | * use the `passThrough` request handler of `when` instead of `respond`. 1639 | * 1640 | * Additionally, we don't want to manually have to flush mocked out requests like we do during unit 1641 | * testing. For this reason the e2e $httpBackend automatically flushes mocked out requests 1642 | * automatically, closely simulating the behavior of the XMLHttpRequest object. 1643 | * 1644 | * To setup the application to run with this http backend, you have to create a module that depends 1645 | * on the `ngMockE2E` and your application modules and defines the fake backend: 1646 | * 1647 | *
1648 |  *   myAppDev = angular.module('myAppDev', ['myApp', 'ngMockE2E']);
1649 |  *   myAppDev.run(function($httpBackend) {
1650 |  *     phones = [{name: 'phone1'}, {name: 'phone2'}];
1651 |  *
1652 |  *     // returns the current list of phones
1653 |  *     $httpBackend.whenGET('/phones').respond(phones);
1654 |  *
1655 |  *     // adds a new phone to the phones array
1656 |  *     $httpBackend.whenPOST('/phones').respond(function(method, url, data) {
1657 |  *       phones.push(angular.fromJson(data));
1658 |  *     });
1659 |  *     $httpBackend.whenGET(/^\/templates\//).passThrough();
1660 |  *     //...
1661 |  *   });
1662 |  * 
1663 | * 1664 | * Afterwards, bootstrap your app with this new module. 1665 | */ 1666 | 1667 | /** 1668 | * @ngdoc method 1669 | * @name ngMockE2E.$httpBackend#when 1670 | * @methodOf ngMockE2E.$httpBackend 1671 | * @description 1672 | * Creates a new backend definition. 1673 | * 1674 | * @param {string} method HTTP method. 1675 | * @param {string|RegExp} url HTTP url. 1676 | * @param {(string|RegExp)=} data HTTP request body. 1677 | * @param {(Object|function(Object))=} headers HTTP headers or function that receives http header 1678 | * object and returns true if the headers match the current definition. 1679 | * @returns {requestHandler} Returns an object with `respond` and `passThrough` methods that 1680 | * control how a matched request is handled. 1681 | * 1682 | * - respond – `{function([status,] data[, headers])|function(function(method, url, data, headers)}` 1683 | * – The respond method takes a set of static data to be returned or a function that can return 1684 | * an array containing response status (number), response data (string) and response headers 1685 | * (Object). 1686 | * - passThrough – `{function()}` – Any request matching a backend definition with `passThrough` 1687 | * handler, will be pass through to the real backend (an XHR request will be made to the 1688 | * server. 1689 | */ 1690 | 1691 | /** 1692 | * @ngdoc method 1693 | * @name ngMockE2E.$httpBackend#whenGET 1694 | * @methodOf ngMockE2E.$httpBackend 1695 | * @description 1696 | * Creates a new backend definition for GET requests. For more info see `when()`. 1697 | * 1698 | * @param {string|RegExp} url HTTP url. 1699 | * @param {(Object|function(Object))=} headers HTTP headers. 1700 | * @returns {requestHandler} Returns an object with `respond` and `passThrough` methods that 1701 | * control how a matched request is handled. 1702 | */ 1703 | 1704 | /** 1705 | * @ngdoc method 1706 | * @name ngMockE2E.$httpBackend#whenHEAD 1707 | * @methodOf ngMockE2E.$httpBackend 1708 | * @description 1709 | * Creates a new backend definition for HEAD requests. For more info see `when()`. 1710 | * 1711 | * @param {string|RegExp} url HTTP url. 1712 | * @param {(Object|function(Object))=} headers HTTP headers. 1713 | * @returns {requestHandler} Returns an object with `respond` and `passThrough` methods that 1714 | * control how a matched request is handled. 1715 | */ 1716 | 1717 | /** 1718 | * @ngdoc method 1719 | * @name ngMockE2E.$httpBackend#whenDELETE 1720 | * @methodOf ngMockE2E.$httpBackend 1721 | * @description 1722 | * Creates a new backend definition for DELETE requests. For more info see `when()`. 1723 | * 1724 | * @param {string|RegExp} url HTTP url. 1725 | * @param {(Object|function(Object))=} headers HTTP headers. 1726 | * @returns {requestHandler} Returns an object with `respond` and `passThrough` methods that 1727 | * control how a matched request is handled. 1728 | */ 1729 | 1730 | /** 1731 | * @ngdoc method 1732 | * @name ngMockE2E.$httpBackend#whenPOST 1733 | * @methodOf ngMockE2E.$httpBackend 1734 | * @description 1735 | * Creates a new backend definition for POST requests. For more info see `when()`. 1736 | * 1737 | * @param {string|RegExp} url HTTP url. 1738 | * @param {(string|RegExp)=} data HTTP request body. 1739 | * @param {(Object|function(Object))=} headers HTTP headers. 1740 | * @returns {requestHandler} Returns an object with `respond` and `passThrough` methods that 1741 | * control how a matched request is handled. 1742 | */ 1743 | 1744 | /** 1745 | * @ngdoc method 1746 | * @name ngMockE2E.$httpBackend#whenPUT 1747 | * @methodOf ngMockE2E.$httpBackend 1748 | * @description 1749 | * Creates a new backend definition for PUT requests. For more info see `when()`. 1750 | * 1751 | * @param {string|RegExp} url HTTP url. 1752 | * @param {(string|RegExp)=} data HTTP request body. 1753 | * @param {(Object|function(Object))=} headers HTTP headers. 1754 | * @returns {requestHandler} Returns an object with `respond` and `passThrough` methods that 1755 | * control how a matched request is handled. 1756 | */ 1757 | 1758 | /** 1759 | * @ngdoc method 1760 | * @name ngMockE2E.$httpBackend#whenPATCH 1761 | * @methodOf ngMockE2E.$httpBackend 1762 | * @description 1763 | * Creates a new backend definition for PATCH requests. For more info see `when()`. 1764 | * 1765 | * @param {string|RegExp} url HTTP url. 1766 | * @param {(string|RegExp)=} data HTTP request body. 1767 | * @param {(Object|function(Object))=} headers HTTP headers. 1768 | * @returns {requestHandler} Returns an object with `respond` and `passThrough` methods that 1769 | * control how a matched request is handled. 1770 | */ 1771 | 1772 | /** 1773 | * @ngdoc method 1774 | * @name ngMockE2E.$httpBackend#whenJSONP 1775 | * @methodOf ngMockE2E.$httpBackend 1776 | * @description 1777 | * Creates a new backend definition for JSONP requests. For more info see `when()`. 1778 | * 1779 | * @param {string|RegExp} url HTTP url. 1780 | * @returns {requestHandler} Returns an object with `respond` and `passThrough` methods that 1781 | * control how a matched request is handled. 1782 | */ 1783 | angular.mock.e2e = {}; 1784 | angular.mock.e2e.$httpBackendDecorator = ['$rootScope', '$delegate', '$browser', createHttpBackendMock]; 1785 | 1786 | 1787 | angular.mock.clearDataCache = function() { 1788 | var key, 1789 | cache = angular.element.cache; 1790 | 1791 | for(key in cache) { 1792 | if (cache.hasOwnProperty(key)) { 1793 | var handle = cache[key].handle; 1794 | 1795 | handle && angular.element(handle.elem).off(); 1796 | delete cache[key]; 1797 | } 1798 | } 1799 | }; 1800 | 1801 | 1802 | 1803 | (window.jasmine || window.mocha) && (function(window) { 1804 | 1805 | var currentSpec = null; 1806 | 1807 | beforeEach(function() { 1808 | currentSpec = this; 1809 | }); 1810 | 1811 | afterEach(function() { 1812 | var injector = currentSpec.$injector; 1813 | 1814 | currentSpec.$injector = null; 1815 | currentSpec.$modules = null; 1816 | currentSpec = null; 1817 | 1818 | if (injector) { 1819 | injector.get('$rootElement').off(); 1820 | injector.get('$browser').pollFns.length = 0; 1821 | } 1822 | 1823 | angular.mock.clearDataCache(); 1824 | 1825 | // clean up jquery's fragment cache 1826 | angular.forEach(angular.element.fragments, function(val, key) { 1827 | delete angular.element.fragments[key]; 1828 | }); 1829 | 1830 | MockXhr.$$lastInstance = null; 1831 | 1832 | angular.forEach(angular.callbacks, function(val, key) { 1833 | delete angular.callbacks[key]; 1834 | }); 1835 | angular.callbacks.counter = 0; 1836 | }); 1837 | 1838 | function isSpecRunning() { 1839 | return currentSpec && (window.mocha || currentSpec.queue.running); 1840 | } 1841 | 1842 | /** 1843 | * @ngdoc function 1844 | * @name angular.mock.module 1845 | * @description 1846 | * 1847 | * *NOTE*: This function is also published on window for easy access.
1848 | * 1849 | * This function registers a module configuration code. It collects the configuration information 1850 | * which will be used when the injector is created by {@link angular.mock.inject inject}. 1851 | * 1852 | * See {@link angular.mock.inject inject} for usage example 1853 | * 1854 | * @param {...(string|Function|Object)} fns any number of modules which are represented as string 1855 | * aliases or as anonymous module initialization functions. The modules are used to 1856 | * configure the injector. The 'ng' and 'ngMock' modules are automatically loaded. If an 1857 | * object literal is passed they will be register as values in the module, the key being 1858 | * the module name and the value being what is returned. 1859 | */ 1860 | window.module = angular.mock.module = function() { 1861 | var moduleFns = Array.prototype.slice.call(arguments, 0); 1862 | return isSpecRunning() ? workFn() : workFn; 1863 | ///////////////////// 1864 | function workFn() { 1865 | if (currentSpec.$injector) { 1866 | throw Error('Injector already created, can not register a module!'); 1867 | } else { 1868 | var modules = currentSpec.$modules || (currentSpec.$modules = []); 1869 | angular.forEach(moduleFns, function(module) { 1870 | if (angular.isObject(module) && !angular.isArray(module)) { 1871 | modules.push(function($provide) { 1872 | angular.forEach(module, function(value, key) { 1873 | $provide.value(key, value); 1874 | }); 1875 | }); 1876 | } else { 1877 | modules.push(module); 1878 | } 1879 | }); 1880 | } 1881 | } 1882 | }; 1883 | 1884 | /** 1885 | * @ngdoc function 1886 | * @name angular.mock.inject 1887 | * @description 1888 | * 1889 | * *NOTE*: This function is also published on window for easy access.
1890 | * 1891 | * The inject function wraps a function into an injectable function. The inject() creates new 1892 | * instance of {@link AUTO.$injector $injector} per test, which is then used for 1893 | * resolving references. 1894 | * 1895 | * See also {@link angular.mock.module module} 1896 | * 1897 | * Example of what a typical jasmine tests looks like with the inject method. 1898 | *
1899 |    *
1900 |    *   angular.module('myApplicationModule', [])
1901 |    *       .value('mode', 'app')
1902 |    *       .value('version', 'v1.0.1');
1903 |    *
1904 |    *
1905 |    *   describe('MyApp', function() {
1906 |    *
1907 |    *     // You need to load modules that you want to test,
1908 |    *     // it loads only the "ng" module by default.
1909 |    *     beforeEach(module('myApplicationModule'));
1910 |    *
1911 |    *
1912 |    *     // inject() is used to inject arguments of all given functions
1913 |    *     it('should provide a version', inject(function(mode, version) {
1914 |    *       expect(version).toEqual('v1.0.1');
1915 |    *       expect(mode).toEqual('app');
1916 |    *     }));
1917 |    *
1918 |    *
1919 |    *     // The inject and module method can also be used inside of the it or beforeEach
1920 |    *     it('should override a version and test the new version is injected', function() {
1921 |    *       // module() takes functions or strings (module aliases)
1922 |    *       module(function($provide) {
1923 |    *         $provide.value('version', 'overridden'); // override version here
1924 |    *       });
1925 |    *
1926 |    *       inject(function(version) {
1927 |    *         expect(version).toEqual('overridden');
1928 |    *       });
1929 |    *     ));
1930 |    *   });
1931 |    *
1932 |    * 
1933 | * 1934 | * @param {...Function} fns any number of functions which will be injected using the injector. 1935 | */ 1936 | window.inject = angular.mock.inject = function() { 1937 | var blockFns = Array.prototype.slice.call(arguments, 0); 1938 | var errorForStack = new Error('Declaration Location'); 1939 | return isSpecRunning() ? workFn() : workFn; 1940 | ///////////////////// 1941 | function workFn() { 1942 | var modules = currentSpec.$modules || []; 1943 | 1944 | modules.unshift('ngMock'); 1945 | modules.unshift('ng'); 1946 | var injector = currentSpec.$injector; 1947 | if (!injector) { 1948 | injector = currentSpec.$injector = angular.injector(modules); 1949 | } 1950 | for(var i = 0, ii = blockFns.length; i < ii; i++) { 1951 | try { 1952 | injector.invoke(blockFns[i] || angular.noop, this); 1953 | } catch (e) { 1954 | if(e.stack && errorForStack) e.stack += '\n' + errorForStack.stack; 1955 | throw e; 1956 | } finally { 1957 | errorForStack = null; 1958 | } 1959 | } 1960 | } 1961 | }; 1962 | })(window); 1963 | -------------------------------------------------------------------------------- /lib/angular-1.2.0-rc.2/angular-resource.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license AngularJS v1.2.0-rc.2 3 | * (c) 2010-2012 Google, Inc. http://angularjs.org 4 | * License: MIT 5 | */ 6 | (function(window, angular, undefined) {'use strict'; 7 | 8 | var $resourceMinErr = angular.$$minErr('$resource'); 9 | 10 | /** 11 | * @ngdoc overview 12 | * @name ngResource 13 | * @description 14 | * 15 | * # ngResource 16 | * 17 | * `ngResource` is the name of the optional Angular module that adds support for interacting with 18 | * [RESTful](http://en.wikipedia.org/wiki/Representational_State_Transfer) server-side data sources. 19 | * `ngReource` provides the {@link ngResource.$resource `$resource`} serivce. 20 | * 21 | * {@installModule resource} 22 | * 23 | * See {@link ngResource.$resource `$resource`} for usage. 24 | */ 25 | 26 | /** 27 | * @ngdoc object 28 | * @name ngResource.$resource 29 | * @requires $http 30 | * 31 | * @description 32 | * A factory which creates a resource object that lets you interact with 33 | * [RESTful](http://en.wikipedia.org/wiki/Representational_State_Transfer) server-side data sources. 34 | * 35 | * The returned resource object has action methods which provide high-level behaviors without 36 | * the need to interact with the low level {@link ng.$http $http} service. 37 | * 38 | * Requires the {@link ngResource `ngResource`} module to be installed. 39 | * 40 | * @param {string} url A parametrized URL template with parameters prefixed by `:` as in 41 | * `/user/:username`. If you are using a URL with a port number (e.g. 42 | * `http://example.com:8080/api`), it will be respected. 43 | * 44 | * If you are using a url with a suffix, just add the suffix, like this: 45 | * `$resource('http://example.com/resource.json')` or `$resource('http://example.com/:id.json')` 46 | * or even `$resource('http://example.com/resource/:resource_id.:format')` 47 | * If the parameter before the suffix is empty, :resource_id in this case, then the `/.` will be 48 | * collapsed down to a single `.`. If you need this sequence to appear and not collapse then you 49 | * can escape it with `/\.`. 50 | * 51 | * @param {Object=} paramDefaults Default values for `url` parameters. These can be overridden in 52 | * `actions` methods. If any of the parameter value is a function, it will be executed every time 53 | * when a param value needs to be obtained for a request (unless the param was overridden). 54 | * 55 | * Each key value in the parameter object is first bound to url template if present and then any 56 | * excess keys are appended to the url search query after the `?`. 57 | * 58 | * Given a template `/path/:verb` and parameter `{verb:'greet', salutation:'Hello'}` results in 59 | * URL `/path/greet?salutation=Hello`. 60 | * 61 | * If the parameter value is prefixed with `@` then the value of that parameter is extracted from 62 | * the data object (useful for non-GET operations). 63 | * 64 | * @param {Object.=} actions Hash with declaration of custom action that should extend the 65 | * default set of resource actions. The declaration should be created in the format of {@link 66 | * ng.$http#Parameters $http.config}: 67 | * 68 | * {action1: {method:?, params:?, isArray:?, headers:?, ...}, 69 | * action2: {method:?, params:?, isArray:?, headers:?, ...}, 70 | * ...} 71 | * 72 | * Where: 73 | * 74 | * - **`action`** – {string} – The name of action. This name becomes the name of the method on your 75 | * resource object. 76 | * - **`method`** – {string} – HTTP request method. Valid methods are: `GET`, `POST`, `PUT`, `DELETE`, 77 | * and `JSONP`. 78 | * - **`params`** – {Object=} – Optional set of pre-bound parameters for this action. If any of the 79 | * parameter value is a function, it will be executed every time when a param value needs to be 80 | * obtained for a request (unless the param was overridden). 81 | * - **`url`** – {string} – action specific `url` override. The url templating is supported just like 82 | * for the resource-level urls. 83 | * - **`isArray`** – {boolean=} – If true then the returned object for this action is an array, see 84 | * `returns` section. 85 | * - **`transformRequest`** – `{function(data, headersGetter)|Array.}` – 86 | * transform function or an array of such functions. The transform function takes the http 87 | * request body and headers and returns its transformed (typically serialized) version. 88 | * - **`transformResponse`** – `{function(data, headersGetter)|Array.}` – 89 | * transform function or an array of such functions. The transform function takes the http 90 | * response body and headers and returns its transformed (typically deserialized) version. 91 | * - **`cache`** – `{boolean|Cache}` – If true, a default $http cache will be used to cache the 92 | * GET request, otherwise if a cache instance built with 93 | * {@link ng.$cacheFactory $cacheFactory}, this cache will be used for 94 | * caching. 95 | * - **`timeout`** – `{number|Promise}` – timeout in milliseconds, or {@link ng.$q promise} that 96 | * should abort the request when resolved. 97 | * - **`withCredentials`** - `{boolean}` - whether to to set the `withCredentials` flag on the 98 | * XHR object. See {@link https://developer.mozilla.org/en/http_access_control#section_5 99 | * requests with credentials} for more information. 100 | * - **`responseType`** - `{string}` - see {@link 101 | * https://developer.mozilla.org/en-US/docs/DOM/XMLHttpRequest#responseType requestType}. 102 | * - **`interceptor`** - `{Object=}` - The interceptor object has two optional methods - 103 | * `response` and `responseError`. Both `response` and `responseError` interceptors get called 104 | * with `http response` object. See {@link ng.$http $http interceptors}. 105 | * 106 | * @returns {Object} A resource "class" object with methods for the default set of resource actions 107 | * optionally extended with custom `actions`. The default set contains these actions: 108 | * 109 | * { 'get': {method:'GET'}, 110 | * 'save': {method:'POST'}, 111 | * 'query': {method:'GET', isArray:true}, 112 | * 'remove': {method:'DELETE'}, 113 | * 'delete': {method:'DELETE'} }; 114 | * 115 | * Calling these methods invoke an {@link ng.$http} with the specified http method, 116 | * destination and parameters. When the data is returned from the server then the object is an 117 | * instance of the resource class. The actions `save`, `remove` and `delete` are available on it 118 | * as methods with the `$` prefix. This allows you to easily perform CRUD operations (create, 119 | * read, update, delete) on server-side data like this: 120 | *
121 |         var User = $resource('/user/:userId', {userId:'@id'});
122 |         var user = User.get({userId:123}, function() {
123 |           user.abc = true;
124 |           user.$save();
125 |         });
126 |      
127 | * 128 | * It is important to realize that invoking a $resource object method immediately returns an 129 | * empty reference (object or array depending on `isArray`). Once the data is returned from the 130 | * server the existing reference is populated with the actual data. This is a useful trick since 131 | * usually the resource is assigned to a model which is then rendered by the view. Having an empty 132 | * object results in no rendering, once the data arrives from the server then the object is 133 | * populated with the data and the view automatically re-renders itself showing the new data. This 134 | * means that in most case one never has to write a callback function for the action methods. 135 | * 136 | * The action methods on the class object or instance object can be invoked with the following 137 | * parameters: 138 | * 139 | * - HTTP GET "class" actions: `Resource.action([parameters], [success], [error])` 140 | * - non-GET "class" actions: `Resource.action([parameters], postData, [success], [error])` 141 | * - non-GET instance actions: `instance.$action([parameters], [success], [error])` 142 | * 143 | * Success callback is called with (value, responseHeaders) arguments. Error callback is called 144 | * with (httpResponse) argument. 145 | * 146 | * Class actions return empty instance (with additional properties below). 147 | * Instance actions return promise of the action. 148 | * 149 | * The Resource instances and collection have these additional properties: 150 | * 151 | * - `$promise`: the {@link ng.$q promise} of the original server interaction that created this 152 | * instance or collection. 153 | * 154 | * On success, the promise is resolved with the same resource instance or collection object, 155 | * updated with data from server. This makes it easy to use in 156 | * {@link ngRoute.$routeProvider resolve section of $routeProvider.when()} to defer view rendering 157 | * until the resource(s) are loaded. 158 | * 159 | * On failure, the promise is resolved with the {@link ng.$http http response} object, 160 | * without the `resource` property. 161 | * 162 | * - `$resolved`: `true` after first server interaction is completed (either with success or rejection), 163 | * `false` before that. Knowing if the Resource has been resolved is useful in data-binding. 164 | * 165 | * @example 166 | * 167 | * # Credit card resource 168 | * 169 | *
170 |      // Define CreditCard class
171 |      var CreditCard = $resource('/user/:userId/card/:cardId',
172 |       {userId:123, cardId:'@id'}, {
173 |        charge: {method:'POST', params:{charge:true}}
174 |       });
175 | 
176 |      // We can retrieve a collection from the server
177 |      var cards = CreditCard.query(function() {
178 |        // GET: /user/123/card
179 |        // server returns: [ {id:456, number:'1234', name:'Smith'} ];
180 | 
181 |        var card = cards[0];
182 |        // each item is an instance of CreditCard
183 |        expect(card instanceof CreditCard).toEqual(true);
184 |        card.name = "J. Smith";
185 |        // non GET methods are mapped onto the instances
186 |        card.$save();
187 |        // POST: /user/123/card/456 {id:456, number:'1234', name:'J. Smith'}
188 |        // server returns: {id:456, number:'1234', name: 'J. Smith'};
189 | 
190 |        // our custom method is mapped as well.
191 |        card.$charge({amount:9.99});
192 |        // POST: /user/123/card/456?amount=9.99&charge=true {id:456, number:'1234', name:'J. Smith'}
193 |      });
194 | 
195 |      // we can create an instance as well
196 |      var newCard = new CreditCard({number:'0123'});
197 |      newCard.name = "Mike Smith";
198 |      newCard.$save();
199 |      // POST: /user/123/card {number:'0123', name:'Mike Smith'}
200 |      // server returns: {id:789, number:'01234', name: 'Mike Smith'};
201 |      expect(newCard.id).toEqual(789);
202 |  * 
203 | * 204 | * The object returned from this function execution is a resource "class" which has "static" method 205 | * for each action in the definition. 206 | * 207 | * Calling these methods invoke `$http` on the `url` template with the given `method`, `params` and `headers`. 208 | * When the data is returned from the server then the object is an instance of the resource type and 209 | * all of the non-GET methods are available with `$` prefix. This allows you to easily support CRUD 210 | * operations (create, read, update, delete) on server-side data. 211 | 212 |
213 |      var User = $resource('/user/:userId', {userId:'@id'});
214 |      var user = User.get({userId:123}, function() {
215 |        user.abc = true;
216 |        user.$save();
217 |      });
218 |    
219 | * 220 | * It's worth noting that the success callback for `get`, `query` and other method gets passed 221 | * in the response that came from the server as well as $http header getter function, so one 222 | * could rewrite the above example and get access to http headers as: 223 | * 224 |
225 |      var User = $resource('/user/:userId', {userId:'@id'});
226 |      User.get({userId:123}, function(u, getResponseHeaders){
227 |        u.abc = true;
228 |        u.$save(function(u, putResponseHeaders) {
229 |          //u => saved user object
230 |          //putResponseHeaders => $http header getter
231 |        });
232 |      });
233 |    
234 | 235 | * # Buzz client 236 | 237 | Let's look at what a buzz client created with the `$resource` service looks like: 238 | 239 | 240 | 260 | 261 |
262 | 263 | 264 |
265 |
266 |

267 | 268 | {{item.actor.name}} 269 | Expand replies: {{item.links.replies[0].count}} 270 |

271 | {{item.object.content | html}} 272 |
273 | 274 | {{reply.actor.name}}: {{reply.content | html}} 275 |
276 |
277 |
278 |
279 | 280 | 281 |
282 | */ 283 | angular.module('ngResource', ['ng']). 284 | factory('$resource', ['$http', '$parse', '$q', function($http, $parse, $q) { 285 | var DEFAULT_ACTIONS = { 286 | 'get': {method:'GET'}, 287 | 'save': {method:'POST'}, 288 | 'query': {method:'GET', isArray:true}, 289 | 'remove': {method:'DELETE'}, 290 | 'delete': {method:'DELETE'} 291 | }; 292 | var noop = angular.noop, 293 | forEach = angular.forEach, 294 | extend = angular.extend, 295 | copy = angular.copy, 296 | isFunction = angular.isFunction, 297 | getter = function(obj, path) { 298 | return $parse(path)(obj); 299 | }; 300 | 301 | /** 302 | * We need our custom method because encodeURIComponent is too aggressive and doesn't follow 303 | * http://www.ietf.org/rfc/rfc3986.txt with regards to the character set (pchar) allowed in path 304 | * segments: 305 | * segment = *pchar 306 | * pchar = unreserved / pct-encoded / sub-delims / ":" / "@" 307 | * pct-encoded = "%" HEXDIG HEXDIG 308 | * unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~" 309 | * sub-delims = "!" / "$" / "&" / "'" / "(" / ")" 310 | * / "*" / "+" / "," / ";" / "=" 311 | */ 312 | function encodeUriSegment(val) { 313 | return encodeUriQuery(val, true). 314 | replace(/%26/gi, '&'). 315 | replace(/%3D/gi, '='). 316 | replace(/%2B/gi, '+'); 317 | } 318 | 319 | 320 | /** 321 | * This method is intended for encoding *key* or *value* parts of query component. We need a custom 322 | * method because encodeURIComponent is too aggressive and encodes stuff that doesn't have to be 323 | * encoded per http://tools.ietf.org/html/rfc3986: 324 | * query = *( pchar / "/" / "?" ) 325 | * pchar = unreserved / pct-encoded / sub-delims / ":" / "@" 326 | * unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~" 327 | * pct-encoded = "%" HEXDIG HEXDIG 328 | * sub-delims = "!" / "$" / "&" / "'" / "(" / ")" 329 | * / "*" / "+" / "," / ";" / "=" 330 | */ 331 | function encodeUriQuery(val, pctEncodeSpaces) { 332 | return encodeURIComponent(val). 333 | replace(/%40/gi, '@'). 334 | replace(/%3A/gi, ':'). 335 | replace(/%24/g, '$'). 336 | replace(/%2C/gi, ','). 337 | replace(/%20/g, (pctEncodeSpaces ? '%20' : '+')); 338 | } 339 | 340 | function Route(template, defaults) { 341 | this.template = template; 342 | this.defaults = defaults || {}; 343 | this.urlParams = {}; 344 | } 345 | 346 | Route.prototype = { 347 | setUrlParams: function(config, params, actionUrl) { 348 | var self = this, 349 | url = actionUrl || self.template, 350 | val, 351 | encodedVal; 352 | 353 | var urlParams = self.urlParams = {}; 354 | forEach(url.split(/\W/), function(param){ 355 | if (!(new RegExp("^\\d+$").test(param)) && param && (new RegExp("(^|[^\\\\]):" + param + "(\\W|$)").test(url))) { 356 | urlParams[param] = true; 357 | } 358 | }); 359 | url = url.replace(/\\:/g, ':'); 360 | 361 | params = params || {}; 362 | forEach(self.urlParams, function(_, urlParam){ 363 | val = params.hasOwnProperty(urlParam) ? params[urlParam] : self.defaults[urlParam]; 364 | if (angular.isDefined(val) && val !== null) { 365 | encodedVal = encodeUriSegment(val); 366 | url = url.replace(new RegExp(":" + urlParam + "(\\W|$)", "g"), encodedVal + "$1"); 367 | } else { 368 | url = url.replace(new RegExp("(\/?):" + urlParam + "(\\W|$)", "g"), function(match, 369 | leadingSlashes, tail) { 370 | if (tail.charAt(0) == '/') { 371 | return tail; 372 | } else { 373 | return leadingSlashes + tail; 374 | } 375 | }); 376 | } 377 | }); 378 | 379 | // strip trailing slashes and set the url 380 | url = url.replace(/\/+$/, ''); 381 | // then replace collapse `/.` if found in the last URL path segment before the query 382 | // E.g. `http://url.com/id./format?q=x` becomes `http://url.com/id.format?q=x` 383 | url = url.replace(/\/\.(?=\w+($|\?))/, '.'); 384 | // replace escaped `/\.` with `/.` 385 | config.url = url.replace(/\/\\\./, '/.'); 386 | 387 | 388 | // set params - delegate param encoding to $http 389 | forEach(params, function(value, key){ 390 | if (!self.urlParams[key]) { 391 | config.params = config.params || {}; 392 | config.params[key] = value; 393 | } 394 | }); 395 | } 396 | }; 397 | 398 | 399 | function ResourceFactory(url, paramDefaults, actions) { 400 | var route = new Route(url); 401 | 402 | actions = extend({}, DEFAULT_ACTIONS, actions); 403 | 404 | function extractParams(data, actionParams){ 405 | var ids = {}; 406 | actionParams = extend({}, paramDefaults, actionParams); 407 | forEach(actionParams, function(value, key){ 408 | if (isFunction(value)) { value = value(); } 409 | ids[key] = value && value.charAt && value.charAt(0) == '@' ? getter(data, value.substr(1)) : value; 410 | }); 411 | return ids; 412 | } 413 | 414 | function defaultResponseInterceptor(response) { 415 | return response.resource; 416 | } 417 | 418 | function Resource(value){ 419 | copy(value || {}, this); 420 | } 421 | 422 | forEach(actions, function(action, name) { 423 | var hasBody = /^(POST|PUT|PATCH)$/i.test(action.method); 424 | 425 | Resource[name] = function(a1, a2, a3, a4) { 426 | var params = {}, data, success, error; 427 | 428 | switch(arguments.length) { 429 | case 4: 430 | error = a4; 431 | success = a3; 432 | //fallthrough 433 | case 3: 434 | case 2: 435 | if (isFunction(a2)) { 436 | if (isFunction(a1)) { 437 | success = a1; 438 | error = a2; 439 | break; 440 | } 441 | 442 | success = a2; 443 | error = a3; 444 | //fallthrough 445 | } else { 446 | params = a1; 447 | data = a2; 448 | success = a3; 449 | break; 450 | } 451 | case 1: 452 | if (isFunction(a1)) success = a1; 453 | else if (hasBody) data = a1; 454 | else params = a1; 455 | break; 456 | case 0: break; 457 | default: 458 | throw $resourceMinErr('badargs', 459 | "Expected up to 4 arguments [params, data, success, error], got {0} arguments", arguments.length); 460 | } 461 | 462 | var isInstanceCall = data instanceof Resource; 463 | var value = isInstanceCall ? data : (action.isArray ? [] : new Resource(data)); 464 | var httpConfig = {}; 465 | var responseInterceptor = action.interceptor && action.interceptor.response || defaultResponseInterceptor; 466 | var responseErrorInterceptor = action.interceptor && action.interceptor.responseError || undefined; 467 | 468 | forEach(action, function(value, key) { 469 | if (key != 'params' && key != 'isArray' && key != 'interceptor') { 470 | httpConfig[key] = copy(value); 471 | } 472 | }); 473 | 474 | httpConfig.data = data; 475 | route.setUrlParams(httpConfig, extend({}, extractParams(data, action.params || {}), params), action.url); 476 | 477 | var promise = $http(httpConfig).then(function(response) { 478 | var data = response.data, 479 | promise = value.$promise; 480 | 481 | if (data) { 482 | if ( angular.isArray(data) != !!action.isArray ) { 483 | throw $resourceMinErr('badcfg', 'Error in resource configuration. Expected response' + 484 | ' to contain an {0} but got an {1}', 485 | action.isArray?'array':'object', angular.isArray(data)?'array':'object'); 486 | } 487 | if (action.isArray) { 488 | value.length = 0; 489 | forEach(data, function(item) { 490 | value.push(new Resource(item)); 491 | }); 492 | } else { 493 | copy(data, value); 494 | value.$promise = promise; 495 | } 496 | } 497 | 498 | value.$resolved = true; 499 | 500 | (success||noop)(value, response.headers); 501 | 502 | response.resource = value; 503 | 504 | return response; 505 | }, function(response) { 506 | value.$resolved = true; 507 | 508 | (error||noop)(response); 509 | 510 | return $q.reject(response); 511 | }).then(responseInterceptor, responseErrorInterceptor); 512 | 513 | 514 | if (!isInstanceCall) { 515 | // we are creating instance / collection 516 | // - set the initial promise 517 | // - return the instance / collection 518 | value.$promise = promise; 519 | value.$resolved = false; 520 | 521 | return value; 522 | } 523 | 524 | // instance call 525 | return promise; 526 | }; 527 | 528 | 529 | Resource.prototype['$' + name] = function(params, success, error) { 530 | if (isFunction(params)) { 531 | error = success; success = params; params = {}; 532 | } 533 | var result = Resource[name](params, this, success, error); 534 | return result.$promise || result; 535 | }; 536 | }); 537 | 538 | Resource.bind = function(additionalParamDefaults){ 539 | return ResourceFactory(url, extend({}, paramDefaults, additionalParamDefaults), actions); 540 | }; 541 | 542 | return Resource; 543 | } 544 | 545 | return ResourceFactory; 546 | }]); 547 | 548 | 549 | })(window, window.angular); 550 | -------------------------------------------------------------------------------- /lib/angular-1.2.0-rc.2/angular-resource.min.js: -------------------------------------------------------------------------------- 1 | /* 2 | AngularJS v1.2.0-rc.2 3 | (c) 2010-2012 Google, Inc. http://angularjs.org 4 | License: MIT 5 | */ 6 | (function(H,g,C){'use strict';var A=g.$$minErr("$resource");g.module("ngResource",["ng"]).factory("$resource",["$http","$parse","$q",function(D,y,E){function n(g,h){this.template=g;this.defaults=h||{};this.urlParams={}}function u(l,h,d){function p(c,b){var e={};b=v({},h,b);q(b,function(a,b){t(a)&&(a=a());var m;a&&a.charAt&&"@"==a.charAt(0)?(m=a.substr(1),m=y(m)(c)):m=a;e[b]=m});return e}function b(c){return c.resource}function e(c){z(c||{},this)}var F=new n(l);d=v({},G,d);q(d,function(c,f){var h= 7 | /^(POST|PUT|PATCH)$/i.test(c.method);e[f]=function(a,f,m,l){var d={},r,s,w;switch(arguments.length){case 4:w=l,s=m;case 3:case 2:if(t(f)){if(t(a)){s=a;w=f;break}s=f;w=m}else{d=a;r=f;s=m;break}case 1:t(a)?s=a:h?r=a:d=a;break;case 0:break;default:throw A("badargs",arguments.length);}var n=r instanceof e,k=n?r:c.isArray?[]:new e(r),x={},u=c.interceptor&&c.interceptor.response||b,y=c.interceptor&&c.interceptor.responseError||C;q(c,function(a,c){"params"!=c&&("isArray"!=c&&"interceptor"!=c)&&(x[c]=z(a))}); 8 | x.data=r;F.setUrlParams(x,v({},p(r,c.params||{}),d),c.url);d=D(x).then(function(a){var b=a.data,f=k.$promise;if(b){if(g.isArray(b)!=!!c.isArray)throw A("badcfg",c.isArray?"array":"object",g.isArray(b)?"array":"object");c.isArray?(k.length=0,q(b,function(a){k.push(new e(a))})):(z(b,k),k.$promise=f)}k.$resolved=!0;(s||B)(k,a.headers);a.resource=k;return a},function(a){k.$resolved=!0;(w||B)(a);return E.reject(a)}).then(u,y);return n?d:(k.$promise=d,k.$resolved=!1,k)};e.prototype["$"+f]=function(a,c, 9 | b){t(a)&&(b=c,c=a,a={});a=e[f](a,this,c,b);return a.$promise||a}});e.bind=function(c){return u(l,v({},h,c),d)};return e}var G={get:{method:"GET"},save:{method:"POST"},query:{method:"GET",isArray:!0},remove:{method:"DELETE"},"delete":{method:"DELETE"}},B=g.noop,q=g.forEach,v=g.extend,z=g.copy,t=g.isFunction;n.prototype={setUrlParams:function(l,h,d){var p=this,b=d||p.template,e,n,c=p.urlParams={};q(b.split(/\W/),function(f){!/^\d+$/.test(f)&&(f&&RegExp("(^|[^\\\\]):"+f+"(\\W|$)").test(b))&&(c[f]=!0)}); 10 | b=b.replace(/\\:/g,":");h=h||{};q(p.urlParams,function(c,d){e=h.hasOwnProperty(d)?h[d]:p.defaults[d];g.isDefined(e)&&null!==e?(n=encodeURIComponent(e).replace(/%40/gi,"@").replace(/%3A/gi,":").replace(/%24/g,"$").replace(/%2C/gi,",").replace(/%20/g,"%20").replace(/%26/gi,"&").replace(/%3D/gi,"=").replace(/%2B/gi,"+"),b=b.replace(RegExp(":"+d+"(\\W|$)","g"),n+"$1")):b=b.replace(RegExp("(/?):"+d+"(\\W|$)","g"),function(a,c,b){return"/"==b.charAt(0)?b:c+b})});b=b.replace(/\/+$/,"");b=b.replace(/\/\.(?=\w+($|\?))/, 11 | ".");l.url=b.replace(/\/\\\./,"/.");q(h,function(c,b){p.urlParams[b]||(l.params=l.params||{},l.params[b]=c)})}};return u}])})(window,window.angular); 12 | /* 13 | //@ sourceMappingURL=angular-resource.min.js.map 14 | */ 15 | -------------------------------------------------------------------------------- /lib/angular-1.2.0-rc.2/angular-resource.min.js.map: -------------------------------------------------------------------------------- 1 | { 2 | "version":3, 3 | "file":"angular-resource.min.js", 4 | "lineCount":11, 5 | "mappings":"A;;;;;aAKC,SAAQ,CAACA,CAAD,CAASC,CAAT,CAAkBC,CAAlB,CAA6B,CAEtC,IAAIC,EAAkBF,CAAAG,SAAA,CAAiB,WAAjB,CAmRtBH,EAAAI,OAAA,CAAe,YAAf,CAA6B,CAAC,IAAD,CAA7B,CAAAC,QAAA,CACU,WADV,CACuB,CAAC,OAAD,CAAU,QAAV,CAAoB,IAApB,CAA0B,QAAQ,CAACC,CAAD,CAAQC,CAAR,CAAgBC,CAAhB,CAAoB,CAwDzEC,QAASA,EAAK,CAACC,CAAD,CAAWC,CAAX,CAAqB,CACjC,IAAAD,SAAA,CAAgBA,CAChB,KAAAC,SAAA,CAAgBA,CAAhB,EAA4B,EAC5B,KAAAC,UAAA,CAAiB,EAHgB,CA2DnCC,QAASA,EAAe,CAACC,CAAD,CAAMC,CAAN,CAAqBC,CAArB,CAA8B,CAKpDC,QAASA,EAAa,CAACC,CAAD,CAAOC,CAAP,CAAoB,CACxC,IAAIC,EAAM,EACVD,EAAA,CAAeE,CAAA,CAAO,EAAP,CAAWN,CAAX,CAA0BI,CAA1B,CACfG,EAAA,CAAQH,CAAR,CAAsB,QAAQ,CAACI,CAAD,CAAQC,CAAR,CAAY,CACpCC,CAAA,CAAWF,CAAX,CAAJ,GAAyBA,CAAzB,CAAiCA,CAAA,EAAjC,CACW,KAAA,CAAAA,EAAA,EAASA,CAAAG,OAAT,EAA4C,GAA5C,EAAyBH,CAAAG,OAAA,CAAa,CAAb,CAAzB,EAAkD,CA/G7D,CA+G6D,CAAA,OAAA,CAAA,CAAA,CA/G7D,CAAA,CAAA,CAAOnB,CAAA,CAAOoB,CAAP,CAAA,CA+GsDC,CA/GtD,CA+GI,EAAkFL,CAAlF,CAAkFA,CAA7FH,EAAA,CAAII,CAAJ,CAAA,CAAW,CAF6B,CAA1C,CAIA,OAAOJ,EAPiC,CAU1CS,QAASA,EAA0B,CAACC,CAAD,CAAW,CAC5C,MAAOA,EAAAC,SADqC,CAI9CC,QAASA,EAAQ,CAACT,CAAD,CAAO,CACtBU,CAAA,CAAKV,CAAL,EAAc,EAAd,CAAkB,IAAlB,CADsB,CAlBxB,IAAIW,EAAQ,IAAIzB,CAAJ,CAAUK,CAAV,CAEZE,EAAA,CAAUK,CAAA,CAAO,EAAP,CAAWc,CAAX,CAA4BnB,CAA5B,CAoBVM,EAAA,CAAQN,CAAR,CAAiB,QAAQ,CAACoB,CAAD,CAASC,CAAT,CAAe,CACtC,IAAIC;AAAU,qBAAAC,KAAA,CAA2BH,CAAAI,OAA3B,CAEdR,EAAA,CAASK,CAAT,CAAA,CAAiB,QAAQ,CAACI,CAAD,CAAKC,CAAL,CAASC,CAAT,CAAaC,CAAb,CAAiB,CAAA,IACpCC,EAAS,EAD2B,CACvB3B,CADuB,CACjB4B,CADiB,CACRC,CAEhC,QAAOC,SAAAC,OAAP,EACA,KAAK,CAAL,CACEF,CACA,CADQH,CACR,CAAAE,CAAA,CAAUH,CAEZ,MAAK,CAAL,CACA,KAAK,CAAL,CACE,GAAIlB,CAAA,CAAWiB,CAAX,CAAJ,CAAoB,CAClB,GAAIjB,CAAA,CAAWgB,CAAX,CAAJ,CAAoB,CAClBK,CAAA,CAAUL,CACVM,EAAA,CAAQL,CACR,MAHkB,CAMpBI,CAAA,CAAUJ,CACVK,EAAA,CAAQJ,CARU,CAApB,IAUO,CACLE,CAAA,CAASJ,CACTvB,EAAA,CAAOwB,CACPI,EAAA,CAAUH,CACV,MAJK,CAMT,KAAK,CAAL,CACMlB,CAAA,CAAWgB,CAAX,CAAJ,CAAoBK,CAApB,CAA8BL,CAA9B,CACSH,CAAJ,CAAapB,CAAb,CAAoBuB,CAApB,CACAI,CADA,CACSJ,CACd,MACF,MAAK,CAAL,CAAQ,KACR,SACE,KAAMvC,EAAA,CAAgB,SAAhB,CAC4E8C,SAAAC,OAD5E,CAAN,CA9BF,CAkCA,IAAIC,EAAiBhC,CAAjBgC,WAAiClB,EAArC,CACIT,EAAQ2B,CAAA,CAAiBhC,CAAjB,CAAyBkB,CAAAe,QAAA,CAAiB,EAAjB,CAAsB,IAAInB,CAAJ,CAAad,CAAb,CAD3D,CAEIkC,EAAa,EAFjB,CAGIC,EAAsBjB,CAAAkB,YAAtBD,EAA4CjB,CAAAkB,YAAAxB,SAA5CuB,EAA2ExB,CAH/E,CAII0B,EAA2BnB,CAAAkB,YAA3BC,EAAiDnB,CAAAkB,YAAAE,cAAjDD,EAAqFtD,CAEzFqB,EAAA,CAAQc,CAAR,CAAgB,QAAQ,CAACb,CAAD,CAAQC,CAAR,CAAa,CACxB,QAAX,EAAIA,CAAJ,GAA8B,SAA9B,EAAuBA,CAAvB,EAAkD,aAAlD,EAA2CA,CAA3C,IACE4B,CAAA,CAAW5B,CAAX,CADF,CACoBS,CAAA,CAAKV,CAAL,CADpB,CADmC,CAArC,CAMA6B;CAAAlC,KAAA,CAAkBA,CAClBgB,EAAAuB,aAAA,CAAmBL,CAAnB,CAA+B/B,CAAA,CAAO,EAAP,CAAWJ,CAAA,CAAcC,CAAd,CAAoBkB,CAAAS,OAApB,EAAqC,EAArC,CAAX,CAAqDA,CAArD,CAA/B,CAA6FT,CAAAtB,IAA7F,CAEI4C,EAAAA,CAAUpD,CAAA,CAAM8C,CAAN,CAAAO,KAAA,CAAuB,QAAQ,CAAC7B,CAAD,CAAW,CAAA,IAClDZ,EAAOY,CAAAZ,KAD2C,CAElDwC,EAAUnC,CAAAqC,SAEd,IAAI1C,CAAJ,CAAU,CACR,GAAKlB,CAAAmD,QAAA,CAAgBjC,CAAhB,CAAL,EAA8B,CAAC,CAACkB,CAAAe,QAAhC,CACE,KAAMjD,EAAA,CAAgB,QAAhB,CAEJkC,CAAAe,QAAA,CAAe,OAAf,CAAuB,QAFnB,CAE6BnD,CAAAmD,QAAA,CAAgBjC,CAAhB,CAAA,CAAsB,OAAtB,CAA8B,QAF3D,CAAN,CAIEkB,CAAAe,QAAJ,EACE5B,CAAA0B,OACA,CADe,CACf,CAAA3B,CAAA,CAAQJ,CAAR,CAAc,QAAQ,CAAC2C,CAAD,CAAO,CAC3BtC,CAAAuC,KAAA,CAAW,IAAI9B,CAAJ,CAAa6B,CAAb,CAAX,CAD2B,CAA7B,CAFF,GAME5B,CAAA,CAAKf,CAAL,CAAWK,CAAX,CACA,CAAAA,CAAAqC,SAAA,CAAiBF,CAPnB,CANQ,CAiBVnC,CAAAwC,UAAA,CAAkB,CAAA,CAEjB,EAAAjB,CAAA,EAASkB,CAAT,EAAezC,CAAf,CAAsBO,CAAAmC,QAAtB,CAEDnC,EAAAC,SAAA,CAAoBR,CAEpB,OAAOO,EA3B+C,CAA1C,CA4BX,QAAQ,CAACA,CAAD,CAAW,CACpBP,CAAAwC,UAAA,CAAkB,CAAA,CAEjB,EAAAhB,CAAA,EAAOiB,CAAP,EAAalC,CAAb,CAED,OAAOtB,EAAA0D,OAAA,CAAUpC,CAAV,CALa,CA5BR,CAAA6B,KAAA,CAkCNN,CAlCM,CAkCeE,CAlCf,CAqCd,OAAKL,EAAL,CAWOQ,CAXP,EAIEnC,CAAAqC,SAGOrC,CAHUmC,CAGVnC,CAFPA,CAAAwC,UAEOxC,CAFW,CAAA,CAEXA,CAAAA,CAPT,CAzFwC,CAwG1CS,EAAAmC,UAAA,CAAmB,GAAnB,CAAyB9B,CAAzB,CAAA,CAAiC,QAAQ,CAACQ,CAAD,CAASC,CAAT;AAAkBC,CAAlB,CAAyB,CAC5DtB,CAAA,CAAWoB,CAAX,CAAJ,GACEE,CAAmC,CAA3BD,CAA2B,CAAlBA,CAAkB,CAARD,CAAQ,CAAAA,CAAA,CAAS,EAD9C,CAGIuB,EAAAA,CAASpC,CAAA,CAASK,CAAT,CAAA,CAAeQ,CAAf,CAAuB,IAAvB,CAA6BC,CAA7B,CAAsCC,CAAtC,CACb,OAAOqB,EAAAR,SAAP,EAA0BQ,CALsC,CA3G5B,CAAxC,CAoHApC,EAAAqC,KAAA,CAAgBC,QAAQ,CAACC,CAAD,CAAyB,CAC/C,MAAO1D,EAAA,CAAgBC,CAAhB,CAAqBO,CAAA,CAAO,EAAP,CAAWN,CAAX,CAA0BwD,CAA1B,CAArB,CAAyEvD,CAAzE,CADwC,CAIjD,OAAOgB,EA/I6C,CAlHtD,IAAIG,EAAkB,KACV,QAAQ,KAAR,CADU,MAEV,QAAQ,MAAR,CAFU,OAGV,QAAQ,KAAR,SAAuB,CAAA,CAAvB,CAHU,QAIV,QAAQ,QAAR,CAJU,CAKpB,QALoB,CAKV,QAAQ,QAAR,CALU,CAAtB,CAOI6B,EAAOhE,CAAAgE,KAPX,CAQI1C,EAAUtB,CAAAsB,QARd,CASID,EAASrB,CAAAqB,OATb,CAUIY,EAAOjC,CAAAiC,KAVX,CAWIR,EAAazB,CAAAyB,WAkDjBhB,EAAA0D,UAAA,CAAkB,cACFV,QAAQ,CAACe,CAAD,CAAS3B,CAAT,CAAiB4B,CAAjB,CAA4B,CAAA,IAC5CC,EAAO,IADqC,CAE5C5D,EAAM2D,CAAN3D,EAAmB4D,CAAAhE,SAFyB,CAG5CiE,CAH4C,CAI5CC,CAJ4C,CAM5ChE,EAAY8D,CAAA9D,UAAZA,CAA6B,EACjCU,EAAA,CAAQR,CAAA+D,MAAA,CAAU,IAAV,CAAR,CAAyB,QAAQ,CAACC,CAAD,CAAO,CAChC,CAAA,OAAAvC,KAAA,CAA0BuC,CAA1B,CAAN,GAA2CA,CAA3C,EAAyDC,MAAJ,CAAW,cAAX,CAA4BD,CAA5B,CAAoC,SAApC,CAAAvC,KAAA,CAAoDzB,CAApD,CAArD,IACIF,CAAA,CAAUkE,CAAV,CADJ,CACuB,CAAA,CADvB,CADsC,CAAxC,CAKAhE;CAAA,CAAMA,CAAAkE,QAAA,CAAY,MAAZ,CAAoB,GAApB,CAENnC,EAAA,CAASA,CAAT,EAAmB,EACnBvB,EAAA,CAAQoD,CAAA9D,UAAR,CAAwB,QAAQ,CAACqE,CAAD,CAAIC,CAAJ,CAAa,CAC3CP,CAAA,CAAM9B,CAAAsC,eAAA,CAAsBD,CAAtB,CAAA,CAAkCrC,CAAA,CAAOqC,CAAP,CAAlC,CAAqDR,CAAA/D,SAAA,CAAcuE,CAAd,CACvDlF,EAAAoF,UAAA,CAAkBT,CAAlB,CAAJ,EAAsC,IAAtC,GAA8BA,CAA9B,EACEC,CACA,CAlCCS,kBAAA,CAiC6BV,CAjC7B,CAAAK,QAAA,CACG,OADH,CACY,GADZ,CAAAA,QAAA,CAEG,OAFH,CAEY,GAFZ,CAAAA,QAAA,CAGG,MAHH,CAGW,GAHX,CAAAA,QAAA,CAIG,OAJH,CAIY,GAJZ,CAAAA,QAAA,CAKG,MALH,CAK8B,KAL9B,CAnBAA,QAAA,CACG,OADH,CACY,GADZ,CAAAA,QAAA,CAEG,OAFH,CAEY,GAFZ,CAAAA,QAAA,CAGG,OAHH,CAGY,GAHZ,CAqDD,CAAAlE,CAAA,CAAMA,CAAAkE,QAAA,CAAgBD,MAAJ,CAAW,GAAX,CAAiBG,CAAjB,CAA4B,SAA5B,CAAuC,GAAvC,CAAZ,CAAyDN,CAAzD,CAAsE,IAAtE,CAFR,EAIE9D,CAJF,CAIQA,CAAAkE,QAAA,CAAgBD,MAAJ,CAAW,OAAX,CAAsBG,CAAtB,CAAiC,SAAjC,CAA4C,GAA5C,CAAZ,CAA8D,QAAQ,CAACI,CAAD,CACxEC,CADwE,CACxDC,CADwD,CAClD,CACxB,MAAsB,GAAtB,EAAIA,CAAA9D,OAAA,CAAY,CAAZ,CAAJ,CACS8D,CADT,CAGSD,CAHT,CAG0BC,CAJF,CADpB,CANmC,CAA7C,CAkBA1E,EAAA,CAAMA,CAAAkE,QAAA,CAAY,MAAZ,CAAoB,EAApB,CAGNlE,EAAA,CAAMA,CAAAkE,QAAA,CAAY,mBAAZ;AAAiC,GAAjC,CAENR,EAAA1D,IAAA,CAAaA,CAAAkE,QAAA,CAAY,QAAZ,CAAsB,IAAtB,CAIb1D,EAAA,CAAQuB,CAAR,CAAgB,QAAQ,CAACtB,CAAD,CAAQC,CAAR,CAAY,CAC7BkD,CAAA9D,UAAA,CAAeY,CAAf,CAAL,GACEgD,CAAA3B,OACA,CADgB2B,CAAA3B,OAChB,EADiC,EACjC,CAAA2B,CAAA3B,OAAA,CAAcrB,CAAd,CAAA,CAAqBD,CAFvB,CADkC,CAApC,CA1CgD,CADlC,CAuMlB,OAAOV,EArQkE,CAAtD,CADvB,CArRsC,CAArC,CAAA,CA+hBEd,MA/hBF,CA+hBUA,MAAAC,QA/hBV;", 6 | "sources":["angular-resource.js"], 7 | "names":["window","angular","undefined","$resourceMinErr","$$minErr","module","factory","$http","$parse","$q","Route","template","defaults","urlParams","ResourceFactory","url","paramDefaults","actions","extractParams","data","actionParams","ids","extend","forEach","value","key","isFunction","charAt","path","obj","defaultResponseInterceptor","response","resource","Resource","copy","route","DEFAULT_ACTIONS","action","name","hasBody","test","method","a1","a2","a3","a4","params","success","error","arguments","length","isInstanceCall","isArray","httpConfig","responseInterceptor","interceptor","responseErrorInterceptor","responseError","setUrlParams","promise","then","$promise","item","push","$resolved","noop","headers","reject","prototype","result","bind","Resource.bind","additionalParamDefaults","config","actionUrl","self","val","encodedVal","split","param","RegExp","replace","_","urlParam","hasOwnProperty","isDefined","encodeURIComponent","match","leadingSlashes","tail"] 8 | } 9 | -------------------------------------------------------------------------------- /lib/angular-1.2.0-rc.2/errors.json: -------------------------------------------------------------------------------- 1 | {"id":"ng","generated":"Wed Sep 04 2013 14:51:13 GMT+0200 (CEST)","errors":{"$cacheFactory":{"iid":"CacheId '{0}' is already taken!"},"ngModel":{"nonassign":"Expression '{0}' is non-assignable. Element: {1}"},"$sce":{"iequirks":"Strict Contextual Escaping does not support Internet Explorer version < 9 in quirks mode. You can fix this by adding the text to the top of your HTML document. See http://docs.angularjs.org/api/ng.$sce for more information.","insecurl":"Blocked loading resource from url not allowed by $sceDelegate policy. URL: {0}","icontext":"Attempted to trust a value in invalid context. Context: {0}; Value: {1}","itype":"Attempted to trust a non-string value in a content requiring a string: Context: {0}","unsafe":"Attempting to use an unsafe value in a safe context."},"$controller":{"noscp":"Cannot export controller '{0}' as '{1}'! No $scope object provided via `locals`."},"$compile":{"nodomevents":"Interpolations for HTML DOM event attributes are disallowed. Please use the ng- versions (such as ng-click instead of onclick) instead.","multidir":"Multiple directives [{0}, {1}] asking for {2} on: {3}","nonassign":"Expression '{0}' used with directive '{1}' is non-assignable!","tplrt":"Template for directive '{0}' must have exactly one root element. {1}","selmulti":"Binding to the 'multiple' attribute is not supported. Element: {0}","tpload":"Failed to load template: {0}","iscp":"Invalid isolate scope definition for directive '{0}'. Definition: {... {1}: '{2}' ...}","ctreq":"Controller '{0}', required by directive '{1}', can't be found!","uterdir":"Unterminated attribute, found '{0}' but no matching '{1}' found."},"$injector":{"modulerr":"Failed to instantiate module {0} due to:\n{1}","unpr":"Unknown provider: {0}","itkn":"Incorrect injection token! Expected service name as string, got {0}","cdep":"Circular dependency found: {0}","nomod":"Module '{0}' is not available! You either misspelled the module name or forgot to load it. If registering a module ensure that you specify the dependencies as the second argument.","pget":"Provider '{0}' must define $get factory method."},"$rootScope":{"inprog":"{0} already in progress","infdig":"{0} $digest() iterations reached. Aborting!\nWatchers fired in the last 5 iterations: {1}"},"ngPattern":{"noregexp":"Expected {0} to be a RegExp but was {1}. Element: {2}"},"$interpolate":{"noconcat":"Error while interpolating: {0}\nStrict Contextual Escaping disallows interpolations that concatenate multiple expressions when a trusted value is required. See http://docs.angularjs.org/api/ng.$sce","interr":"Can't interpolate: {0}\n{1}"},"jqLite":{"offargs":"jqLite#off() does not support the `selector` argument","onargs":"jqLite#on() does not support the `selector` or `eventData` parameters","nosel":"Looking up elements via selectors is not supported by jqLite! See: http://docs.angularjs.org/api/angular.element"},"ngOptions":{"iexp":"Expected expression in form of '_select_ (as _label_)? for (_key_,)?_value_ in _collection_' but got '{0}'. Element: {1}"},"ngRepeat":{"iidexp":"'_item_' in '_item_ in _collection_' should be an identifier or '(_key_, _value_)' expression, but got '{0}'.","dupes":"Duplicates in a repeater are not allowed. Use 'track by' expression to specify unique keys. Repeater: {0}, Duplicate key: {1}","iexp":"Expected expression in form of '_item_ in _collection_[ track by _id_]' but got '{0}'."},"ng":{"areq":"Argument '{0}' is {1}","cpws":"Can't copy! Making copies of Window or Scope instances is not supported.","btstrpd":"App Already Bootstrapped with this Element '{0}'","cpi":"Can't copy! Source and destination are identical."},"$animate":{"notcsel":"Expecting class selector starting with '.' got '{0}'."},"$parse":{"isecfld":"Referencing \"constructor\" field in Angular expressions is disallowed! Expression: {0}","syntax":"Syntax Error: Token '{0}' {1} at column {2} of the expression [{3}] starting at [{4}].","lexerr":"Lexer Error: {0} at column{1} in expression [{2}].","ueoe":"Unexpected end of expression: {0}","isecfn":"Referencing Function in Angular expressions is disallowed! Expression: {0}"},"$httpBackend":{"noxhr":"This browser does not support XMLHttpRequest."},"$location":{"ipthprfx":"Invalid url \"{0}\", missing path prefix \"{1}\".","isrcharg":"The first argument of the `$location#search()` call must be a string or an object.","ihshprfx":"Invalid url \"{0}\", missing hash prefix \"{1}\"."},"$resource":{"badargs":"Expected up to 4 arguments [params, data, success, error], got {0} arguments","badcfg":"Error in resource configuration. Expected response to contain an {0} but got an {1}"},"$sanitize":{"badparse":"The sanitizer was unable to parse the following block of html: {0}"}}} -------------------------------------------------------------------------------- /lib/angular-1.2.0-rc.2/version.json: -------------------------------------------------------------------------------- 1 | {"full":"1.2.0-rc.2","major":"1","minor":"2","dot":"0","codename":"barehand-atomsplitting","cdn":"1.2.0rc1"} -------------------------------------------------------------------------------- /lib/angular-1.2.0-rc.2/version.txt: -------------------------------------------------------------------------------- 1 | 1.2.0-rc.2 -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ng-http-circuitbreaker", 3 | "version": "0.1.0", 4 | "repository": { 5 | "type": "git", 6 | "url": "https://github.com/mikepugh/ng-http-circuitbreaker" 7 | }, 8 | "keywords": [ 9 | "angularjs", 10 | "circuit breaker", 11 | "fail fast" 12 | ], 13 | "devDependencies": { 14 | "grunt": "~0.4.1", 15 | "grunt-karma": "~0.6.2", 16 | "karma-script-launcher": "~0.1.0", 17 | "karma-firefox-launcher": "~0.1.0", 18 | "karma-chrome-launcher": "~0.1.0", 19 | "karma-jasmine": "~0.1.3", 20 | "karma-phantomjs-launcher": "~0.1.0", 21 | "karma-coverage": "~0.1.0", 22 | "karma": "~0.10.2", 23 | "grunt-contrib-uglify": "~0.2.5", 24 | "grunt-contrib-jshint": "~0.7.1", 25 | "grunt-contrib-copy": "~0.4.1" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /test/karma-unit.conf.js: -------------------------------------------------------------------------------- 1 | module.exports = function(config) { 2 | config.set({ 3 | files : [ 4 | 'lib/angular-1.2.0-rc.2/angular.js', 5 | 'lib/angular-1.2.0-rc.2/angular-mocks.js', 6 | 'app/scripts/ng-http-circuitbreaker.js', 7 | 'test/unit/**/*.js' 8 | ], 9 | basePath: '../', 10 | frameworks: ['jasmine'], 11 | reporters: ['progress'], 12 | browsers: ['PhantomJS'], 13 | autoWatch: false, 14 | singleRun: true, 15 | colors: true 16 | }); 17 | }; 18 | -------------------------------------------------------------------------------- /test/unit/ng-http-circuitbreakerSpec.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by HPugh on 10/31/13. 3 | */ 4 | 5 | describe('ng-http-circuitbreaker > ', function() { 6 | 7 | var $http; 8 | 9 | var stableSpy = { 10 | success: function() {}, 11 | failure: function() {}, 12 | then: function() {} 13 | }; 14 | 15 | var unstableSpy = { 16 | success: function() {}, 17 | failure: function() {}, 18 | then: function() {} 19 | } 20 | 21 | 22 | 23 | var createFailedHttpCalls = function createFailedHttpCalls(n) { 24 | for(var i = 0; i < n; i++) { 25 | $http.get('/api/unstable') 26 | .success(unstableSpy.success) 27 | .error(unstableSpy.failure); 28 | } 29 | }; 30 | 31 | var createSuccessfulHttpCalls = function createSuccessfulHttpCalls(n) { 32 | for(var i = 0; i < n; i++) { 33 | $http.get('/api/stable') 34 | .success(stableSpy.success) 35 | .error(stableSpy.failure); 36 | } 37 | }; 38 | 39 | var createAuthHttpCalls = function createAuthHttpCalls(n) { 40 | for(var i = 0; i < n; i++) { 41 | $http.get('/api/auth') 42 | .success(stableSpy.success) 43 | .error(stableSpy.failure); 44 | } 45 | }; 46 | 47 | 48 | describe('circuit provider > ', function() { 49 | 50 | beforeEach(module('ng-http-circuitbreaker', function($provide, $httpProvider, ngHttpCircuitBreakerConfigProvider) { 51 | $provide.provider('ngHttpCircuitBreakerConfig', ngHttpCircuitBreakerConfigProvider); 52 | $provide.provider('$http', $httpProvider); 53 | })); 54 | 55 | it('should inject ngHttpCircuitBreakerConfig with default setup', inject(function(ngHttpCircuitBreakerConfig) { 56 | expect(ngHttpCircuitBreakerConfig.circuits).toBeDefined(); 57 | 58 | expect(ngHttpCircuitBreakerConfig.circuits.length).toBe(0); 59 | })); 60 | 61 | //endPointRegEx, failureLimit, responseSLA, timeUntilHalfOpen, statusCodesToIgnore 62 | 63 | it('should allow single circuit definition', function() { 64 | module(function(ngHttpCircuitBreakerConfigProvider) { 65 | ngHttpCircuitBreakerConfigProvider 66 | .circuit({endPointRegEx: /^\/api\//i, failureLimit: 5, responseSLA: 500, timeUntilHalfOpen: 5000, statusCodesToIgnore: [401,403,409]}); 67 | }); 68 | inject(function(ngHttpCircuitBreakerConfig) { 69 | expect(ngHttpCircuitBreakerConfig.circuits).toBeDefined(); 70 | expect(ngHttpCircuitBreakerConfig.circuits.length).toBe(1); 71 | expect(ngHttpCircuitBreakerConfig.circuits[0].failureLimit).toBe(5); 72 | expect(ngHttpCircuitBreakerConfig.circuits[0].STATE).toBe(0); 73 | expect(ngHttpCircuitBreakerConfig.circuits[0].statusCodesToIgnore).toBeDefined(); 74 | expect(ngHttpCircuitBreakerConfig.circuits[0].statusCodesToIgnore).toContain(401); 75 | expect(ngHttpCircuitBreakerConfig.circuits[0].statusCodesToIgnore).toContain(403); 76 | expect(ngHttpCircuitBreakerConfig.circuits[0].statusCodesToIgnore).toContain(409); 77 | }); 78 | 79 | 80 | }); 81 | 82 | it('should allow multiple circuit definitions', function() { 83 | module(function(ngHttpCircuitBreakerConfigProvider) { 84 | ngHttpCircuitBreakerConfigProvider 85 | .circuit({endPointRegEx: /^\/api\//i, failureLimit: 5, responseSLA: 500, timeUntilHalfOpen: 5000, statusCodesToIgnore: [401,403,409]}) 86 | .circuit({endPointRegEx: /^\/api2\//i, failureLimit: 10, responseSLA: 1000, timeUntilHalfOpen: 7500, statusCodesToIgnore: [401,403]}); 87 | }); 88 | inject(function(ngHttpCircuitBreakerConfig) { 89 | expect(ngHttpCircuitBreakerConfig.circuits).toBeDefined(); 90 | expect(ngHttpCircuitBreakerConfig.circuits.length).toBe(2); 91 | expect(ngHttpCircuitBreakerConfig.circuits[0].failureLimit).toBe(5); 92 | expect(ngHttpCircuitBreakerConfig.circuits[0].STATE).toBe(0); 93 | expect(ngHttpCircuitBreakerConfig.circuits[0].statusCodesToIgnore).toBeDefined(); 94 | expect(ngHttpCircuitBreakerConfig.circuits[0].statusCodesToIgnore).toContain(401); 95 | expect(ngHttpCircuitBreakerConfig.circuits[0].statusCodesToIgnore).toContain(403); 96 | expect(ngHttpCircuitBreakerConfig.circuits[0].statusCodesToIgnore).toContain(409); 97 | expect(ngHttpCircuitBreakerConfig.circuits[1].failureLimit).toBe(10); 98 | expect(ngHttpCircuitBreakerConfig.circuits[1].STATE).toBe(0); 99 | expect(ngHttpCircuitBreakerConfig.circuits[1].statusCodesToIgnore).toBeDefined(); 100 | expect(ngHttpCircuitBreakerConfig.circuits[1].statusCodesToIgnore).toContain(401); 101 | expect(ngHttpCircuitBreakerConfig.circuits[1].statusCodesToIgnore).toContain(403); 102 | expect(ngHttpCircuitBreakerConfig.circuits[1].statusCodesToIgnore).toNotContain(409); 103 | }); 104 | 105 | 106 | }); 107 | 108 | it('should provide default status codes to ignore', function() { 109 | module(function(ngHttpCircuitBreakerConfigProvider) { 110 | ngHttpCircuitBreakerConfigProvider 111 | .circuit({endPointRegEx: /^\/api\//i, failureLimit: 5, responseSLA: 500, timeUntilHalfOpen: 5000}); 112 | }); 113 | inject(function(ngHttpCircuitBreakerConfig) { 114 | expect(ngHttpCircuitBreakerConfig.circuits).toBeDefined(); 115 | expect(ngHttpCircuitBreakerConfig.circuits.length).toBe(1); 116 | expect(ngHttpCircuitBreakerConfig.circuits[0].failureLimit).toBe(5); 117 | expect(ngHttpCircuitBreakerConfig.circuits[0].STATE).toBe(0); 118 | expect(ngHttpCircuitBreakerConfig.circuits[0].statusCodesToIgnore).toBeDefined(); 119 | expect(ngHttpCircuitBreakerConfig.circuits[0].statusCodesToIgnore).toContain(401); 120 | expect(ngHttpCircuitBreakerConfig.circuits[0].statusCodesToIgnore).toContain(403); 121 | expect(ngHttpCircuitBreakerConfig.circuits[0].statusCodesToIgnore).toContain(409); 122 | }); 123 | 124 | 125 | }); 126 | 127 | it('should allow explicit listening for all status codes', function() { 128 | module(function(ngHttpCircuitBreakerConfigProvider) { 129 | ngHttpCircuitBreakerConfigProvider 130 | .circuit({endPointRegEx: /^\/api\//i, failureLimit: 5, responseSLA: 500, timeUntilHalfOpen: 5000, statusCodesToIgnore: []}); 131 | }); 132 | inject(function(ngHttpCircuitBreakerConfig) { 133 | expect(ngHttpCircuitBreakerConfig.circuits).toBeDefined(); 134 | expect(ngHttpCircuitBreakerConfig.circuits.length).toBe(1); 135 | expect(ngHttpCircuitBreakerConfig.circuits[0].failureLimit).toBe(5); 136 | expect(ngHttpCircuitBreakerConfig.circuits[0].STATE).toBe(0); 137 | expect(ngHttpCircuitBreakerConfig.circuits[0].statusCodesToIgnore).toBeDefined(); 138 | expect(ngHttpCircuitBreakerConfig.circuits[0].statusCodesToIgnore.length).toBe(0); 139 | }); 140 | 141 | }); 142 | 143 | it('should require status codes to ignore be an array', function() { 144 | module(function(ngHttpCircuitBreakerConfigProvider) { 145 | expect(ngHttpCircuitBreakerConfigProvider 146 | .circuit({endPointRegEx: /^\/api\//i, statusCodesToIgnore: 401})).toThrow('Invalid statusCodesToIgnore - expecting array of integer values representing HTTP response codes'); 147 | }); 148 | }); 149 | 150 | it('should not allow a zero failure limit', function() { 151 | module(function(ngHttpCircuitBreakerConfigProvider) { 152 | expect(ngHttpCircuitBreakerConfigProvider 153 | .circuit({endPointRegEx: /^\/api\//i, failureLimit: 0, responseSLA: 500, timeUntilHalfOpen:5000, statusCodesToIgnore: []})).toThrow('Invalid failure limit - must be positive, non-zero value'); 154 | }); 155 | }); 156 | 157 | it('should not allow a negative failure limit', function() { 158 | module(function(ngHttpCircuitBreakerConfigProvider) { 159 | expect(ngHttpCircuitBreakerConfigProvider 160 | .circuit({endPointRegEx: /^\/api\//i, failureLimit: -1, responseSLA: 500, timeUntilHalfOpen:5000, statusCodesToIgnore: []})).toThrow('Invalid failure limit - must be positive, non-zero value'); 161 | }); 162 | }); 163 | 164 | it('should not allow a non-numeric failure limit', function() { 165 | module(function(ngHttpCircuitBreakerConfigProvider) { 166 | expect(ngHttpCircuitBreakerConfigProvider 167 | .circuit({endPointRegEx: /^\/api\//i, failureLimit: 'bad'})).toThrow('Invalid failure limit - must be positive, non-zero value'); 168 | }); 169 | }); 170 | 171 | it('should not allow a negative response sla', function() { 172 | module(function(ngHttpCircuitBreakerConfigProvider) { 173 | expect(ngHttpCircuitBreakerConfigProvider 174 | .circuit({endPointRegEx: /^\/api\//i, responseSLA: -250})).toThrow('Invalid Response SLA - must be non-negative. Set to 0 if you do not want a response sla to be set.'); 175 | }); 176 | }); 177 | 178 | it('should not allow a non-numeric response sla', function() { 179 | module(function(ngHttpCircuitBreakerConfigProvider) { 180 | expect(ngHttpCircuitBreakerConfigProvider 181 | .circuit({endPointRegEx: /^\/api\//i, responseSLA: 'bad'})).toThrow('Invalid Response SLA - must be non-negative. Set to 0 if you do not want a response sla to be set.'); 182 | }); 183 | }); 184 | 185 | it('should allow user to explicitly bypass response SLAs', function() { 186 | module(function(ngHttpCircuitBreakerConfigProvider) { 187 | ngHttpCircuitBreakerConfigProvider 188 | .circuit({endPointRegEx: /^\/api\//i, responseSLA: 0}); 189 | }); 190 | inject(function(ngHttpCircuitBreakerConfig) { 191 | expect(ngHttpCircuitBreakerConfig.circuits[0].failureLimit).toBe(5); 192 | expect(ngHttpCircuitBreakerConfig.circuits[0].responseSLA).toBe(0); 193 | expect(ngHttpCircuitBreakerConfig.circuits[0].timeUntilHalfOpen).toBe(5000); 194 | expect(ngHttpCircuitBreakerConfig.circuits[0].statusCodesToIgnore.length).toBe(3); 195 | expect(ngHttpCircuitBreakerConfig.circuits[0].statusCodesToIgnore.indexOf(401)).not.toBe(-1); 196 | expect(ngHttpCircuitBreakerConfig.circuits[0].statusCodesToIgnore.indexOf(403)).not.toBe(-1); 197 | expect(ngHttpCircuitBreakerConfig.circuits[0].statusCodesToIgnore.indexOf(409)).not.toBe(-1); 198 | }); 199 | }); 200 | 201 | it('should not allow a negative time until half open', function() { 202 | module(function(ngHttpCircuitBreakerConfigProvider) { 203 | expect(ngHttpCircuitBreakerConfigProvider 204 | .circuit({endPointRegEx: /^\/api\//i, timeUntilHalfOpen: -5000})).toThrow('Invalid Circuit timeUntilHalfOpen - must be a positive non-zero integer value'); 205 | }); 206 | }); 207 | 208 | it('should not allow a non-numeric time until half open', function() { 209 | module(function(ngHttpCircuitBreakerConfigProvider) { 210 | expect(ngHttpCircuitBreakerConfigProvider 211 | .circuit({endPointRegEx: /^\/api\//i, timeUntilHalfOpen: 'bad'})).toThrow('Invalid Circuit timeUntilHalfOpen - must be a positive non-zero integer value'); 212 | }); 213 | }); 214 | 215 | it('should not allow a zero time until half open', function() { 216 | module(function(ngHttpCircuitBreakerConfigProvider) { 217 | expect(ngHttpCircuitBreakerConfigProvider 218 | .circuit({endPointRegEx: /^\/api\//i, timeUntilHalfOpen: 0})).toThrow('Invalid Circuit timeUntilHalfOpen - must be a positive non-zero integer value'); 219 | }); 220 | }); 221 | 222 | it('should provide default values for all configs except for endpoint', function() { 223 | module(function(ngHttpCircuitBreakerConfigProvider) { 224 | ngHttpCircuitBreakerConfigProvider 225 | .circuit({endPointRegEx: /^\/api\//i}); 226 | }); 227 | 228 | inject(function(ngHttpCircuitBreakerConfig) { 229 | expect(ngHttpCircuitBreakerConfig.circuits[0].failureLimit).toBe(5); 230 | expect(ngHttpCircuitBreakerConfig.circuits[0].responseSLA).toBe(500); 231 | expect(ngHttpCircuitBreakerConfig.circuits[0].timeUntilHalfOpen).toBe(5000); 232 | expect(ngHttpCircuitBreakerConfig.circuits[0].statusCodesToIgnore.length).toBe(3); 233 | expect(ngHttpCircuitBreakerConfig.circuits[0].statusCodesToIgnore.indexOf(401)).not.toBe(-1); 234 | expect(ngHttpCircuitBreakerConfig.circuits[0].statusCodesToIgnore.indexOf(403)).not.toBe(-1); 235 | expect(ngHttpCircuitBreakerConfig.circuits[0].statusCodesToIgnore.indexOf(409)).not.toBe(-1); 236 | }); 237 | }); 238 | 239 | it('should not allow identical circuit endpoints', function() { 240 | module(function(ngHttpCircuitBreakerConfigProvider) { 241 | expect(ngHttpCircuitBreakerConfigProvider 242 | .circuit({endPointRegEx: /^\/api\//i, failureLimit: 5, responseSLA: 500, timeUntilHalfOpen: 5000, statusCodesToIgnore: [401,403,409]}) 243 | .circuit({endPointRegEx: /^\/api\//i, failureLimit: 5, responseSLA: 500, timeUntilHalfOpen: 5000, statusCodesToIgnore: [401,403,409]}) 244 | ).toThrow('Duplicate endpoint regular expression found'); 245 | }); 246 | }) 247 | 248 | }); 249 | 250 | describe('circuit stats > ', function() { 251 | var $httpBackend, 252 | $rootScope, 253 | $timeout, 254 | ckgConfig, 255 | ckt, 256 | cktStats; 257 | 258 | beforeEach(module('ng-http-circuitbreaker', function($provide, ngHttpCircuitBreakerConfigProvider) { 259 | $provide.provider('ngHttpCircuitBreakerConfig', ngHttpCircuitBreakerConfigProvider); 260 | //$provide.provider('$http', $httpProvider); 261 | })); 262 | beforeEach( 263 | module(function(ngHttpCircuitBreakerConfigProvider, $httpProvider) { 264 | ngHttpCircuitBreakerConfigProvider.circuit({endPointRegEx: /^\/api\//i, failureLimit: 5, responseSLA: 500, timeUntilHalfOpen: 5000, statusCodesToIgnore: [401,403,409]}); 265 | $httpProvider.interceptors.push('ngHttpCircuitBreaker'); 266 | })); 267 | 268 | beforeEach(inject(function($injector) { 269 | 270 | // angular services & mocks 271 | $http = $injector.get('$http'); 272 | $rootScope = $injector.get('$rootScope'); 273 | $timeout = $injector.get('$timeout'); 274 | 275 | // circuit breaker objects 276 | cktConfig = $injector.get('ngHttpCircuitBreakerConfig'); 277 | ckt = $injector.get('ngHttpCircuitBreaker'); 278 | 279 | cktStats = $injector.get('ngHttpCircuitBreakerStats'); 280 | 281 | })); 282 | 283 | beforeEach(inject(function(_$httpBackend_) { 284 | $httpBackend = _$httpBackend_; 285 | $httpBackend.whenGET('/api/unstable').respond(function() { 286 | return [500, 'App Error']; 287 | }); 288 | $httpBackend.whenGET('/ApI/uNsTAbLe').respond(function() { 289 | return [500, 'App Error']; 290 | }); 291 | $httpBackend.whenGET('/api/stable').respond(function() { 292 | return [200, 'OK']; 293 | }); 294 | $httpBackend.whenGET('/api/auth').respond(function() { 295 | return [401, 'Auth Required']; 296 | }); 297 | $httpBackend.whenGET('/twitter/api').respond(function() { 298 | return [200, 'OK']; 299 | }); 300 | })); 301 | 302 | it('should start stats in a clean state', function() { 303 | expect(cktStats).toBeDefined(); 304 | expect(cktStats.totalCalls).toBe(0); 305 | expect(cktStats.totalFailedCalls).toBe(0); 306 | expect(cktStats.totalSuccessfulCalls).toBe(0); 307 | expect(cktStats.totalIgnoredFailureResponses).toBe(0); 308 | expect(cktStats.circuits).toBeDefined(); 309 | expect(cktStats.circuits.length).toBe(1); 310 | }); 311 | 312 | 313 | }); 314 | 315 | describe('circuit breaker > ', function() { 316 | 317 | var $httpBackend, 318 | $rootScope, 319 | $timeout, 320 | cktConfig, 321 | ckt; 322 | 323 | 324 | beforeEach(module('ng-http-circuitbreaker', function($provide, ngHttpCircuitBreakerConfigProvider) { 325 | $provide.provider('ngHttpCircuitBreakerConfig', ngHttpCircuitBreakerConfigProvider); 326 | //$provide.provider('$http', $httpProvider); 327 | })); 328 | beforeEach( 329 | module(function(ngHttpCircuitBreakerConfigProvider, $httpProvider) { 330 | ngHttpCircuitBreakerConfigProvider.circuit({endPointRegEx: /^\/api\//i, failureLimit: 5, responseSLA: 500, timeUntilHalfOpen: 5000, statusCodesToIgnore: [401,403,409]}); 331 | $httpProvider.interceptors.push('ngHttpCircuitBreaker'); 332 | })); 333 | 334 | beforeEach(inject(function($injector) { 335 | 336 | // angular services & mocks 337 | $http = $injector.get('$http'); 338 | $rootScope = $injector.get('$rootScope'); 339 | $timeout = $injector.get('$timeout'); 340 | 341 | // circuit breaker objects 342 | cktConfig = $injector.get('ngHttpCircuitBreakerConfig'); 343 | ckt = $injector.get('ngHttpCircuitBreaker'); 344 | 345 | // setup spies to monitor promise behavior 346 | spyOn(unstableSpy, 'success'); 347 | spyOn(unstableSpy, 'failure'); 348 | spyOn(unstableSpy, 'then'); 349 | spyOn(stableSpy, 'success'); 350 | spyOn(stableSpy, 'failure'); 351 | spyOn(stableSpy, 'then'); 352 | 353 | // we also want to spy on the response interceptors and validate proper calling 354 | spyOn(ckt, 'responseError').andCallThrough(); 355 | spyOn(ckt, 'response').andCallThrough(); 356 | spyOn(ckt, 'request').andCallThrough(); 357 | })); 358 | 359 | beforeEach(inject(function(_$httpBackend_) { 360 | $httpBackend = _$httpBackend_; 361 | $httpBackend.whenGET('/api/unstable').respond(function() { 362 | return [500, 'App Error']; 363 | }); 364 | $httpBackend.whenGET('/ApI/uNsTAbLe').respond(function() { 365 | return [500, 'App Error']; 366 | }); 367 | $httpBackend.whenGET('/api/stable').respond(function() { 368 | return [200, 'OK']; 369 | }); 370 | $httpBackend.whenGET('/api/auth').respond(function() { 371 | return [401, 'Auth Required']; 372 | }); 373 | $httpBackend.whenGET('/twitter/api').respond(function() { 374 | return [200, 'OK']; 375 | }); 376 | })); 377 | 378 | it('should setup circuits', function() { 379 | expect(cktConfig).toBeDefined(); 380 | expect(cktConfig.circuits).toBeDefined(); 381 | expect(cktConfig.circuits.length).toBe(1); 382 | }); 383 | 384 | it('should process a single failure', function() { 385 | createFailedHttpCalls(1); 386 | 387 | $httpBackend.flush(); 388 | expect(cktConfig.circuits[0].failureCount).toBe(1); 389 | expect(unstableSpy.success).not.toHaveBeenCalled(); 390 | expect(unstableSpy.failure).toHaveBeenCalled(); 391 | }); 392 | 393 | it('should not affect endpoints not covered by circuit', function() { 394 | var twitter = false; 395 | 396 | $http.get('/twitter/api') 397 | .success(function(data, status, headers, config) { 398 | twitter = true; 399 | expect(config).toBeDefined(); 400 | expect(config.cktbkr).toBeUndefined(); 401 | }) 402 | .error(unstableSpy.failure); 403 | $httpBackend.flush(); 404 | 405 | expect(ckt.responseError).not.toHaveBeenCalled(); 406 | expect(ckt.request).toHaveBeenCalled(); 407 | expect(ckt.response).toHaveBeenCalled(); 408 | 409 | expect(twitter).toBeTruthy(); 410 | expect(unstableSpy.failure).not.toHaveBeenCalled(); 411 | }); 412 | 413 | it('should not trip circuit on responses with ignored status codes', function() { 414 | expect(cktConfig.circuits[0].failureCount).toBe(0); 415 | createAuthHttpCalls(5); 416 | expect(cktConfig.circuits[0].failureCount).toBe(0); 417 | expect(cktConfig.circuits[0].STATE).toBe(0); 418 | }); 419 | 420 | it('should process multiple failures safely', function() { 421 | 422 | createFailedHttpCalls(4); 423 | 424 | $httpBackend.flush(); 425 | 426 | expect(ckt.response).not.toHaveBeenCalled(); 427 | expect(unstableSpy.success).not.toHaveBeenCalled(); 428 | expect(unstableSpy.failure).toHaveBeenCalled(); 429 | 430 | expect(cktConfig.circuits[0].failureCount).toBe(4); 431 | expect(cktConfig.circuits[0].STATE).toBe(0); 432 | }); 433 | 434 | it('successful calls should decrement failure counter', function() { 435 | expect(cktConfig.circuits[0].failureCount).toBe(0); 436 | 437 | createFailedHttpCalls(4); 438 | $httpBackend.flush(); 439 | 440 | expect(cktConfig.circuits[0].failureCount).toBe(4); 441 | 442 | createSuccessfulHttpCalls(1); 443 | 444 | $httpBackend.flush(); 445 | expect(cktConfig.circuits[0].failureCount).toBe(3); 446 | }); 447 | 448 | it('should not allow failure count to go negative', function() { 449 | expect(cktConfig.circuits[0].failureCount).toBe(0); 450 | 451 | createSuccessfulHttpCalls(1); 452 | 453 | $httpBackend.flush(); 454 | expect(cktConfig.circuits[0].failureCount).toBe(0); 455 | }); 456 | 457 | it('excessive failures should trip the circuit breaker', function() { 458 | expect(cktConfig.circuits[0].failureCount).toBe(0); 459 | createFailedHttpCalls(5); 460 | $httpBackend.flush(); 461 | expect(cktConfig.circuits[0].failureCount).toBe(5); 462 | expect(cktConfig.circuits[0].STATE).toBe(2); 463 | }); 464 | 465 | it('should trip the circuit breaker and fail fast', function() { 466 | expect(cktConfig.circuits[0].failureCount).toBe(0); 467 | createFailedHttpCalls(5); 468 | $httpBackend.flush(); 469 | createSuccessfulHttpCalls(1); 470 | $rootScope.$digest(); // can't use $httpBackend.flush here 471 | expect(cktConfig.circuits[0].STATE).toBe(2); 472 | expect(stableSpy.failure).toHaveBeenCalled(); 473 | expect(stableSpy.success).not.toHaveBeenCalled(); 474 | }); 475 | 476 | it('should move to half-open state', function() { 477 | expect(cktConfig.circuits[0].failureCount).toBe(0); 478 | createFailedHttpCalls(5); 479 | $httpBackend.flush(); 480 | expect(cktConfig.circuits[0].STATE).toBe(2); 481 | $timeout.flush(); // circuit should now be in half-open state 482 | expect(cktConfig.circuits[0].STATE).toBe(1); 483 | }); 484 | 485 | it('should allow a request through when half-open and close circuit on success', function() { 486 | expect(cktConfig.circuits[0].failureCount).toBe(0); 487 | createFailedHttpCalls(5); 488 | $httpBackend.flush(); 489 | expect(cktConfig.circuits[0].STATE).toBe(2); 490 | $timeout.flush(); 491 | // circuit should now be in half-open state 492 | expect(cktConfig.circuits[0].STATE).toBe(1); 493 | // next call through should pass through and succeed, CLOSING circuit 494 | createSuccessfulHttpCalls(1); 495 | $httpBackend.flush(); 496 | expect(cktConfig.circuits[0].STATE).toBe(0); 497 | expect(cktConfig.circuits[0].failureCount).toBe(0); 498 | }); 499 | 500 | it('should allow a request through when half-open but leave circuit open on failure', function() { 501 | expect(cktConfig.circuits[0].failureCount).toBe(0); 502 | createFailedHttpCalls(5); 503 | $httpBackend.flush(); 504 | expect(cktConfig.circuits[0].STATE).toBe(2); 505 | $timeout.flush(); // circuit should now be in half-open state 506 | expect(cktConfig.circuits[0].STATE).toBe(1); 507 | // next http call should fail, leaving circuit open 508 | createFailedHttpCalls(1); 509 | $httpBackend.flush(); 510 | expect(cktConfig.circuits[0].STATE).toBe(2); 511 | expect(cktConfig.circuits[0].failureCount).toBe(6); 512 | }); 513 | 514 | it('should move to half-open, fail and stay closed, and then move to half-open again', function() { 515 | expect(cktConfig.circuits[0].failureCount).toBe(0); 516 | createFailedHttpCalls(5); 517 | $httpBackend.flush(); 518 | expect(cktConfig.circuits[0].STATE).toBe(2); // circuit is now OPEN 519 | 520 | $timeout.flush(); // circuit should now be in half-open state 521 | expect(cktConfig.circuits[0].STATE).toBe(1); 522 | 523 | // next http call should fail, leaving circuit open 524 | createFailedHttpCalls(1); 525 | $httpBackend.flush(); 526 | expect(cktConfig.circuits[0].STATE).toBe(2); 527 | expect(cktConfig.circuits[0].failureCount).toBe(6); 528 | expect(unstableSpy.failure).toHaveBeenCalled(); 529 | 530 | $timeout.flush(); // should now be half-open again 531 | expect(cktConfig.circuits[0].STATE).toBe(1); 532 | }); 533 | 534 | it('a closed circuit should not affect other circuits', function() { 535 | expect(cktConfig.circuits[0].failureCount).toBe(0); 536 | createFailedHttpCalls(5); 537 | $httpBackend.flush(); 538 | expect(cktConfig.circuits[0].STATE).toBe(2); 539 | 540 | var twitter = false; 541 | 542 | $http.get('/twitter/api') 543 | .success(function(data, status, headers, config) { 544 | twitter = true; 545 | expect(config).toBeDefined(); 546 | expect(config.cktbkr).toBeUndefined(); 547 | }) 548 | .error(stableSpy.failure); 549 | $httpBackend.flush(); 550 | 551 | // /api circuit should still be OPEN 552 | expect(cktConfig.circuits[0].STATE).toBe(2); 553 | expect(stableSpy.failure).not.toHaveBeenCalled(); 554 | expect(twitter).toBeTruthy(); 555 | }); 556 | 557 | it('should not care about casing since circuit defined with case insensitive regex', function() { 558 | $http.get('/ApI/uNsTAbLe') 559 | .success(unstableSpy.success) 560 | .error(unstableSpy.failure); 561 | 562 | $httpBackend.flush(); 563 | 564 | expect(cktConfig.circuits[0].STATE).toBe(0); 565 | expect(cktConfig.circuits[0].failureCount).toBe(1); 566 | }); 567 | 568 | it('should set the timer flag', function() { 569 | expect(cktConfig.circuits[0].timerSet).toBeFalsy(); 570 | createFailedHttpCalls(4); 571 | $httpBackend.flush(); 572 | expect(cktConfig.circuits[0].timerSet).toBeFalsy(); 573 | createFailedHttpCalls(1); 574 | $httpBackend.flush(); 575 | expect(cktConfig.circuits[0].timerSet).toBeTruthy(); 576 | 577 | }); 578 | 579 | it('should clear the timer flag after time until half open', function() { 580 | expect(cktConfig.circuits[0].timerSet).toBeFalsy(); 581 | createFailedHttpCalls(4); 582 | $httpBackend.flush(); 583 | expect(cktConfig.circuits[0].timerSet).toBeFalsy(); 584 | createFailedHttpCalls(1); 585 | $httpBackend.flush(); 586 | expect(cktConfig.circuits[0].timerSet).toBeTruthy(); 587 | $timeout.flush(); 588 | expect(cktConfig.circuits[0].timerSet).toBeFalsy(); 589 | }); 590 | 591 | 592 | 593 | }); 594 | }); 595 | 596 | --------------------------------------------------------------------------------