I'm implementing a functional router guard in Angular 15.2.9 which checks if a user is logged in. When this is not the case, the guard should return either false
or a UrlTree
(i.e. redirect to login page), depending on the parameter redirectToLogin: boolean
.
I'm using a factory function for the functional route guard similar to the one in this article:
export const isLoggedIn = (redirectToLogin: boolean): CanActivateFn => {
return (next: ActivatedRouteSnapshot, state: RouterStateSnapshot) => {
const isLoggedIn = !!inject(CookieService).check('some_cookie');
if (isLoggedIn) return true;
if (!isLoggedIn && !redirectToLogin) return false;
const redirectUrl = inject(Router).createUrlTree(['user', 'sign_in'], {
queryParams: { next: encodeURIComponent(`/${next.url.join('/')}`) },
queryParamsHandling: 'merge',
});
return redirectUrl;
};
};
It works like expected but I am not able to write specs for it:
let cookieServiceSpy;
let activatedRouteSnapshot;
beforeEach(() => {
cookieServiceSpy = jasmine.createSpyObj<CookieService>(['check']);
activatedRouteSnapshot = { url: [], data: {} };
TestBed.configureTestingModule({
providers: [
isLoggedIn,
{ provide: CookieService, useValue: cookieServiceSpy },
{
provide: ActivatedRouteSnapshot,
useValue: activatedRouteSnapshot,
},
],
});
});
it('should return true when secure_user_id cookie is present and redirectToLogin is false', () => {
// given
cookieServiceSpy.check.and.returnValue(true);
activatedRouteSnapshot.data.redirectToLogin = false;
// when
const result = TestBed.runInInjectionContext(isLoggedIn); // <-- this line is the problem
// then
expect(result).toBeTrue();
});
results in a typescript error: Argument of type '(redirectToLogin: boolean) => CanActivateFn' is not assignable to parameter of type '() => CanActivateFn'. Target signature provides too few arguments. Expected 1 or more, but got 0.
I tried several other options too:
TestBed.runInInjectionContext(isLoggedIn as any)
➔ Can't resolve all parameters for isLoggedIn: (?). in test runTestBed.runInInjectionContext(isLoggedIn(false))
➔ Argument of type 'CanActivateFn' is not assignable to parameter of type '() => boolean | UrlTree | Observable<boolean | UrlTree> | Promise<boolean | UrlTree>'. Target signature provides too few arguments. Expected 2 or more, but got 0.TestBed.runInInjectionContext(isLoggedIn(false) as any)
➔ Can't resolve all parameters for isLoggedIn: (?). in test runTestBed.runInInjectionContext(isLoggedIn(false)(activatedRouteSnapshot, null))
➔ Argument of type 'boolean | UrlTree | Observable<boolean | UrlTree> | Promise<boolean | UrlTree>' is not assignable to parameter of type '() => unknown'. Type 'boolean' is not assignable to type '() => unknown'.TestBed.runInInjectionContext(isLoggedIn(false)(activatedRouteSnapshot, null) as any);
➔ inject() must be called from an injection context such as a constructor, a factory function, a field initializer, or a function used with EnvironmentInjector#runInContext
.TestBed.runInInjectionContext(isLoggedIn(false) as any)(activatedRouteSnapshot, null);
➔ This expression is not callable. Type '{}' has no call signatures.(TestBed.runInInjectionContext(isLoggedIn(false) as any) as any)(activatedRouteSnapshot, null)
➔ Can't resolve all parameters for isLoggedIn: (?).I also tried omitting the factory function and using Route Data as parameter as shown here but injecting ActivatedRouteSnapshot
is not possible and using ActivatedRoute
results in nested Observables... there has to be an easier way.
How can I test the functional router guard?
The error you are observing is due to 2 things :
isLoggedIn
TestBed.runInInjectionContext
, you initilized the injector and the injector tried to convert the providers to factories which failed because isLoggedIn
is not annotated and has 1 or more parameters (here only 1).If you really needed to inject the CanActivateFn
you could use {provide: isLoggedIn, useValue: () => { ... }
but in our case you are not injecting the function, you're just calling it.
So no need to provide that function and the call is straight foward :
beforeEach(() => {
cookieServiceSpy = jasmine.createSpyObj<CookieService>(['check']);
activatedRouteSnapshot = { url: [], data: {} };
TestBed.configureTestingModule({
providers: [
{ provide: CookieService, useValue: cookieServiceSpy },
],
});
});
it('should return true when secure_user_id cookie is present and redirectToLogin is false', async () => {
// given
cookieServiceSpy.check.and.returnValue(true);
activatedRouteSnapshot.data.redirectToLogin = false;
// when
const result = TestBed.runInInjectionContext(() => isLoggedIn(true)(activatedRouteSnapshot, undefined)); // Double call to retrieve the result.
// then
expect(result).toBeTrue();
});