After adding the routing guards to your project, you need to unit test their methods to make sure that an unauthenticated user is correctly redirected to a "/login"
path. Check out a sample test configuration and test cases that verify if the router always redirects users unmistakably.
What we are going to build
We will add tests for checking if our guard
redirects users accordingly to the authentication result. The guard
in this example uses AuthService
to verify if users are logged in and Router
to redirect them to the login form if they are not.
Don’t worry if you don’t have a project like that. You can read the Securing your Spring Boot and Angular app with JWT #3 – Frontend post to see how to implement security features in an Angular app or just clone the little-pinecone/jwt-spring-boot-angular-scaffolding repository with a completed project (look into the frontend
module).
Requirements
- Angular CLI – a command line interface tool that generates a project as well as performs many development tasks.
I’m working onAngular 7
:
1234567891011121314151617$ ng --versionAngular CLI: 7.1.4Node: 10.15.0OS: linux x64Angular:...Package Version------------------------------------------------------@angular-devkit/architect 0.11.4@angular-devkit/core 7.1.4@angular-devkit/schematics 7.1.4@schematics/angular 7.1.4@schematics/update 0.11.4rxjs 6.3.3typescript 3.1.6 - An Angular project with a base
guard
and authentication service implemented. This example requires the publicisLoggedIn()
method in the service that returns a boolean value.
The AuthGuard implementation
The guard
that we are going to test was generated with the following command:
1 |
$ ng generate guard auth/guards/auth |
The source code that I written for it is placed in the snippet below:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
// src/app/auth/guards/auth.guard.ts … export class AuthGuard implements CanActivate { constructor(private authService: AuthService, private router: Router) {} canActivate( next: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean> | Promise<boolean> | boolean { let url: string = state.url; return this.checkLogin(url); } private checkLogin(url: string): boolean { if(this.authService.isLoggedIn()) { return true; } this.authService.redirectToUrl = url; this.router.navigate(['/login']); return false; } } |
I use the AuthService
to obtain the boolean value, on which the guard
decides whether to allow users to access the requested resources or redirect them to the login page. In case the users were not authorised they need to log in and, after that, are automatically redirected to the url
they tried to reach in the first place.
The guard
presented above implements the canActivate()
method and in the following sections we are going to test its logic.
Test the guard
When we created the guard
using the command mentioned above, the Angular CLI
generated the auth.guard.spec.ts
file for us. The main structure required for testing was automatically prepared and we even have one default test case already there –
it('should be created'… checks whether the component can be properly instantiated.
Run in the command line: $ ng test to verify that all tests pass, before introducing any new code. |
Configure tests
The test configuration is resolved in the describe('AuthGuard', () => {…} section. To simplify the test cases to the maximum, we are going to define all required variables and mockups in advance. Furthermore, we need to slightly expand the beforeEach(() => {…} section.
Define variables
We are going to start with providing all variables that will be used during tests. You can see the list containing three objects and three mockups below:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
// src/app/auth/guards/auth.guard.spec.ts … describe('AuthGuard', () => { let injector: TestBed; let authService: AuthService let guard: AuthGuard; let routeMock: any = { snapshot: {}}; let routeStateMock: any = { snapshot: {}, url: '/cookies'}; let routerMock = {navigate: jasmine.createSpy('navigate')} … // beforeEach // test cases }); |
To avoid manually creating instances of any classes we need, we are using the TestBed
as our injector
. Following there are declarations for the AuthService
and, obviously, the AuthGuard
instances.
Why we need the mockups?
The CanActivate
interface expects the following dependencies to be met:
1 2 3 |
export interface CanActivate { canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean … } |
Therefore, to properly test the AuthGuard
implementation we are going to use:
routeMock
– to mock theActivatedRouteSnapshot
dependency;routeStateMock
, to mock theRouterStateSnapshot
dependency used to obtain a protected path that a user tried to access –'/cookies'
is used in my app (adjust it accordingly to your application);
The last mockup, routerMock
, is used to create a spy on the navigate()
function to verify the correct work of redirection issued by our guard
.
Set the preconditions for each test
Now we are going to complete the prerequisites for tests. Keep in mind that we rely on the TestBed
to create classes and inject services. That ensures that every test will be run with all required objects provided. Instantiate the injector
, authService
and guard
like in the snippet below:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
// src/app/auth/guards/auth.guard.spec.ts … describe('AuthGuard', () => { … beforeEach(() => { TestBed.configureTestingModule({ providers: [AuthGuard, { provide: Router, useValue: routerMock },], imports: [HttpClientTestingModule] }); injector = getTestBed(); authService = injector.get(AuthService); guard = injector.get(AuthGuard); … }); |
Don’t forget to specify that the guard in the tests is supposed to use the routerMock
declared earlier as the Router
instance. That allows us to spy on the navigate()
function. Make sure that you included the HttpClientTestingModule
in the imports
, without it the routing won’t work and the tests will fail.
Verify imports
Below you can see all the required imports for the auth.guard.spec.ts
file:
1 2 3 4 5 6 7 |
// src/app/auth/guards/auth.guard.spec.ts import { TestBed, inject, getTestBed } from '@angular/core/testing'; import { AuthGuard } from './auth.guard'; import { AuthService } from '../services/auth.service'; import { Router } from '@angular/router'; import { HttpClientTestingModule } from '@angular/common/http/testing'; … |
Add test cases
Let’s expand the default set of tests with additional cases.
First, let’s test the situation when an unauthorised user tried to access a path that is protected. The access should be denied and the user redirected to the login page:
1 2 3 4 5 6 7 |
// src/app/auth/guards/auth.guard.spec.ts … it('should redirect an unauthenticated user to the login route', () => { expect(guard.canActivate(routeMock, routeStateMock)).toEqual(false); expect(routerMock.navigate).toHaveBeenCalledWith(['/login']); }); … |
If our private checkLogin()
function from the guard
works correctly, the router will navigate this devious user to the '/login'
page and the guard.canActivate()
function will return false.
The user authentication result returned from the authService
is false by default, so we didn’t have to mock it here. If the authentication service works differently in your app, don’t forget to mock its response here. Thanks to instantiating the service in the beforeEach
section, it is accessible in every test case.
On the other hand, we need to verify that the guard
will allow an authenticated user to access the protected route:
1 2 3 4 5 6 7 |
// src/app/auth/guards/auth.guard.spec.ts … it('should allow the authenticated user to access app', () => { spyOn(authService, 'isLoggedIn').and.returnValue(true); expect(guard.canActivate(routeMock, routeStateMock)).toEqual(true); }); … |
We mock the authentication result returned from the authService
to make sure it will be positive. We expect that the guard
will grant access to the requested resource ('/cookies'
).
Run the tests
Use the following command to start the tests:
1 |
$ ng test |
And verify the results:
The described tests should only be a part of a larger suite. To fully verify that the authorisation mechanisms in your application work properly, you need to cover more cases. You can find other tests in the project repository. |
Wrapping up
The complete code for the auth.guard.spec.ts
file:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
// src/app/auth/guards/auth.guard.spec.ts import { TestBed, inject, getTestBed } from '@angular/core/testing'; import { AuthGuard } from './auth.guard'; import { AuthService } from '../services/auth.service'; import { Router } from '@angular/router'; import { HttpClientTestingModule } from '@angular/common/http/testing'; describe('AuthGuard', () => { let injector: TestBed; let authService: AuthService let guard: AuthGuard; let routeMock: any = { snapshot: {}}; let routeStateMock: any = { snapshot: {}, url: '/cookies'}; let routerMock = {navigate: jasmine.createSpy('navigate')} beforeEach(() => { TestBed.configureTestingModule({ providers: [AuthGuard, { provide: Router, useValue: routerMock },], imports: [HttpClientTestingModule] }); injector = getTestBed(); authService = injector.get(AuthService); guard = injector.get(AuthGuard); }); it('should be created', () => { expect(guard).toBeTruthy(); }); it('should redirect an unauthenticated user to the login route', () => { expect(guard.canActivate(routeMock, routeStateMock)).toEqual(false); expect(routerMock.navigate).toHaveBeenCalledWith(['/login']); }); it('should allow the authenticated user to access app', () => { spyOn(authService, 'isLoggedIn').and.returnValue(true); expect(guard.canActivate(routeMock, routeStateMock)).toEqual(true); }); }); |
If you need more details about securing an Angular application I once again recommend the Securing your Spring Boot and Angular app with JWT #3 – Frontend post.
Photo by Genessa Panainte on StockSnap
Your example looks great, and fits with what I would expect, but it does not work. I modeled my implementation to match yours, but regardless of what I do, I continue to get Error: Can’t resolve all parameters for AuthGuard: (?).I when I run even an simple test for expect(true).toEqual(true). If I remove my injected reference to AppService (your AuthService) from the constructor of my AuthGuard, then it works.
i tried testing my code using your logic, but i keep getting an error
TypeError: Cannot read property ‘__source’ of undefined
My code:
import { Injectable, OnInit, EventEmitter, Output, Directive } from ‘@angular/core’;
import { CanActivate, ActivatedRouteSnapshot, Router, RouterStateSnapshot } from ‘@angular/router’;
import { Observable } from ‘rxjs’;
import { environment } from ‘../../environments/environment’;
import { HttpService } from ‘../services/http.service’;
import { OAuthService } from ‘angular-oauth2-oidc’;
import { OAuth2OdicService } from ‘../services/oauth2-odic.service’;
import { SessionStorageService } from ‘../services/session.storage.service’;
@Directive({
selector: ‘[AuthGuard]’,
host: {},
providers: []
})
@Injectable()
export class AuthGuard implements CanActivate {
public accessToken: string;
@Output() valueChange = new EventEmitter();
constructor(
private sessionStorageService: SessionStorageService,
private oauthService: OAuth2OdicService,
private router: Router,
private httpService: HttpService) {
}
canActivate(
route: ActivatedRouteSnapshot,
state: RouterStateSnapshot) {
return new Promise<boolean>((resolve, reject) => {
const canAuthenticate = (route.fragment !== null && !!route.fragment);
if (!canAuthenticate && !sessionStorage.getItem(‘id_token’)) {
this.login();
reject(false);
} else {
const accessToken = this.getQueryParam(route.fragment, ‘access_token’);
const idToken = this.getQueryParam(route.fragment, ‘id_token’);
const tokenType = this.getQueryParam(route.fragment, ‘token_type’);
if (accessToken && idToken && tokenType) {
this.sessionStorageService.setItem(‘id_token’, idToken);
this.sessionStorageService.setItem(‘access_token’, accessToken);
this.sessionStorageService.setItem(‘token_type’, tokenType);
resolve(true);
this.router.navigate([‘/search’]);
} else {
if (sessionStorage.getItem(‘access_token’) && sessionStorage.getItem(‘id_token’)) {
resolve(true);
this.router.navigate([‘/search’]);
}
}
}
});
}
private login() {
this.httpService.getConfigurations().subscribe(data => {
const redirectUri = window.location.origin;
const clientId = data.clientId;
const loginUrl = data.authenticationUrl ;
const accessToken = this.oauthService.initImplicitFlow(loginUrl + ‘?’, clientId, redirectUri + ‘/’);
});
}
private getQueryParam(queryString: string, paramName: string) {
if (queryString && paramName) {
const expression = new RegExp(
${paramName}=([^&]*));
const results = queryString.match(expression);
return results[1] ? results[1] : ”;
}
}
}
My test:
import { async, ComponentFixture, TestBed, inject, getTestBed } from ‘@angular/core/testing’;
import { AuthGuard } from ‘./auth.guard’;
import { HttpService } from ‘../services/http.service’;
import { OAuthService } from ‘angular-oauth2-oidc’;
import { OAuth2OdicService } from ‘../services/oauth2-odic.service’;
import { SessionStorageService } from ‘../services/session.storage.service’;
import {HttpClient} from ‘@angular/common/http’;
import {DataMappingService} from ‘../services/data-mapping.service’;
import {GoogleAnalyticsEventsService} from ‘@fm-ui-adk/components-2.0/dist/analytics’;
import {HttpClientTestingModule, HttpTestingController} from ‘@angular/common/http/testing’;
import { RouterTestingModule } from ‘@angular/router/testing’;
import { OAuthModule } from ‘angular-oauth2-oidc’;
import { CanActivate, ActivatedRouteSnapshot, Router, RouterStateSnapshot } from ‘@angular/router’;
import { state } from ‘@angular/animations’;
describe(‘AuthGuard’, () => {
let lastConnection: any;
let httpTestingController: HttpTestingController;
let guard: AuthGuard;
let injector: TestBed;
let oAuth2OdicService:OAuth2OdicService;
let httpService:HttpService;
let routeMock: any = { snapshot: {}};
let routeStateMock: any = { snapshot: {}, url: ‘/search’};
let routerMock = {navigate: jasmine.createSpy(‘navigate’)}
//let fixture: ComponentFixture<AuthGuard>;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [
HttpClientTestingModule,
RouterTestingModule,
OAuthModule.forRoot()
],
providers: [
AuthGuard, { provide: Router, useValue: routerMock },
SessionStorageService,
OAuth2OdicService ,
],
});
injector = getTestBed();
oAuth2OdicService = injector.get(oAuth2OdicService);
httpService =injector.get(HttpService);
guard = injector.get(AuthGuard);
});
it(‘should be created’, inject([AuthGuard], (guard: AuthGuard) => {
expect(guard).toBeTruthy();
}));
it(‘should call canActivate method’,() => {
const resolve: any =true;
expect(guard.canActivate(routeMock,routeStateMock)).toEqual(resolve);
expect(routerMock.navigate).toHaveBeenCalledWith([‘search’]);
});
What a great article! Congratulations on the excellent work!
Thank you so much for sharing this. I really appreciate it!