Unit Testing a Keycloak-enabled Angular app - javascript

I'm trying to write a test suite for an Angular app that uses Keycloak for authentication.
However, as Keycloak requires you to manually bootstrap Angular and set up a few interceptors, I'm unable to fire any test due to the following error:
Error: $injector:unpr
Unknown Provider: AuthProvider <- Auth <- authInterceptor <- $http <- $templateRequest <- $route
This is the code for the interceptor that raises the error:
angular.module('MPMReportGenerator')
.factory('authInterceptor', function authInterceptor ($q, Auth, $log) {
return {
request: function (config) {
var deferred = $q.defer()
Auth.updateToken(5).success(function () {
config.headers = config.headers || {}
config.headers.Authorization = 'Bearer ' + Auth.token
deferred.resolve(config)
}).error(function () {
deferred.reject('Failed to refresh token')
})
$log.info(deferred.promise)
return deferred.promise
}
}
})
My thinking is that I should mock the interceptor and just have it return the request.
However, I fail to see how I could do that, since this interceptor is never injected anywhere as a dependency, it's simply declared with the block above and that's it. My understanding of mocked services is that they need to be injected somewhere to be mocked.
My implementation of Keycloak into Angular comes straight from their examples, if that helps.
Edit
I've been trying to inject a mocked Auth module into the service I'm writing a test for, but still no change.
I'm very new to unit testing in general, so I'm a bit lost trying to track this down. I feel like I know where the issue is, but not how to solve it (The Auth service is added during the bootstrap of the app, I need to mock it for things to work, but it seems I don't know how/where to mock it properly)
Here's the whole testing code:
describe('Services', function () {
'use strict'
beforeEach(module('MPMReportGenerator'))
module(function ($provide) {
$provide.factory('Auth', function () {
return null
})
})
var sectionService, $httpBackend, mockAuth
beforeEach(inject(function (_sectionService_, _$httpBackend_, Auth) {
sectionService = _sectionService_
$httpBackend = _$httpBackend_
mockAuth = Auth
}))
it('should get sections', function () {
$httpBackend.expect('GET', '/MPMReportGenerator/api/categories/all').respond(200)
sectionService.getSections()
expect($httpBackend.flush).not.toThrow()
})
})
Edit 2
I've managed to get past my initial error by making a mock version of Auth.
I am now facing issues implementing a mock version of Keycloak's Javascript library.
My current mock code is as follow:
beforeEach(module(function ($provide) {
$provide.factory('Auth', function ($q) {
return {
updateToken: function (minValidity) {
return {
success: function (fn) {
var deferred = $q.defer()
deferred.resolve('')
fn(deferred.promise)
},
error: function (fn) {
var deferred = $q.defer()
deferred.resolve('Error')
fn(deferred.promise)
}
}
},
token: 'thisisafaketokenfortesting'
}
})
}))
And throws this error:
Expected function not to throw, but it threw TypeError: undefined is not an object (near '...}).error(function () {...').
target/MPMReportGenerator-1.0.0/js/app.service.spec.js:42:43
loaded#http://localhost:9876/context.js:151:17
My actual test is this:
it('should get sections', function () {
$httpBackend.expect('GET', '/MPMReportGenerator/api/categories/all').respond(200)
sectionService.getSections()
expect($httpBackend.flush).not.toThrow()
})

I finally figured it out.
Here is the needed code if anyone wants to test an Angular app with keycloak:
beforeEach(
module(function ($provide) {
$provide.factory('Auth', function ($q) {
return {
updateToken: function (minValidity) {
return {
success: function () {
return {
error: function () {
var deferred = $q.defer()
return deferred.promise
}
}
}
}
},
token: 'thisisafaketokenfortesting'
}
})
}))
Note that you will likely need to mock other parts of the keycloak library if you intend to test the interceptors provided in the official examples.
Edit
Don't use the code above, the following works much better:
$provide.factory('Auth', function () {
return {
updateToken: function (minValidity) {
return {
success: function () {
return this
},
error: function () {
return this
}
}
},
token: 'thisisafaketokenfortesting'
}
})

Related

Circular dependency found in angular js

Tring to add interceptor header for my every request, however, it is giving me below error.
Uncaught Error: [$injector:cdep] Circular dependency found: $http <- Auth <- httpRequestInterceptor <- $http <- $templateRequest <- $route
app.js
var app= angular.module('myDemoApp',['ngRoute'])
app.factory('httpRequestInterceptor', ['Auth', function (Auth) {
return {
request: function (config) {
config.headers['x-access-token'] = Auth.getToken();
return config;
}
};
}]);
app.config(function ($httpProvider) {
$httpProvider.interceptors.push('httpRequestInterceptor');
});
Auth Service
(function () {
'use strict';
myDemoApp.factory('Auth', ['$http', '$window', Auth]);
/******Auth function start*****/
function Auth($http, $window) {
var authFactory = {};
authFactory.setToken = setToken;
authFactory.getToken = getToken;
return authFactory;
/*setToken function start*/
function setToken(token) {
if (token) {
$window.localStorage.setItem('token', token);
} else {
$window.localStorage.removeItem('token');
}
}
/*getToken function start*/
function getToken() {
return $window.localStorage.getItem('token')
}
}
})();
You can't do this because.
You have created httpRequestInterceptor which intercepts all $http requests.
Now, you are passing Auth in the httpRequestInterceptor.
If you'll see, Auth uses $http request inside itself.
So, your interceptor can itself cause a http request using Auth.
Hence, its circular error and angularjs wont allow you to do that !
Remove $http from Auth factory OR dont insert a service into interceptor which itself uses $http.
I hope you got, how the infinite loop chain being created, hence a circular dependency error !

Yet another 'Unknown provider' for an AngularJS service

I actually hate to be that guy, but I've been sitting with this
problem for some days now. I have these three files as a part of a
larger angularjs application. I can not get even this rudimentary test
to pass (or even work). I've been comparing files within the project,
I've read on-line (tried all those ways people have suggested). I have
even written the files from scratch a few times. I'm probably not able
to see my error anymore. I guess this is easier to spot (right away)
for a back-seat driver.
I'd be most appreciative for any help.
The output from gulp/karma
PhantomJS 2.1.1 (Linux 0.0.0) SiteDescriptionService the service should be defined FAILED
Error: [$injector:unpr] Unknown provider: SiteDescriptionServiceProvider <- SiteDescriptionService
http://errors.angularjs.org/1.5.8/$injector/unpr?p0=SiteDescriptionServiceProvider%20%3C-%20SiteDescriptionService (line 4511)
bower_components/angular/angular.js:4511:86
getService#bower_components/angular/angular.js:4664:46
bower_components/angular/angular.js:4516:48
getService#bower_components/angular/angular.js:4664:46
injectionArgs#bower_components/angular/angular.js:4688:68
invoke#bower_components/angular/angular.js:4710:31
workFn#bower_components/angular-mocks/angular-mocks.js:3085:26
loaded#http://localhost:8080/context.js:151:17
inject#bower_components/angular-mocks/angular-mocks.js:3051:28
app/service/sitedescriptor-service-test.js:10:19
app/service/sitedescriptor-service-test.js:4:13
global code#app/service/sitedescriptor-service-test.js:1:9
Expected undefined to be truthy.
app/service/sitedescriptor-service-test.js:17:32
loaded#http://localhost:8080/context.js:151:17
The module declaration
(function(){
'use strict';
angular.module('application.service', []);
})();
The service itself
(function () {
angular.module('application.service')
.service('SiteDescriptorService',
['$http', '$q', function ($http, $q) {
var lastRequestFailed = true,
promise,
items = [];
return {
name: 'SiteDescriptorService',
getItems: function () {
if (!promise || lastRequestFailed) {
promise = $http.get('site.json').then(
function (response) {
lastRequestFailed = false;
items = response.data;
return items;
}, function (response) { // error
lastRequestFailed = true;
return $q.reject(response);
});
}
return promise;
}
};
}]
);
})();
and the test
describe('SiteDescriptionService', function() {
'use strict';
describe('the service', function() {
var service, httpBackend;
beforeEach(module('application.service'));
beforeEach(inject(function(_SiteDescriptionService_, $httpBackend) {
service = _SiteDescriptionService_;
httpBackend = $httpBackend;
console.log(service);
}));
it('should be defined', function() {
expect(service).toBeTruthy();
});
});
});
Cheers
Mats
Looks like you just use incorrect name when injecting dependency, should be 'SiteDescriptorService' and not 'SiteDescriptionService'

Mocking custom provider injected into provider when unit testing Angular in Jasmine

I'm unit testing a provider in Jasmine, which relies on another provider. There's no configuration associated with this provider. When mocking a provider, I've read you're supposed to use something like
beforeEach(module(function ($provide) {
mockInjectedProvider = { };
$provide.value('injected', mockInjectedProvider );
}));
which works fine for me when injecting a custom provider into a service. When injecting them into a provider it doesn't work though. The code doesn't fail, but what gets executed when testing is the actual provider, not the mocked one. Abstracted example below.
var mockInjectedProvider;
beforeEach(function () {
module('myModule');
});
beforeEach(module(function ($provide) {
mockInjectedProvider = {
myFunc: function() {
return "testvalue"
}
}
};
$provide.value('injected', mockInjectedProvider );
}));
beforeEach(inject(function (_base_) {
baseProvider = _base_;
}));
it("injectedProvider should be mocked", function () {
var resultFromMockedProvider = baseProvider.executeMyFuncFromInjected();
expect(resultFromMockedProvider).toEqual("testvalue");
}); // Here instead of using my mock it executes the actual dependency
In the $provide.value statement I've tried including both injected and injectedProvider, as well as using $provide.provider and mocking a $get function on it but nothing seems to work. I just can't get it to mock away the actual provider. Abstracted base provider looks like this.
(function (ng, module) {
module.provider("base",
["injectedProvider", function (injectedProvider) {
this.executeMyFuncFromInjected= function() {
return injectedProvider.myFunc(); // let's say this returns "realvalue"
}
this.$get = function () {
return this;
};
}]
);
})(window.angular, window.angular.module("myModule"));
Everything in my code is working except the Jasmine mocking.
In this case is better to just mock the return value instead of the provider.
var mockInjectedProvider;
beforeEach(function () {
module('myModule');
});
beforeEach(inject(function (_injected_) {
spyOn(_injected_, "myFunc").and.returnValue("testvalue");
}));
beforeEach(inject(function (_base_) {
baseProvider = _base_;
}));
it("injectedProvider should be mocked", function () {
var resultFromMockedProvider = baseProvider.executeMyFuncFromInjected();
expect(resultFromMockedProvider).toEqual("testvalue");
}); // Here instead of using my mock it executes the actual dependency

TypeError: parsed is undefined on angularjs service unit test

I am trying to make a unit test for a service that uses $http. I am using Jasmine and I keep on getting this error:
TypeError: parsed is undefined in angular.js (line 13737)
This is what my service looks like:
angular.module('myapp.services', [])
.factory('inviteService', ['$rootScope', '$http', function($rootScope, $http) {
var inviteService = {
token: '',
getInvite: function(callback, errorCallback) {
$http.get('/invites/' + this.token + '/get-invite')
.success(function(data) {
callback(data);
})
.error(function(data, status, headers, config) {
errorCallback(status);
});
}
};
return inviteService;
}]);
This is what my test looks like:
describe ('Invite Service', function () {
var $httpBackend, inviteService, authRequestHandler;
var token = '1123581321';
beforeEach(module('myapp.services'));
beforeEach(inject(function ($injector) {
$httpBackend = $injector.get('$httpBackend');
authRequestHandler = $httpBackend.when('/invites/' + token + '/get-invite').respond({userId: 'userX'}, {'A-Token': 'xxx'});
inviteService = $injector.get('inviteService');
}));
afterEach (function () {
$httpBackend.verifyNoOutstandingExpectation ();
$httpBackend.verifyNoOutstandingRequest ();
});
describe ('getInvite', function () {
beforeEach(function () {
inviteService.token = token;
});
it ('should return the invite', function () {
$httpBackend.expectGET('/invites/' + token + '/get-invite');
inviteService.getInvite();
$httpBackend.flush();
});
});
});
I am pretty new to unit testing angularjs based apps and I used the example in the angularjs documentation
https://docs.angularjs.org/api/ngMock/service/$httpBackend
I am not sure what I could be missing, and I already tried different things and I always get the same error, any help will be appreciated.
The parsed variable is the URL from the service in question. It is undefined for one of the following reasons:
URL is malformed
$http.get is not called
token is not defined
sucess and error callbacks have no data
.respond is not called
.respond does not include a response object as an argument
For example:
describe('simple test', test);
function test()
{
it('should call inviteService and pass mock data', foo);
function foo()
{
module('myapp.services');
inject(myServiceTest);
function myServiceTest(inviteService, $httpBackend)
{
$httpBackend.expect('GET', /.*/).respond(200, 'bar');
function callback(){};
inviteService.getInvite.token = '1123581321';
inviteService.getInvite(callback, callback);
$httpBackend.flush();
expect(callback).toHaveBeenCalledOnce();
}
}
}
References
AngularJS source: angular-mocksSpec.js - "should throw exception when only parsed body differs from expected body object"
AngularJS source: urlUtils.js
AngularJS source: urlUtilsSpec.js

In an Angular, how would I get bootstrap data from the server before launch?

A bit of info. I'm working on a single page app, but am attempting to make it just an HTML file, rather than an actual dynamic page that contains all the bootstrap information in it. I'm also hoping to, when the app boots (or perhaps prior to), check to see if the current session is 'logged in', and if not then direct the hash to the 'login'.
I'm new to Angular, and am having a difficult time figuring out how to program out this flow. So, in essence..
HTML page loaded with 'deferred' bootstrap
Hit URL to get login status
If status is 'not logged in', direct to #/login
Start app
Any pointers on where #2 and #3 would live? In my 'easy world' I'd just use jquery to grab that data, and then call the angular.resumeBootstrap([appname]). But, as I'm trying to actually learn Angular rather than just hack around the parts I don't understand, I'd like to know what would be used in this place. I was looking at providers, but I'm not sure that's what I need.
Thanks!
EDIT
Based on #Mik378's answer, I've updated my code to the following as a test. It works to a point, but as the 'get' is async, it allows the application to continue loading whatever it was before then shooting off the status results..
var app = angular.module('ping', [
'ngRoute',
'ping.controllers'
]).provider('security', function() {
this.$get = ['$http', function($http) {
var service = {
getLoginStatus: function () {
if (service.isAuthenticated())
return $q.when(service.currentUser);
else
return $http.get('/login/status').then(function (response) {
console.log(response);
service.loggedIn = response.data.loggedIn;
console.log(service);
return service.currentUser;
});
},
isAuthenticated: function () {
return !!service.loggedIn;
}
};
return service;
}];
}).run(['security', function(security) {
return security.getLoginStatus().then(function () {
if(!security.isAuthenticated()) {
console.log("BADNESS");
} else {
console.log("GOODNESS");
}
});
}]);
My hope was that this could somehow be completed prior to the first controller booting up so that it wasn't loading (or attempting to load) things that weren't even cleared for access yet.
EDIT #2
I started looking into the 'resolve' property in the router, and #Mik378 verified what I was looking at. My final code that is (currently) working how I want it is as follows (appologies about the super long code block)
angular.module('ping.controllers', [])
.controller('Dashboard', ['$scope', function($scope) {
console.log('dashboard')
}])
.controller('Login', ['$scope', function($scope) {
console.log('login')
}]);
var app = angular.module('ping', [
'ngRoute',
'ping.controllers'
]).run(['$rootScope', '$location', function($root, $location) {
$root.$on("$routeChangeError", function (event, current, previous, rejection) {
switch(rejection) {
case "not logged in":
$location.path("/login"); //<-- NOTE #1
break;
}
});
}]);
app.provider('loginSecurity', function() {
this.$get = ['$http', '$q', function($http, $q) {
var service = {
defer: $q.defer, //<-- NOTE #2
requireAuth: function() { //<-- NOTE #3
var deferred = service.defer();
service.getLoginStatus().then(function() {
if (!service.isAuthenticated()) {
deferred.reject("not logged in")
} else {
deferred.resolve("Auth OK")
}
});
return deferred.promise;
},
getLoginStatus: function() {
if (service.isAuthenticated()) {
return $q.when(service.currentUser);
} else {
return $http.get('/login/status').then(function(response) {
console.log(response);
service.loggedIn = response.data.loggedIn;
console.log(service);
return service.currentUser;
});
}
},
isAuthenticated: function() {
return !!service.loggedIn;
}
};
return service;
}
];
});
app.config(['$routeProvider', function($routeProvider) {
console.log('Routing loading');
$routeProvider.when('/', {
templateUrl: 'static/scripts/dashboard/template.html',
controller: 'Dashboard',
resolve: {'loginSecurity': function (loginSecurity) {
return loginSecurity.requireAuth(); //<- NOTE #4
}}
});
$routeProvider.when('/login', {
templateUrl: 'static/scripts/login/template.html',
controller: 'Login'
});
$routeProvider.otherwise({redirectTo: '/404'});
}]);
Notes:
This section hooks into routing failures. In the case of a "no login", I wanted to catch the failure and pop the person over to the login page.
I can't get access to the $q inside of the requireAuth function, so I grabbed a reference to it. Perhaps a better way of doing this exists?
This function wraps up the other two - it uses the promise returned from getLoginStatus, but returns its own promise that will be rejected if the end result from the getLoginStatus winds up with the user not being logged in. Sort of a round-about way of doing it.
This returns #3's promise, which is used by the $routeProvider.. so if it fails, the routing fails and you end up catching it at #1.
Whew. I think that's enough for a day. Time for a beer.
No need to use a deferred bootstrap for your case:
angular.module('app').run(['security', '$location', function(security) {
// Get the current user when the application starts
// (in case they are still logged in from a previous session)
security.requestCurrentUser().then(function(){
if(!security.isAuthenticated())
$location.path('yourPathToLoginPage')
}; //service returning the current user, if already logged in
}]);
this method requestCurrentUser would be the following:
requestCurrentUser: function () {
if (service.isAuthenticated())
return $q.when(service.currentUser);
else
return $http.get('/api/current-user').then(function (response) {
service.currentUser = response.data.user;
return service.currentUser;
});
}
and inside security service again:
isAuthenticated: function () {
return !!service.currentUser;
}
Note the run method of the module => As soon as the application runs, this service is called.
-- UPDATE --
To prevent any controller to be initialized before the promise provided by requestCurrentUser is resolved, a better solution, as evoked in the comments below, is to use the resolve route property .

Categories