Unit testing Angular HTTP interceptor - javascript

I have a standard HTTP interceptor as a factory:
angular
.module('app.services')
.factory('HttpInterceptorService', HttpInterceptorService);
function HttpInterceptorService($injector) {
// Callable functions
var service = {
response: response,
responseError: responseError
};
return service;
// Pass through clean response
function response(data) {
return data;
}
// Handle error response
function responseError(rejection) {
// Handle bypass requests
if (angular.isDefined(rejection.config) && rejection.config.bypassInterceptor) {
return rejection;
}
// Get $state via $injector to avoid a circular dependency
var state = $injector.get('$state');
switch (rejection.status) {
case 404:
return state.go('404');
break;
default:
return state.go('error');
}
}
}
In manual testing, I can see this works correctly by redirecting the user to the relevant 404 or error page if an HTTP call returns an error response. The basic principal of this is documented by Angular here: https://docs.angularjs.org/api/ng/service/$http#interceptors
Now I'm trying to write a unit test with Karma & Jasmine to test that the responseError function works correctly. I've checked out this SO answer to help me. My test looks like this:
describe('HttpInterceptorService', function() {
// Bindable members
var $window,
HttpInterceptorService;
// Load module
beforeEach(module('app.services'));
// Set window value
beforeEach(function () {
$window = { location: { href: null } };
module(function($provide) {
$provide.value('$window', $window);
});
});
// Bind references to global variables
beforeEach(inject(function(_HttpInterceptorService_) {
HttpInterceptorService = _HttpInterceptorService_;
}));
// Check service exists with methods
it('Exists with required methods', function() {
expect(HttpInterceptorService).toBeDefined();
expect(angular.isFunction(HttpInterceptorService.response)).toBe(true);
expect(angular.isFunction(HttpInterceptorService.responseError)).toBe(true);
});
// Test 404 HTTP response
describe('When HTTP response 404', function () {
beforeEach(function() {
HttpInterceptorService.responseError({ status: 404 });
});
it('Sets window location', function () {
expect($window.location.href).toBe('/404');
});
});
});
My test passes the Exists with required methods check but fails Sets window location with the following error:
Error: [$injector:unpr] Unknown provider: $stateProvider <- $state

The module doesn't seem to have ui.router module loaded, hence $state service is undefined. This is fine, because real router introduces extra moving parts and is highly undesirable in unit tests.
For functional test it is normal to treat a unit as a blackbox, provide initial conditions and test the results, asserting window.location would be appropriate.
For unit test there's no need to treat a unit as a blackbox, $state service may be stubbed:
var statePromiseMock = {};
beforeEach(module('app.services', {
$state: {
go: jasmine.createSpy().and.returnValue(statePromiseMock)
}
}));
And tested like:
it('...', inject(function (HttpInterceptorService, $state) {
var state404Promise = HttpInterceptorService.responseError({ status: 404 });
expect($state.go).toHaveBeenCalledWith('404');
expect(state404Promise).toBe(statePromiseMock);
...
}))
I.e. it may be something like
describe('HttpInterceptorService', function() {
// Bindable members
var HttpInterceptorService;
var statePromiseMock = {};
beforeEach(module('app.services', {
$state: {
go: jasmine.createSpy().and.returnValue(statePromiseMock)
}
}));
// Bind references to global variables
beforeEach(inject(function(_HttpInterceptorService_) {
HttpInterceptorService = _HttpInterceptorService_;
}));
// Check service exists with methods
it('Exists with required methods', function() {
expect(HttpInterceptorService).toBeDefined();
expect(angular.isFunction(HttpInterceptorService.response)).toBe(true);
expect(angular.isFunction(HttpInterceptorService.responseError)).toBe(true);
});
it('...', inject(function($state) {
var state404Promise = HttpInterceptorService.responseError({
status: 404
});
expect($state.go).toHaveBeenCalledWith('404');
expect(state404Promise).toBe(statePromiseMock);
}))
});

Related

Creating a function around angular $http requests

First project in AngularJS and I started creating my services (factories) that I made modular like this
angular.module('app.services.public', [])
.factory('publicService', ['$http', function publicService($http) {
var results = {};
results.contact = function (name, email, message){
return $http.get();
};
return results;
}]);
That I then call in my main angular app by including it. When I call it, I need to listen for success or error
publicService.contact().success(callback).error(callback)
My question is, I'm going to be doing a lot of API requests through these services and seems to be bad code to listen to the error everytime since 90% of the time it will do the same thing.
How can I create a wrapper around the $http.get or around all factory calls?
So something like
apiCall = function (url, data, successCallback, errorCallback){
$http.get(url,data).success(function(){
successCallback()
}).error(function(){
if(errorCallback()){ errorCallback(); return; }
// or display general error message
})
}
I would recommend against converting promise-based into callback-based APIs. Angular adopted promises and it best to stay with them.
Also, stay away from $http-specific .success/.error and use promise .then/.catch APIs.
How wide do you need to cast your net to handle $http errors?
1) Say, it only applies to your publicService service, then you can "handle" it at the each function:
.factory("publicService", function($http, $q){
function handleError(){
// invokes error handlers
}
return {
onError: function(cb){
// register error handlers
},
doSomethingA: function(){
return $http.get("some/url/A")
.then(function(response){
return response.data;
})
.catch(function(error){
handleError(error);
return $q.reject(error); // still "rethrow" the error
}
},
doSomethingB: function(){
// similar to above
},
// etc...
};
})
Then you could separate request from error handling:
.controller("MainCtrl", function($scope, publicService){
publicService.onError(function(error){
$scope.showError = true; // or something like that
})
})
.controller("FunctionACtrl", function($scope, publicService){
publicService.doSomethingA()
.then(function(data){
$scope.data = data;
});
})
2) Of course, the above, would only apply to request made via publicService. If you want to catch all $http errors, you could implement an $http interceptors. I won't go into detail - there is enough info in documentation and elsewhere - but it would could work like below:
.factory("ErrorService", function(){
return {
onError: function(cb){
// register error handlers
},
broadcastError: function(error){
// invoke error handlers
}
};
})
Then in interceptor, use ErrorService as a dependency:
'responseError': function(rejection) {
ErrorService.broadcastError(rejection);
return $q.reject(rejection);
}
Then you could handle the errors globally:
.controller("MainCtrl", function($scope, ErrorService){
ErrorService.onError(function(error){
$scope.showError = true; // or something like that
})
})
You have the right idea. You can do it easily with a Factory.
myApp.factory(APIService, function(publicService, $http) {
return {
// create methods in here
...
contact: function(cb) {
$http.get(url,data).success(cb).error(function(err){
console.error('oh no!', err);
});
}
};
});
Then you can use it in your controllers.
APIService.contact(function(data){
console.log('response from the api!', data);
});
You can even move your error handler to its own factory as well.
I would suggest an implementation using angular's $q service.
angular.module('app.services.public', [])
.factory('publicService', ['$http', '$q', function publicService($http, $q) {
var results = {};
results.contact = function (name, email, message){
return $q.when($http.get());
};
return results;
}]);
Or rather than use the $q.when(...) method you can use $q.deferred like so:
angular.module('app.services.public', [])
.factory('publicService', ['$http', '$q', function publicService($http, $q) {
var deferred = $q.deferred();
var results = {};
results.contact = function (name, email, message){
$http.get().success(function(data){
deferred.resolve({
// assumes data retried from http request has a title and price attribute
title: data.title,
cost: data.price});
}).error(function(data){
deferred.reject(data);
});
};
return deferred.promise;
}]);

undefined is not a function using $resource [duplicate]

When trying to poll a custom method copies on an AngularJS Resource I get the following error at angular.js:10033: (The method copy works just fine.)
TypeError: undefined is not a function
at https://code.angularjs.org/1.3.0-beta.8/angular-resource.min.js:9:347
at Array.forEach (native)
at q (https://code.angularjs.org/1.3.0-beta.8/angular.min.js:7:280)
at q.then.p.$resolved (https://code.angularjs.org/1.3.0-beta.8/angular-resource.min.js:9:329)
at J (https://code.angularjs.org/1.3.0-beta.8/angular.min.js:101:5)
at J (https://code.angularjs.org/1.3.0-beta.8/angular.min.js:101:5)
at https://code.angularjs.org/1.3.0-beta.8/angular.min.js:102:173
at g.$eval (https://code.angularjs.org/1.3.0-beta.8/angular.min.js:113:138)
at g.$digest (https://code.angularjs.org/1.3.0-beta.8/angular.min.js:110:215)
at g.$apply (https://code.angularjs.org/1.3.0-beta.8/angular.min.js:113:468)
Angular.js 10016 - 10035:
function consoleLog(type) {
var console = $window.console || {},
logFn = console[type] || console.log || noop,
hasApply = false;
// Note: reading logFn.apply throws an error in IE11 in IE8 document mode.
// The reason behind this is that console.log has type "object" in IE8...
try {
hasApply = !!logFn.apply;
} catch (e) {}
if (hasApply) {
return function() {
var args = [];
forEach(arguments, function(arg) {
args.push(formatError(arg));
});
return logFn.apply(console, args); // This is line 10033 where the error gets thrown.
};
}
Simplified resource:
angular.module('vgm.content-pages')
.factory('ContentPage', function($resource, $http) {
return $resource('/api/content-page/:id', { id:'#page.id' }, {
copy: {
method: 'POST',
url: '/api/content-page/copy/:id'
},
copies: {
method: 'GET',
isArray: true,
url: '/api/content-page/copies/:id'
}
});
});
Simplified directive where I'm getting the error:
angular.module('vgm.genericForms')
.directive('vgmFormCopyPicker', function() {
return {
restrict: 'E',
replace: true,
templateUrl: '/static/common/generic-forms/widgets/view-copy-picker.html',
scope: {
resource: '=',
},
controller: function($scope, $element) {
$scope.pages = [];
$scope.loadCopies = function() {
$scope.resource.$copies()
.then(function(response) {
$scope.pages = response.data;
});
};
}
};
});
As soon as I run the loadCopies method, executing the line $scope.resource.$copies() throws the error above.
In the Chrome Inspector I see the call to my API is actually being done. But resolving the promise seems to throw some error...
How can I solve this error?
EDIT:
$scope.resource = ContentPage.get({id: $stateParams.id}).$promise
$scope.resource.$save() // Works
$scope.resource.$update() // Works
$scope.resource.$copy() // Works
$scope.resource.$copies() // Does not work!
Angular Resource is trying to overwrite my initial resource with an array of items. But the instance of Resource does not have a method push of course.
I found the answer:
A resource is supposed to represent the matched data object for the rest of its lifespan. If you want to fetch new data you should do so with a new object.
$scope.copies = ContentPage.copies()
The answer from Guido is correct but I didn't get it at the first time.
If you add a custom method to your Angular $resource and using isArray: true and expecting to get an Array of something from your WebService you probably want to store the response in an Array.
Therefore you shouldn't use the instance method like this:
var ap = new Ansprechpartner();
$scope.nameDuplicates = ap.$searchByName(...);
But use the resource directly:
$scope.nameDuplicates = Ansprechpartner.searchByName(...)
Using following Angular resource:
mod.factory('Ansprechpartner', ['$resource',
function ($resource) {
return $resource('/api/Ansprechpartner/:id',
{ id: '#ID' },
{
"update": { method: "PUT" },
"searchByName": { method: "GET", url: "/api/Ansprechpartner/searchByName/:name", isArray: true }
}
);
}
]);
I am using Mean.js and this plagued for a few hours. There is a built in $update but it was not working when I tried to apply it to an object returned by a $resource. In order to make it work I had to change how I was calling the resource to update it.
For example, with a Student module, I was returning it with a $resource into $scope.student. When I tried to update student.$update was returning this error. By modifying the call to be Students.update() it fixed the problem.
$scope.update = function() {
var student = $scope.student;
var result = Students.update(student);
if(result){
$scope.message = 'Success';
} else {
$scope.error =
'Sorry, something went wrong.';
}
};

Is there a way I can give an error alert when my HTTP calls fail because of internet access?

My code makes many AngularJS $http requests. Often it is 3-4 at the same time.
Is there some way that I can intercept the http messages so that I get just one alert pop up if the internet connectivity is lost and there are multiple requests going on? I have seen other sites that do this but then if the action requires a number of http calls it seems that I could get more than one error popup coming.
If possible I would like to do this in just the one place in my code.
You need add responseInterceptors inside you $httpProvider in configuration phase of angular. This interceptor gets called after angular $httpProvider processing the response.
CODE
module.config(['$httpProvider', function($httpProvider) {
var interceptor = ['$rootScope', '$q', '$location', function(scope, $q, $location) {
function success(response) {
return response;
}
function error(response) {
var status = response.status;
if (status == 500) {
alert("Internal Server Error")
return;
}
if (status == 404) {
alert("Page not found")
return;
}
// otherwise
return $q.reject(responseInterceptors);
}
return function(promise) {
return promise.then(success, error);
}
}];
$httpProvider.responseInterceptors.push(interceptor);
}]);
Above code will provide you better control on error handling when any request fails.
For more details refer this anwser.
Update
For showing alert only once we could create a service,if error occurred then that will handle the set error variable
Service
module.service('errorService',function(){
//getter
this.getErrorFlag = function(){
return this.isError;
}
//setter
this.setErrorFlag = function(val){
this.isError = val;
}
});
Interceptor
module.config(['$httpProvider', function($httpProvider) {
var interceptor = ['$rootScope', '$q', '$location','errorService', function(scope, $q, $location,errorService) {
function success(response) {
return response;
}
function error(response) {
//setting error variable
errorService.setErrorFlag(true)
return $q.reject(responseInterceptors);
}
return function(promise) {
return promise.then(success, error);
}
}];
$httpProvider.responseInterceptors.push(interceptor);
}]);
Inside you controller put all $http call promises inside $q.all, and when promises gets resolved then check for isError flag of errorService.
IsError flag will be true if error occurred at least one & by using it you can show error only once.
Controller
module.controller('appCtrl',['$scope','$q','errorService',function($scope,$q,errorService){
var ajax1 = $http.get('url').then(function(){
},
function(){
});
var ajax2 = $http.get('url').then(function(){
},
function(){
});
var ajax3 = $http.get('url').then(function(){
},
function(){
});
errorService.setErrorFlag(false);
$q.all(ajax1, ajax2, ajax3).then(function(data){
//check for isError flag from service
if(errorService.getErrorFlag())
alert('Error occurred while processing request.'); //this will show only once
else
alert('No error occurred while processing request.')
});
}]);
Hope this could help you.
Thanks.
If you don't want to use an interceptor, you could simply process the error callback of your $http calls:
$scope.httpError = null;
$scope.processHttpError = function() {
// If you don't already have got an http error
if (!$scope.httpError) {
$scope.httpError = "Cannot load stuff";
}
};
$http.get('/someUrl')
.success(function(data, status, headers, config) { ... })
.error($scope.processHttpError);

Angular/Karma - "Unexpected Request" error when expected and observed request are the same

I'm unit testing a controller's method. My app fires off a few GET requests on app start, and I'm getting unexpected request errors when I run my tests.
Here is the login method of my controller. Note: I'm using angular classy (thus the nonstandard syntax):
login: function() {
var self = this,
params,
loginSuccess,
user;
if (self.$.user.email === '' || self.$.user.password === '') {
self.$notification.alert({
title: "Error",
message: "Email and password can't be blank!"
});
} else {
params = {
user: self.$.user,
position: self.Geolocation.currentPosition
};
self.$session.login(params).then(loginSuccess);
}
};
Here is the test:
describe("LoginController", function() {
var scope, $rootScope, createController, $controller,
$httpBackend, $session, session, $notification;
var emptyCredentials = {
email: "bleh",
password: ""
};
beforeEach(angular.mock.module('myapp'));
beforeEach(inject(function($injector){
jasmine.getJSONFixtures().fixturesPath='base/unit/fixtures';
$httpBackend = $injector.get("$httpBackend");
$httpBackend
.when('GET', 'http://myapp.com:3000/api/v2/apps/3?app_id=3')
.respond(200, getJSONFixture('apps.json'));
$httpBackend
.when('GET', 'http://myapp.com:3000/api/v2.2/locations?app_id=3')
.respond(200, getJSONFixture('locations.json'));
$httpBackend
.when('GET', 'http://myapp.com:3000/api/v2/users/me?app_id=3')
.respond(200, getJSONFixture('me.json'));
$session = $injector.get("$session");
// mock session so that session.loggedIn doesn't return true
// on subsequent tests
session = {
setUser: function() {
},
loggedIn: function() {
return false;
},
authPath: function() {
return false;
},
login: $session.login
};
$rootScope = $injector.get("$rootScope");
$controller = $injector.get("$controller");
$notification = $injector.get("$notification");
scope = $rootScope.$new();
createController = function () {
return $controller('LoginController', {
$scope: scope,
$session: session,
$notification: $notification
});
};
// spies on $notification.alerts
spyOn($notification, 'alert');
}));
afterEach (function () {
$httpBackend.verifyNoOutstandingExpectation();
$httpBackend.verifyNoOutstandingRequest();
$httpBackend.resetExpectations();
});
it('should show alert on empty login credentials', function() {
$httpBackend.expectGET('http://myapp.com:3000/api/v2/apps/3?app_id=3');
$httpBackend.expectGET('http://myapp.com:3000/api/v2.2/locations?app_id=3');
$httpBackend.expectGET('http://myapp.com:3000/api/v2/users/me?app_id=3');
var controller = createController();
scope.user = emptyCredentials;
scope.login();
$httpBackend.flush();
expect($notification.alert).toHaveBeenCalledWith({
title: "Error",
message: "Email and password can't be blank!"
});
});
When I run karma, I get these errors:
Error: Unexpected request: GET http://myapp.com:3000/api/v2/apps/3?app_id=3
Expected GET http://myapp.com:3000/api/v2/apps/3?app_id=3
at $httpBackend (/Users/myapp/vendor/angular-mocks/angular-mocks.js:1178:9)
at sendReq (/Users/myapp/vendor/angular/angular.js:8315:9)
at serverRequest (/Users/myapp/vendor/angular/angular.js:8049:16)
at wrappedCallback (/Users/myapp/vendor/angular/angular.js:11520:81)
at wrappedCallback (/Users/myapp/vendor/angular/angular.js:11520:81)
at /Users/myapp/vendor/angular/angular.js:11606:26
at Scope.$eval (/Users/myapp/vendor/angular/angular.js:12632:28)
at Scope.$digest (/Users/myapp/vendor/angular/angular.js:12444:31)
at Function.$httpBackend.flush (/Users/myapp/vendor/angular-mocks/angular-mocks.js:1438:16)
at null.<anonymous> (/Users/myapp/test/unit/controllers/login_controller.test.js:104:18)
Error: Unexpected request: GET http://myapp.com:3000/api/v2.2/locations?app_id=3
Expected GET http://myapp.com:3000/api/v2/apps/3?app_id=3
at $httpBackend (/Users/myapp/vendor/angular-mocks/angular-mocks.js:1178:9)
at sendReq (/Users/myapp/vendor/angular/angular.js:8315:9)
at serverRequest (/Users/myapp/vendor/angular/angular.js:8049:16)
at wrappedCallback (/Users/myapp/vendor/angular/angular.js:11520:81)
at wrappedCallback (/Users/myapp/vendor/angular/angular.js:11520:81)
at /Users/myapp/vendor/angular/angular.js:11606:26
at Scope.$eval (/Users/myapp/vendor/angular/angular.js:12632:28)
at Scope.$digest (/Users/myapp/vendor/angular/angular.js:12444:31)
at Function.$httpBackend.verifyNoOutstandingExpectation (/Users/myapp/vendor/angular-mocks/angular-mocks.js:1470:16)
at null.<anonymous> (/Users/myapp/test/unit/controllers/login_controller.test.js:75:18)
FYI Line 104 of login_controller.test.js is $httpBackend.flush(). Line 75 of login_controller.test.js is $httpBackend.verifyNoOutstandingExpectation().
I believe the $httpBackend.when() are called before the app is started (and thus before app start's requests are fired), but maybe that's not true. Curiously enough, the error reports that it found an unexpected request of XXX, and the very next line says it expected a request of the same path! So not sure why that's happening.
Running Angular 1.2.21, Angular Mocks 1.2.21
Can anyone explain to me why I'm seeing this error, and what I can do to properly flush and handle those requests?
Thanks.
I figured out what was going on:
It turns out a vendor plugin was extending the native String object, redefining a match() method. This was conflicting with how angular-mocks was comparing the expectation to the request, and thus threw the error falsely.
If you happen to experience this issue (where expected request and the observed request are identical), look for plugins or libraries that extend the native String object.
Mootools, for example, does this and would cause this issue.
The problem is that now $http uses promises even for request interceptors and so the verification is happening before $httpBackend gets the request. Try adding $rootScope.$digest(); and you will get "No more request expected".
Refer: https://github.com/angular/angular.js/issues/5453

Unit test failing when function called on startup

This is an extension off a former question of mine: $httpBackend in AngularJs Jasmine unit test
In my controller, the getStuff function is called on startup. This causes my unit test to fail. When I comment it out, my unit tests pass and work successfully. When uncommented, the error is:
Error: Unexpected request: GET /api/stuff
No more request expected
My controller is:
$scope.stuff = [];
$scope.getStuff = function () {
var url = site.root + 'api/stuff';
$http.get(url)
.success(function (data) {
$scope.stuff = data;
})
.error(function(error) {
console.log(error);
});
};
//$scope.getStuff();
and my unit test is:
it('should get stuff', function () {
var url = '/api/stuff';
var httpResponse = [{ "stuffId": 1 }, { "stuffId": 2 }];
httpLocalBackend.expectGET(url).respond(200, httpResponse);
$scope.getStuff();
httpLocalBackend.flush();
expect($scope.stuff.length).toBe(2);
} );
Everything unit test wise, works fine like this. Unfortunately, this breaks the actual site functionality. When I uncomment the last line of the controller, the unit test breaks, and the site works. Any help is appreciated. Thanks!
FIXED: Thanks to fiskers7's answer, this is my solution.
it('should get stuff', function () {
var url = '/api/stuff';
var httpResponse = [{ "stuffId": 1 }, { "stuffId": 2 }];
httpLocalBackend.expectGET(url).respond(200, httpResponse);
httpLocalBackend.expectGET(url).respond(200, httpResponse);
$scope.getStuff();
httpLocalBackend.flush();
expect($scope.stuff.length).toBe(2);
} );
Verbatim from my comment:
When you create the controller it makes a call to 'api/stuff' and when you call $scope.getStuff() in your test you call it again. So instead of one call to 'api/stuff' you have two which is what the error is saying. httpBackend didn't expect two calls to the endpoint, only one so it throws the error.
Code example from my comment to this answer if you need to see it.
it('should get stuff', function () {
var url = '/api/stuff';
var httpResponse = [{ "stuffId": 1 }, { "stuffId": 2 }];
httpLocalBackend.expectGET(url).respond(200, httpResponse);
httpLocalBackend.flush();
expect($scope.stuff.length).toBe(2);
});

Categories