I'm currently struggling on unit testing my canActivate()
method from my Routerguard Service. The Service looks as follows:
import {Injectable} from '@angular/core';
import {ActivatedRouteSnapshot, CanActivate, Router} from '@angular/router';
import {AuthService} from '../../auth/auth.service';
import {Observable, of} from 'rxjs';
import {NotificationService} from '../../../../shared/services/notification.service';
import {concatMap, map, take, tap} from 'rxjs/operators';
@Injectable({
providedIn: 'root'
})
export class ProfileGuard implements CanActivate {
constructor(private auth: AuthService, private router: Router,
private notification: NotificationService) {
}
canActivate(next: ActivatedRouteSnapshot): Observable<boolean> {
// checks for user if not - page not found
return this.auth.getUserEntity(next.params.uid).pipe(concatMap(user => {
if (user) {
// checks for permission if not - redirect to user overview
return this.auth.currentUser.pipe(
take(1),
map(current => this.auth.canEditProfile(current, next.params)),
tap(canEdit => {
if (!canEdit) {
this.router.navigate([`/profile/${next.params.uid}`]).then(() =>
this.notification.danger('Access denied. Must have permission to edit profile.'));
}
})
);
} else {
this.router.navigate(['/page-not-found']);
return of(false);
}
}));
}
}
It actually looks more complicated than it is: The first observer checks if there is a user in the db with the params value as unique identifier. The second observer checks then for the permission to edit this user. Now on the unit testing part of things:
describe('RouterGuardService', () => {
const routerStub: Router = jasmine.createSpyObj('Router', ['navigate']);
const authStub: AuthService = jasmine.createSpyObj('AuthService', ['getUserEntity', 'currentUser', 'canEditProfile']);
const notificationStub: NotificationService = jasmine.createSpyObj('NotificationService', ['danger']);
function createInputRoute(url: string): ActivatedRouteSnapshot {
const route: ActivatedRouteSnapshot = new ActivatedRouteSnapshot();
const urlSegs: UrlSegment[] = [];
urlSegs.push(new UrlSegment(url, {}));
route.url = urlSegs;
route.params = {
uid: url.replace('/profile/', '')
.replace('/edit', '')
};
return route;
}
beforeEach(() => {
TestBed.configureTestingModule({
providers: [
{provide: AuthService, useValue: authStub},
{provide: Router, useValue: routerStub},
{provide: NotificationService, useValue: notificationStub},
ProfileGuard]
});
});
it('should redirect to user overview - if has not permission', inject([ProfileGuard], (service: ProfileGuard) => {
(<jasmine.Spy>authStub.canEditProfile).and.returnValue(false);
authStub.currentUser = of(<any>{uid: 'jdkffdjjfdkls', role: Role.USER});
(<jasmine.Spy>authStub.getUserEntity).and.returnValue(of({uid: 'jdkffdjjfdkls', role: Role.USER}));
const spy = (<jasmine.Spy>routerStub.navigate).and.stub();
const notifySpy = (<jasmine.Spy>notificationStub.danger).and.stub();
const url: ActivatedRouteSnapshot = createInputRoute('/profile/BBB/edit');
service.canActivate(url).subscribe(res => {
console.log(res);
expect(spy).toHaveBeenCalledWith(['/BBB']);
expect(notifySpy).toHaveBeenCalledWith('Access denied. Must have permission to edit profile.');
expect(res).toBe(false);
}, err => console.log(err));
}));
});
But my test does not check my expect methods instead it console logs the error. Can maybe anyone help me on this?
First issue - when you create authStub
:
const authStub: AuthService = jasmine.createSpyObj('AuthService', ['getUserEntity', 'currentUser', 'canEditProfile']);
In this case you add currentUser
as a Method but not as a property. The correct way to create jasmine spyObj both with methods and properties:
const authStub = {
...jasmine.createSpyObj('authStub', ['getUserEntity', 'canEditProfile']),
currentUser: of(<any>{ uid: 'jdkffdjjfdkls', role: Role.USER })
} as jasmine.SpyObj<AuthService>;
Notice, that in your example - this object mutation inside the test does not affect anything:
authStub.currentUser = of(<any>{uid: 'jdkffdjjfdkls', role: Role.USER});
The reason is that you're using useValue
when providing services to TestBed
and it means that the tests already got the instance of auth service which doesn't have currentUser
property. This is why it's important to initialize it before running configureTestingModule
method.
Second issue - since your guard code is async, you have to write your unit tests asynchronously (you can use done
, sync
or fakeAsync&tick
).
Here is the final solution:
describe('RouterGuardService', () => {
const routerStub: Router = jasmine.createSpyObj('Router', ['navigate']);
const authStub = {
...jasmine.createSpyObj('authStub', ['getUserEntity', 'canEditProfile']),
currentUser: of(<any>{ uid: 'jdkffdjjfdkls', role: Role.USER })
} as jasmine.SpyObj<AuthService>;
const notificationStub: NotificationService = jasmine.createSpyObj('NotificationService', ['danger']);
let profileGuardService: ProfileGuard;
function createInputRoute(url: string): ActivatedRouteSnapshot {
// ...
}
beforeEach(() => {
TestBed.configureTestingModule({
// ...
});
profileGuardService = TestBed.get(ProfileGuard);
});
it('should redirect to user overview - if has not permission', fakeAsync(() => {
(<jasmine.Spy>authStub.canEditProfile).and.returnValue(false);
(<jasmine.Spy>authStub.getUserEntity).and.returnValue(of({ uid: 'jdkffdjjfdkls', role: Role.USER }));
const spy = (<jasmine.Spy>routerStub.navigate).and.callFake(() => Promise.resolve());
const notifySpy = (<jasmine.Spy>notificationStub.danger).and.stub();
const url: ActivatedRouteSnapshot = createInputRoute('/profile/BBB/edit');
let expectedRes;
profileGuardService.canActivate(url).subscribe(res => {
expectedRes = res;
}, err => console.log(err));
tick();
expect(spy).toHaveBeenCalledWith(['/profile/BBB']);
expect(notifySpy).toHaveBeenCalledWith('Access denied. Must have permission to edit profile.');
expect(expectedRes).toBe(false);
}));
});
If you want to have different currentUser
-s for each test dynamically, you can do this trick - initialize currentUser
property in authStub
with BehaviorSubject:
const authStub = {
...jasmine.createSpyObj('authStub', ['getUserEntity', 'canEditProfile']),
currentUser: new BehaviorSubject({})
} as jasmine.SpyObj<AuthService>;
And then inside the unit tests itself you can call next
method in order to set necessary current user mock:
it('should redirect to user overview - if has not permission', fakeAsync(() => {
(<BehaviorSubject<any>>authStub.currentUser).next(<any>{ uid: 'jdkffdjjfdkls', role: Role.USER });
// ...