How to test Angular AuthGuard – examples for the CanActivate interface

featured_image

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 on Angular 7:
  • An Angular project with a base guard and authentication service implemented. This example requires the public isLoggedIn() 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:

The source code that I written for it is placed in the snippet below:

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:

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:

Therefore, to properly test the AuthGuard implementation we are going to use:

  • routeMock – to mock the ActivatedRouteSnapshot dependency;
  • routeStateMock, to mock the RouterStateSnapshot 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:

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:

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:

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:

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:

And verify the results:

Test AuthGuard - tests results screenshot

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:

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

4 thoughts on “How to test Angular AuthGuard – examples for the CanActivate interface

  1. 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.

  2. 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’]);
    });

Leave a Reply

Your email address will not be published. Required fields are marked *