В Angular защита — это фундаментальная функция, которая защищает маршруты внутри приложения. Охранники проверяют, выполняются ли определенные условия, прежде чем разрешить пользователям доступ к определенным маршрутам. Guards можно использовать для аутентификации и авторизации, управления доступом на основе ролей, проверки флагов функций и многого другого.

Angular предоставляет несколько типов охранников, которые можно использовать для разных целей, например, canActivate, canActivateChild, canDeactivate и canLoad. Эти охранники определяются как классы, которые реализуют определенные интерфейсы, предоставляемые инфраструктурой Angular. Когда защита добавляется в массив canActivate маршрута, она вызывается перед тем, как позволить пользователю перейти к желаемому маршруту.

В этом примере у нас есть маршрут Dashboard, для доступа к которому требуется роль администратора.

// routes.ts
{
  path: 'dashboard',
  loadComponent: () => import('./pages/dashboard.component')
},

Традиционный (класс) Router Guard

Чтобы предотвратить доступ пользователя без прав администратора к этому маршруту, мы можем использовать защиту класса, как показано ниже.

@Injectable({providedIn: 'root'})
export class AuthGuard implements CanActivate {
  constructor(private authService: AuthService, private router: Router) {}

  canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) {
     return this.authService.isAdmin().pipe(
       tap(isAdmin =>  !isAdmin ?  this.router.navigate(['/login']) : true)
     )
  }
}

Затем мы можем применить защиту к маршрутам.

{
  path: 'dashboard',
  canActivate: [AuthGuard],
  loadComponent: () => import('./pages/dashboard.component')
},

Этот старый подход работает, но на основе углового опроса:

Мы получили отзывы от нескольких разработчиков, которые говорят о том, что разработчики хотят меньше шаблонов и больше производительности.

Что привело к развитию функциональной защиты маршрутизатора в Angular v14.

Функциональные средства защиты маршрутизатора с inject() легкие, эргономичные и более компонуемые, чем средства защиты на основе классов.

Функциональная защита маршрутизатора с помощью функции inject()

Функция внедрения — это новая функция в Angular 14, которая помогает внедрять внешние зависимости в наши функции.

const authGuard = () => {
  const authService = inject(AuthService)
  const router = inject(Router)

  if (authService.isAdmin()) {
    return true
  }

  return router.navigate(['/login'])
}

Регистрация функции функциональной защиты аналогична регистрации на основе класса.

// routes.ts
{
  path: 'dashboard',
  canActivate: [authGuard],
  loadComponent: () => import('./pages/dashboard.component')
},

Тестирование функциональной защиты

Тестирование защиты в Angular является важной частью процесса разработки, чтобы убедиться, что защита работает правильно и обеспечивает желаемую функциональность. Честно говоря, мне не удалось найти много упоминаний о тестировании inject() внутри функциональной защиты маршрутизатора. Вот пример, который работает для меня. Идея смоделировать inject() внутри пакета @angular/core.

import { authGuard } from './auth.guard'
import * as angularCore from '@angular/core'

// METHOD 1: Mocking inject()
const isAdminMock= jest.fn()

describe('AuthGuard', () => {
  beforeEach(() => {
    const injectSpy = jest.spyOn(angularCore, 'inject')
    injectSpy.mockImplementation((providerToken: unknown) => {
      if (providerToken === AuthService) {
        return {
          isAdmin: isAdminMock,
        }
      }
    })
  })

  afterEach(() => {
    jest.clearAllMocks()
  })

  it('should return true when user is admin', () => {
    isAdminMock.mockReturnValue(true)
    expect(authGuard()).toBe(true)
  })
})

// METHOD 2: using TestBed
describe('AuthGuard', () => {
  it('should return true', () => {
    TestBed.configureTestingModule({
      providers: [
        {
          provide: AuthService,
          useValue: { isAdmin: () => true },
        },
      ],
    });

    const guard = TestBed.runInInjectionContext(authGuard);
    expect(guard).toBeTruthy();
  });
});

Я надеюсь, что вы найдете этот пост полезным. Если у вас есть какие-либо улучшения, пожалуйста, оставьте комментарий :)