I'm relatively new to Angular 7, having come from AngularJS, I've written a guard implementing CanLoad which stops users without the correct claims loading a module. It checks whether the user is logged in and whether the user has a claim that's expected by the route.
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { LoadGuard } from './core/authentication/guards/load.guard';
import { MainMenuComponent } from './core/navigation/main-menu/main-menu.component';
import { PageNotFoundComponent } from './core/navigation/page-not-found/page-not-found.component';
import { UnauthorisedComponent } from './core/navigation/unauthorised/unauthorised.component';
const routes: Routes = [
{ path:'', component: MainMenuComponent, outlet: 'menu'},
{ path: 'authentication', loadChildren: './core/authentication/authentication.module#AuthenticationModule' },
{ path: 'home', loadChildren: './areas/home/home.module#HomeModule', canLoad: [LoadGuard], data: {expectedClaim: 'home'} },
{ path:"unauthorised", component: UnauthorisedComponent},
{ path:'**', component: PageNotFoundComponent }
];
@NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule]
})
export class AppRoutingModule { }
The guard works, however I'm having trouble writing the unit test for it.
import { Injectable } from '@angular/core';
import { CanLoad, Route, Router } from '@angular/router';
import { Observable } from 'rxjs';
import { AuthenticationService } from 'src/app/Services/Authentication/authentication.service';
@Injectable({
providedIn: 'root'
})
export class LoadGuard implements CanLoad {
constructor(private authService: AuthenticationService, private router: Router){}
canLoad(route: Route): Observable<boolean> | Promise<boolean> | boolean {
if (!route || !route.path) return false;
let isValid: boolean = this.checkLoggedIn(route.path);
if (isValid) {
if (route.data && route.data.expectedClaim) {
let expectedClaim = route.data.expectedClaim;
isValid = this.checkClaim(expectedClaim);
}
}
return isValid;
}
checkLoggedIn(url: string): boolean {
if (this.authService.checkLoggedIn()) {
return true;
}
this.authService.redirectUrl = url;
console.log('this.authService.redirectUrl (after)= ' + this.authService.redirectUrl);
this.router.navigate(['/authentication/login']);
return false;
}
checkClaim(claim: string) {
let hasClaim: boolean = false;
if (this.authService.currentUser) {
hasClaim = this.authService.currentUser.claims.indexOf(claim) > -1;
}
return hasClaim;
}
}
The unit test I have below doesn't work:
import { HttpClientModule } from '@angular/common/http';
import { fakeAsync, TestBed } from '@angular/core/testing';
import { ActivatedRouteSnapshot, Router, RouterStateSnapshot, ActivatedRoute, Route } from '@angular/router';
import { LoadGuard } from './load.guard';
class MockActivatedRouteSnapshot {
private _data: any;
get data(){
return this._data;
}
}
let mockRouterStateSnapshot : RouterStateSnapshot;
describe('LoadGuard', () => {
let loadGuard: LoadGuard;
let route: ActivatedRouteSnapshot;
let authService;
let mockRouter: any;
beforeEach(() => {
mockRouter = jasmine.createSpyObj('Router', ['navigate']);
TestBed.configureTestingModule({
imports: [
HttpClientModule,
],
providers: [
LoadGuard,
{ provide: ActivatedRouteSnapshot, useClass: MockActivatedRouteSnapshot},
{ provide: Router, useValue: mockRouter},
]
});
});
it('should be created', () => {
authService = { checkLoggedIn: () => true };
loadGuard = new LoadGuard(authService, mockRouter);
expect(loadGuard).toBeTruthy();
});
describe('check expected claims', ()=>{
it('should not be able to load an valid route needing claim when logged in without claim', fakeAsync(() => {
authService = { checkLoggedIn: () => true };
loadGuard = new LoadGuard(authService, mockRouter);
let route = new Route();
spyOnProperty(route,'data','get').and.returnValue({expectedClaim: 'policy'});
mockRouterStateSnapshot = jasmine.createSpyObj<RouterStateSnapshot>('RouterStateSnapshot', ['toString']);
mockRouterStateSnapshot.url = "test";
expect(loadGuard.canLoad(route)).toBeFalsy();
}));
});
It doesn't allow me to New a Route. I might just be doing the test wrong. Can anyone help with this?
Unlike canActivate()
, canLoad()
only requires a Route argument. I.e. canLoad(route: Route):boolean
vs canActivate( route: ActivatedRouteSnapshot, state: RouterStateSnapshot )
. Route
is just an interface that we use to define and export the routes and it should already exist in the TestBed Module context. Hence, you don't need to mock it at all or create a new instance of it.
In your beforeEach(async()) jasmine function, import RouterTestingModule with routes.
TestBed.configureTestingModule({
imports: [HttpClientModule, ... ..., RouterTestingModule.withRoutes(routes)],
...
providers: [AuthenticationService, LoadGuard ] //only need to provide it here!
})
with routes
being the export const routes: Routes = [{}]
that you had defined with loadChildren
and canLoad
.
Take note of this, importing RouterTestingModule automatically provides (i.e. injects) these services:
As can be seen in this API docs link: https://angular.io/api/router/testing/RouterTestingModule#providers
Because of that, you can simply reference these injected services without having to mock them as you had done so.
In your describe() jasmine function, declare these:
describe('AppComponent', () => {
... //all your other declarations like componentFixture, etc
loadGuard: LoadGuard;
authService: AuthenticationService;
router: Router;
location: Location;
loader: NgModuleFactoryLoader;
...
});
In your beforeEach() jasmine function:
location = TestBed.get(Location); //these already exist in TestBed context because of RouterTestingModule
router = TestBed.get(Router);
... //other declarations
loadGuard = TestBed.get(LoadGuard);
authService = TestBed.get(AuthenticationService);
Now, the goal with this unit test case is to test routing and whether the corresponding module is actually loaded or not.
Hence, you also need to stubbed your lazily loaded modules, in your beforeEach() jasmine function:
loader = TestBed.get(NgModuleFactoryLoader); //already exists in TestBed context because of RouterTestingModule
loader.stubbedModules = {
'./areas/home/home.module#HomeModule': HomeModule,
... //other lazily loaded modules
}
fixture.detectChanges();
Since you have already imported your routes in configureTestingModule()
as mentioned above, you don't need to reset your router config as the API specs would have you to (https://angular.io/api/router/testing/SpyNgModuleFactoryLoader#description).
With all that set up, you are ready to test your canLoad() guard.
it('if X claims is false, user shouldn't be able to load module', fakeAsync(() => {
spyOn(authService, 'checkClaims').and.returnValue(false);
fixture.detectChanges();
router.navigateByUrl('XRoute');
tick();
expect(location.path()).toBe('/"unauthorised'); //or /404 depending on your req
}))
You don't need to mock anything else or create spy objects and what not.
Although this answer is several months late, and most likely, you don't even need it now. I hope by posting it, it will help other Angular developers since this is the only question in Stackoverflow that specifically asked about unit testing canLoad() which I had faced too.