diff --git a/src/ngResource/resource.js b/src/ngResource/resource.js index 1a157b481872..db4dc4449c00 100644 --- a/src/ngResource/resource.js +++ b/src/ngResource/resource.js @@ -167,9 +167,9 @@ function shallowClearAndCopy(src, dst) { * for more information. * - **`responseType`** - `{string}` - see * [requestType](https://developer.mozilla.org/en-US/docs/DOM/XMLHttpRequest#responseType). - * - **`interceptor`** - `{Object=}` - The interceptor object has two optional methods - - * `response` and `responseError`. Both `response` and `responseError` interceptors get called - * with `http response` object. See {@link ng.$http $http interceptors}. + * - **`interceptor`** - `{Object=}` - The interceptor object has four optional methods - + * `request`, `requestError`, `response`, and `responseError`. See + * {@link ng.$http $http interceptors} for details. * * @param {Object} options Hash with custom settings that should extend the * default `$resourceProvider` behavior. The supported options are: @@ -633,6 +633,10 @@ angular.module('ngResource', ['ng']). var isInstanceCall = this instanceof Resource; var value = isInstanceCall ? data : (action.isArray ? [] : new Resource(data)); var httpConfig = {}; + var requestInterceptor = action.interceptor && action.interceptor.request || + undefined; + var requestErrorInterceptor = action.interceptor && action.interceptor.requestError || + undefined; var responseInterceptor = action.interceptor && action.interceptor.response || defaultResponseInterceptor; var responseErrorInterceptor = action.interceptor && action.interceptor.responseError || @@ -667,7 +671,14 @@ angular.module('ngResource', ['ng']). extend({}, extractParams(data, action.params || {}), params), action.url); - var promise = $http(httpConfig).then(function(response) { + // Start the promise chain + var promise = $q. + resolve(httpConfig). + then(requestInterceptor). + catch(requestErrorInterceptor). + then($http); + + promise = promise.then(function(response) { var data = response.data; if (data) { diff --git a/test/ngResource/resourceSpec.js b/test/ngResource/resourceSpec.js index 260f2efd5f47..c3ec78ffb7e1 100644 --- a/test/ngResource/resourceSpec.js +++ b/test/ngResource/resourceSpec.js @@ -3,7 +3,7 @@ describe("resource", function() { describe("basic usage", function() { - var $resource, CreditCard, callback, $httpBackend, resourceProvider; + var $resource, CreditCard, callback, $httpBackend, resourceProvider, $q; beforeEach(module('ngResource')); @@ -14,6 +14,7 @@ describe("basic usage", function() { beforeEach(inject(function($injector) { $httpBackend = $injector.get('$httpBackend'); $resource = $injector.get('$resource'); + $q = $injector.get('$q'); CreditCard = $resource('/CreditCard/:id:verb', {id:'@id.key'}, { charge:{ method:'post', @@ -978,6 +979,188 @@ describe("basic usage", function() { }); }); + + describe('requestInterceptor', function() { + var rejectReason = {'lol':'cat'}; + var successSpy, failureSpy; + + beforeEach(function() { + successSpy = jasmine.createSpy('successSpy'); + failureSpy = jasmine.createSpy('failureSpy'); + }); + + it('should allow per action request interceptor that gets full configuration', function() { + var CreditCard = $resource('/CreditCard', {}, { + query: { + method: 'get', + isArray: true, + interceptor: { + request: function(httpConfig) { + callback(httpConfig); + return httpConfig; + } + } + } + }); + + $httpBackend.expect('GET', '/CreditCard').respond([{id: 1}]); + + var resource = CreditCard.query(); + resource.$promise.then(successSpy, failureSpy); + + $httpBackend.flush(); + expect(callback).toHaveBeenCalledOnce(); + expect(successSpy).toHaveBeenCalledOnce(); + expect(failureSpy).not.toHaveBeenCalled(); + + expect(callback).toHaveBeenCalledWith({ + 'method': 'get', + 'url': '/CreditCard' + }); + }); + + it('should call $http with the value returned from requestInterceptor', function() { + var CreditCard = $resource('/CreditCard', {}, { + query: { + method: 'get', + isArray: true, + interceptor: { + request: function(httpConfig) { + httpConfig.url = '/DebitCard'; + return httpConfig; + } + } + } + }); + + $httpBackend.expect('GET', '/DebitCard').respond([{id: 1}]); + + var resource = CreditCard.query(); + resource.$promise.then(successSpy, failureSpy); + + $httpBackend.flush(); + expect(successSpy).toHaveBeenCalledOnce(); + expect(failureSpy).not.toHaveBeenCalled(); + }); + + it('should abort the operation if the requestInterceptor rejects the operation', function() { + var CreditCard = $resource('/CreditCard', {}, { + query: { + method: 'get', + isArray: true, + interceptor: { + request: function() { + return $q.reject(rejectReason); + } + } + } + }); + + var resource = CreditCard.query(); + resource.$promise.then(successSpy, failureSpy); + + // Make sure all promises resolve. + $rootScope.$apply(); + + // Ensure the resource promise was rejected + expect(resource.$resolved).toBeTruthy(); + expect(successSpy).not.toHaveBeenCalled(); + expect(failureSpy).toHaveBeenCalledOnce(); + expect(failureSpy).toHaveBeenCalledWith(rejectReason); + + // Ensure that no requests were made. + $httpBackend.verifyNoOutstandingRequest(); + }); + + it('should call requestErrorInterceptor if requestInterceptor rejects the operation', function() { + var CreditCard = $resource('/CreditCard', {}, { + query: { + method: 'get', + isArray: true, + interceptor: { + request: function() { + return $q.reject(rejectReason); + }, + requestError: function(rejection) { + callback(rejection); + return $q.reject(rejection); + } + } + } + }); + + var resource = CreditCard.query(); + resource.$promise.then(successSpy, failureSpy); + $rootScope.$digest(); + + expect(callback).toHaveBeenCalledOnce(); + expect(callback).toHaveBeenCalledWith(rejectReason); + expect(successSpy).not.toHaveBeenCalled(); + expect(failureSpy).toHaveBeenCalledOnce(); + expect(failureSpy).toHaveBeenCalledWith(rejectReason); + + // Ensure that no requests were made. + $httpBackend.verifyNoOutstandingRequest(); + }); + + it('should abort the operation if a requestErrorInterceptor rejects the operation', function() { + var CreditCard = $resource('/CreditCard', {}, { + query: { + method: 'get', + isArray: true, + interceptor: { + request: function() { + return $q.reject(rejectReason); + }, + requestError: function(rejection) { + return $q.reject(rejection); + } + } + } + }); + + var resource = CreditCard.query(); + resource.$promise.then(successSpy, failureSpy); + $rootScope.$apply(); + + expect(resource.$resolved).toBeTruthy(); + expect(successSpy).not.toHaveBeenCalled(); + expect(failureSpy).toHaveBeenCalledOnce(); + expect(failureSpy).toHaveBeenCalledWith(rejectReason); + + // Ensure that no requests were made. + $httpBackend.verifyNoOutstandingRequest(); + }); + + it('should continue the operation if a requestErrorInterceptor rescues it', function() { + var CreditCard = $resource('/CreditCard', {}, { + query: { + method: 'get', + isArray: true, + interceptor: { + request: function(httpConfig) { + return $q.reject(httpConfig); + }, + requestError: function(httpConfig) { + return $q.resolve(httpConfig); + } + } + } + }); + + $httpBackend.expect('GET', '/CreditCard').respond([{id: 1}]); + + var resource = CreditCard.query(); + resource.$promise.then(successSpy, failureSpy); + $httpBackend.flush(); + + expect(resource.$resolved).toBeTruthy(); + expect(successSpy).toHaveBeenCalledOnce(); + expect(failureSpy).not.toHaveBeenCalled(); + $httpBackend.verifyNoOutstandingRequest(); + }); + }); + it('should allow per action response interceptor that gets full response', function() { CreditCard = $resource('/CreditCard', {}, { query: { @@ -1318,7 +1501,7 @@ describe("basic usage", function() { }); describe('errors', function() { - var $httpBackend, $resource, $q; + var $httpBackend, $resource, $q, $rootScope; beforeEach(module(function($exceptionHandlerProvider) { $exceptionHandlerProvider.mode('log'); @@ -1330,6 +1513,7 @@ describe('errors', function() { $httpBackend = $injector.get('$httpBackend'); $resource = $injector.get('$resource'); $q = $injector.get('$q'); + $rootScope = $injector.get('$rootScope'); })); @@ -1366,6 +1550,78 @@ describe('errors', function() { /^\[\$resource:badcfg\] Error in resource configuration for action `get`\. Expected response to contain an object but got an array \(Request: GET \/Customer\/123\)/ ); }); + + describe('requestInterceptor', function() { + var rejectReason = {'lol':'cat'}; + var successSpy, failureSpy, callback; + + beforeEach(function() { + successSpy = jasmine.createSpy('successSpy'); + failureSpy = jasmine.createSpy('failureSpy'); + callback = jasmine.createSpy(); + }); + + it('should call requestErrorInterceptor if requestInterceptor throws an error', function() { + var CreditCard = $resource('/CreditCard', {}, { + query: { + method: 'get', + isArray: true, + interceptor: { + request: function() { + throw rejectReason; + }, + requestError: function(rejection) { + callback(rejection); + return $q.reject(rejection); + } + } + } + }); + + var resource = CreditCard.query(); + resource.$promise.then(successSpy, failureSpy); + $rootScope.$apply(); + + expect(callback).toHaveBeenCalledOnce(); + expect(callback).toHaveBeenCalledWith(rejectReason); + expect(successSpy).not.toHaveBeenCalled(); + expect(failureSpy).toHaveBeenCalledOnce(); + expect(failureSpy).toHaveBeenCalledWith(rejectReason); + + // Ensure that no requests were made. + $httpBackend.verifyNoOutstandingRequest(); + }); + + it('should abort the operation if a requestErrorInterceptor throws an exception', function() { + var CreditCard = $resource('/CreditCard', {}, { + query: { + method: 'get', + isArray: true, + interceptor: { + request: function() { + return $q.reject(); + }, + requestError: function() { + throw rejectReason; + } + } + } + }); + + var resource = CreditCard.query(); + resource.$promise.then(successSpy, failureSpy); + $rootScope.$apply(); + + expect(resource.$resolved).toBeTruthy(); + expect(successSpy).not.toHaveBeenCalled(); + expect(failureSpy).toHaveBeenCalledOnce(); + expect(failureSpy).toHaveBeenCalledWith(rejectReason); + + // Ensure that no requests were made. + $httpBackend.verifyNoOutstandingRequest(); + }); + }); + }); describe('cancelling requests', function() { @@ -1430,7 +1686,7 @@ describe('cancelling requests', function() { ); it('should use `cancellable` value if passed a non-numeric `timeout` in an action', - inject(function($log, $q) { + inject(function($log, $q, $rootScope) { spyOn($log, 'debug'); $httpBackend.whenGET('/CreditCard').respond({}); @@ -1443,6 +1699,7 @@ describe('cancelling requests', function() { }); var creditCard = CreditCard.get(); + $rootScope.$apply(); expect(creditCard.$cancelRequest).toBeDefined(); expect(httpSpy.calls.argsFor(0)[0].timeout).toEqual(jasmine.any($q)); expect(httpSpy.calls.argsFor(0)[0].timeout.then).toBeDefined();