Home > Back-end >  Angular - How to test Guards with ActivatedRouteSnapshot RouterStateSnapshot and GlobalState
Angular - How to test Guards with ActivatedRouteSnapshot RouterStateSnapshot and GlobalState

Time:06-25

I'm creating unit test for my Angular app but i don'k know create tests for my custom guard that use Redux.

This is my code of the guard

import { Injectable } from '@angular/core';
import { ActivatedRouteSnapshot, CanActivate, CanLoad, Route, Router, RouterStateSnapshot, UrlSegment, UrlTree } from '@angular/router';
import { first, map, Observable, tap } from 'rxjs';
import { GlobalState } from '../state/app.reducer';
import { Store } from '@ngrx/store';
import { RolesAccount } from 'src/app/pages/auth/interfaces/auth.constant';
 
@Injectable({
  providedIn: 'root'
})
export class AdministratorGuard implements CanActivate, CanLoad {
 
  constructor(private route: Router,private store: Store<GlobalState>) {}
 
  canActivate( route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean | UrlTree> | Promise<boolean | UrlTree> | boolean | UrlTree {
    return this.isAdmin()
  }
  canLoad( route: Route, segments: UrlSegment[]): Observable<boolean | UrlTree> | Promise<boolean | UrlTree> | boolean | UrlTree {
    return this.isAdmin()
  }
 
  isAdmin():Observable<true | UrlTree>{    
    return this.store.select('authentication').pipe( 
      first(), // take of first value
      map(userStore => userStore.userLogged?.role || ''), 
      //TODO we hardcode the email of administrator until role is in JWT
      map(role => role === RolesAccount.ADMIN ? true : this.route.createUrlTree(['/home']))
    );    
  }
}

I run npm run coverage

And this is the uncovered block (I need create unit test for canActivate, canLoad, and isAdmin

Uncovered block

And this is my unit testing file (default testing)

import { TestBed } from '@angular/core/testing';
import { RouterTestingModule } from '@angular/router/testing';
import { MockStore, provideMockStore } from '@ngrx/store/testing';
import { AuthComponent } from 'src/app/pages/auth/auth.component';
import { IUserState } from 'src/app/pages/auth/interfaces/auth.interfaces';

import { AdministratorGuard } from '../administrator.guard';

fdescribe('AdministratorGuard', () => {
  let guard: AdministratorGuard;
  let store: MockStore<IUserState>;  

  beforeEach(() => {
    TestBed.configureTestingModule({
      imports:[
        RouterTestingModule.withRoutes(
          [
            {
              path: 'auth',
              component: AuthComponent
            },
          ]
        ),
      ],
      providers:[
        provideMockStore({}),
      ]
    });
    guard = TestBed.inject(AdministratorGuard);
    store = TestBed.inject(MockStore);
  });

  it('should be created', () => {
    expect(guard).toBeTruthy();
  });
});

Thanks in advance

CodePudding user response:

With how you have it defined you would need to add your guard to the auth path that you have defined.

RouterTestingModule.withRoutes(
[
  {
    path: 'auth',
    canActivate: [AdministratorGuard],
    component: AuthComponent
  },
]),

Then in your it you would need to route to your 'auth' path to fire that guard.

Now, I don't like this approach for the following reason - you are testing that the RouterTestingModule behaves like the real RouterModule.

Instead I would suggest testing your guard like a service.

beforeEach(() => {
  TestBed.configureTestingModule({

    providers: [
      AdministratorGuard,
      provideMockStore({}),
    ]
  });
  guard = TestBed.inject(AdministratorGuard);
  store = TestBed.inject(MockStore);
});

it('should allow canActivate', (done) => {
  // ... setup to allow it to return true
  guard.canActivate(null, null).subscribe(x => {
    // code to make
    expect(x).toBe(true);
    done();
  });
});

it('should deny canActivate', (done) => {
  // ... setup to return false
  guard.canActivate(null, null).subscribe(x => {
    // code to make
    expect(x).toBe(false);
    done();
  });
});

Then add your other tests for canLoad to do the same. You may need more than one true/false scenario for each. You won't want to test the isAdmin function directly as it isn't the public interface for this service.

CodePudding user response:

Thanks Wesley for your comment. Here's the solution

This is my code of guard

import { Injectable } from '@angular/core';
import { ActivatedRouteSnapshot, CanActivate, CanLoad, Route, Router, RouterStateSnapshot, UrlSegment, UrlTree } from '@angular/router';
import { first, map, Observable, tap } from 'rxjs';
import { GlobalState } from '../state/app.reducer';
import { Store } from '@ngrx/store';
import { RolesAccount } from 'src/app/pages/auth/interfaces/auth.constant';

@Injectable({
  providedIn: 'root'
})
export class AdministratorGuard implements CanActivate, CanLoad {

  constructor(private route: Router,private store: Store<GlobalState>) {}

  canActivate(): Observable<true | UrlTree> {
    return this.isAdmin()
  }
  canLoad(): Observable<true | UrlTree>{
    return this.isAdmin()
  }

  isAdmin():Observable<true | UrlTree>{    
    return this.store.select('authentication').pipe( 
      first(), // take of first value
      map(userStore => userStore?.userLogged?.role || ''), 
      //TODO we hardcode the email of administrator until role is in JWT
      map(role => role === RolesAccount.ADMIN ? true : this.route.createUrlTree(['/home']))
    );    
  }
}

And this is the test

import { TestBed } from '@angular/core/testing';
import { ActivatedRouteSnapshot, Router, RouterStateSnapshot } from '@angular/router';
import { RouterTestingModule } from '@angular/router/testing';
import { MockStore, provideMockStore } from '@ngrx/store/testing';
import { lastValueFrom, of } from 'rxjs';
import { AuthComponent } from 'src/app/pages/auth/auth.component';
import { RolesAccount } from 'src/app/pages/auth/interfaces/auth.constant';
import { IUserState } from 'src/app/pages/auth/interfaces/auth.interfaces';

import { AdministratorGuard } from '../administrator.guard';

describe('AdministratorGuard', () => {
  let guard: AdministratorGuard;
  let store: MockStore<IUserState>;  
  let defaultState:IUserState = {
    authentication:{
      userLogged:{
        name:'',
        email:'',
        phone:'',
        accessToken:'',
        refreshToken:'',
        role: RolesAccount.USER
      },  
    }  
  }
  beforeEach(() => {
    const routerStub = {
      events: of('/'),
      createUrlTree: (commands: any, navExtras = {}) => {}
    };
    TestBed.configureTestingModule({
      imports:[
        RouterTestingModule.withRoutes(
          [
            {
              path: 'auth',
              component: AuthComponent
            },
          ]
        ),
      ],
      providers:[
        provideMockStore({
          initialState:defaultState
        }),
        { provide: Router, useValue: routerStub}        
      ]
    });
    guard = TestBed.inject(AdministratorGuard);
    store = TestBed.inject(MockStore);

  });

  it('should be created', () => {
    expect(guard).toBeTruthy();
  });

  it('can Activate to be True ', () => {
    const storeSpy = spyOn(store, 'select').and.callThrough();
    guard.canActivate()
    expect(storeSpy).toHaveBeenCalledTimes(1);
  });

  it('can canLoad to be True ', () => {
    const storeSpy = spyOn(store, 'select').and.callThrough();
    guard.canLoad()
    expect(storeSpy).toHaveBeenCalledTimes(1);
  })

  it('validate role ADMIN',async () => {
    const nextState:IUserState = {  
      authentication:{
        userLogged:{
          name:'Test',
          email:'[email protected]',
          phone:' 5411557788',
          accessToken:'asfksakmfaskmfsakm',
          refreshToken:'safla25l4235lllfs',
          role: RolesAccount.ADMIN
        },    
      }     
    }

    store.setState(nextState);

    const isAdmin = await lastValueFrom(guard.isAdmin())

    expect(isAdmin).toBeTrue()
  })

  it('if is not admin,navigate home',async () => {
    const nextState:IUserState = {  
      authentication:null
    }

    store.setState(nextState);

    const isAdmin = await lastValueFrom(guard.isAdmin())

    expect(isAdmin).toBeUndefined()
  })
});
  • Related