Skip to main content

Guards e interceptores

Casos de prueba de los guards de ruta y de los interceptores HTTP del núcleo del frontend (src/app/core/guards y src/app/core/interceptors). Los guards se prueban con TestBed inyectando la clase real y sustituyendo sus dependencias con jasmine.createSpyObj o useValue. Los interceptores se invocan directamente con intercept(req, handler) usando un HttpHandler falso. No hay xit, fdescribe ni specs vacíos.

Guards​

src/app/core/guards/admin.guard.spec.ts​

  • SUT: AdminGuard (canActivate() devuelve un observable).
  • Mocking: TestBed con GlobalService reemplazado por { profile: profile$ } (un ReplaySubject<User>(1)) y Router como jasmine.createSpyObj('Router', ['navigate']). El helper emitUser(userType) emite un usuario con ese userType.

AdminGuard​

it() títuloEscenarioResultado esperadoMocks/spies
should allow activation when the user is adminDado un perfil con userType admin, cuando se suscribe a canActivate(), entonces emite.Emite true.profile$ (ReplaySubject)
should block activation when the user is NOT adminDado un perfil con userType atc (no admin), cuando se suscribe a canActivate().Emite false.profile$
should block activation when the user is agentDado un perfil con userType agent, cuando se suscribe a canActivate().Emite false.profile$
should redirect to /dashboard when the user is NOT adminDado un perfil atc, cuando se evalúa el guard.router.navigate se llama con ['/dashboard'].Router.navigate spy
should NOT redirect when the user is adminDado un perfil admin, cuando se evalúa el guard.router.navigate no se llama.Router.navigate spy
should treat userType comparison as case-insensitiveDado un perfil con userType Admin (mayúscula inicial), cuando se evalúa el guard.Emite true (la comparación ignora mayúsculas).profile$

Total del archivo: 6 tests.

src/app/core/guards/auth.guard.spec.ts​

  • SUT: AuthGuard (canActivate() síncrono).
  • Mocking: TestBed con provideMockStore(), AuthService como createSpyObj(['isLoggedOut']), Router como createSpyObj(['navigate']) e IframeDetectionService como { isIframe: () => false }. MockStore.dispatch se espía con spyOn donde hace falta.

AuthGuard​

it() títuloEscenarioResultado esperadoMocks/spies
should allow activation when the user is logged inDado isLoggedOut() devuelve false, cuando se llama canActivate().Devuelve true.AuthService.isLoggedOut
should block activation when the user is logged outDado isLoggedOut() devuelve true, cuando se llama canActivate().Devuelve false.AuthService.isLoggedOut
should redirect to /auth/login when the user is logged outDado sesión cerrada, cuando se llama canActivate().router.navigate se llama con ['/auth/login'].AuthService.isLoggedOut, Router.navigate
should dispatch userLogout when the user is logged outDado sesión cerrada y store.dispatch espiado, cuando se llama canActivate().Se despacha UserActions.userLogout().AuthService.isLoggedOut, spyOn(store, 'dispatch')
should NOT redirect when the user is logged inDado sesión activa, cuando se llama canActivate().router.navigate no se llama.AuthService.isLoggedOut, Router.navigate

Total del archivo: 5 tests.

src/app/core/guards/loggedIn.guard.spec.ts​

  • SUT: LoggedInAuthGuard (protege la página de login para usuarios ya autenticados).
  • Mocking: TestBed con AuthService como createSpyObj(['isLoggedIn']) y Router como createSpyObj(['navigateByUrl']).

LoggedInAuthGuard​

it() títuloEscenarioResultado esperadoMocks/spies
should allow access to the login page when the user is NOT logged inDado isLoggedIn() devuelve false, cuando se llama canActivate().Devuelve true.AuthService.isLoggedIn
should block access to the login page when the user IS already logged inDado isLoggedIn() devuelve true, cuando se llama canActivate().Devuelve false.AuthService.isLoggedIn
should redirect to / when the user is already logged inDado usuario ya autenticado, cuando se llama canActivate().router.navigateByUrl se llama con '/'.AuthService.isLoggedIn, Router.navigateByUrl
should NOT redirect when the user is NOT logged inDado usuario no autenticado, cuando se llama canActivate().router.navigateByUrl no se llama.AuthService.isLoggedIn, Router.navigateByUrl

Total del archivo: 4 tests.

src/app/core/guards/permission.guard.spec.ts​

  • SUT: permissionGuard (guard funcional que lee route.data.permission).
  • Mocking: TestBed con Router (createSpyObj(['navigate'])) y MyPermissionsService (createSpyObj(['hasPermission']), que devuelve of(true) o of(false)). El helper runGuard(permiso) construye un ActivatedRouteSnapshot falso y ejecuta el guard con TestBed.runInInjectionContext.

permissionGuard​

it() títuloEscenarioResultado esperadoMocks/spies
should allow activation when the resolved set has the required acronymDado hasPermission resuelve true para CONTACT_READ, cuando se ejecuta el guard.Emite true y hasPermission se llamó con 'CONTACT_READ'.MyPermissionsService.hasPermission
should block activation when the resolved set is missing the required acronymDado hasPermission resuelve false para CONTACT_DELETE.Emite false.MyPermissionsService.hasPermission
should redirect to /profile/profile-detail when access is deniedDado acceso denegado (false).router.navigate se llama con ['/profile/profile-detail'].MyPermissionsService.hasPermission, Router.navigate
should NOT redirect when access is grantedDado acceso concedido (true).router.navigate no se llama.MyPermissionsService.hasPermission, Router.navigate

Total del archivo: 4 tests.

Interceptores​

src/app/core/interceptors/device-token.interceptor.spec.ts​

  • SUT: DeviceTokenInterceptor (añade las cabeceras X-Device-Token y X-Lang).
  • Mocking: TestBed con DeviceTokenService como { getOrCreateDeviceToken: () => 'device-123' } y LanguageService como { code } (un BehaviorSubject<LanguageCode> inicial ES). El HttpHandler falso captura la petición recibida en handled.

DeviceTokenInterceptor​

it() títuloEscenarioResultado esperadoMocks/spies
sends X-Lang with the tenant language from the storeDado que el idioma del store cambia a EN, cuando se intercepta un GET /api/v1/tickets.La petición lleva X-Lang: en y X-Device-Token: device-123.code BehaviorSubject, DeviceTokenService, handler falso
ignores the browser languageDado que navigator.language es en-US y el idioma del store es ES.La petición lleva X-Lang: es.spyOnProperty(navigator, 'language')
does not override an X-Lang already set by the callerDado una petición que ya trae X-Lang: en (idioma del store ES).Se conserva X-Lang: en.handler falso

Total del archivo: 3 tests.

src/app/core/interceptors/http-error.interceptor.spec.ts​

  • SUT: ErrorInterceptor (clase exportada desde http-error.interceptor.ts).
  • Mocking: TestBed con provideMockStore(), ToastrNotificationService (showNotification, getMessageTest que devuelve el mismo mensaje), TranslateService (instant devuelve la clave), Router (navigate), IframeDetectionService (isIframe devuelve false) y FrameBridgeService real. store.dispatch se espía. El helper handlerThatThrows(status, message?) devuelve un HttpHandler que lanza un HttpErrorResponse con ese estado.

ErrorInterceptor​

it() títuloEscenarioResultado esperadoMocks/spies
should show a distinct "forbidden" toast on a 403 responseDado que el backend responde 403.Se muestra una notificación con título words.forbiddenActionTitle y mensaje words.forbiddenAction.showNotification, TranslateService.instant
should NOT dispatch userLogout on a 403 responseDado un 403.No se despacha UserActions.userLogout().store.dispatch spy
should NOT navigate away on a 403 responseDado un 403.router.navigate no se llama.Router.navigate spy
should still dispatch userLogout on a 401 response (unchanged behavior)Dado un 401.Se despacha UserActions.userLogout().store.dispatch spy
should show the forbidden toast (not the generic 401 logout flow) when a stale frontend permission cache lets a request through but the backend still rejects it with 403Dado una caché de permisos obsoleta que dejó pasar la petición y el backend la rechaza con 403 (Forbidden: missing CONTACT_READ).Se muestra el toast de prohibido, no se despacha userLogout y no hay navegación.showNotification, store.dispatch, Router.navigate
should use the generic error toast for a non-401/403 responseDado un 500 con mensaje boom.Se muestra una notificación con título Error y mensaje boom.showNotification, getMessageTest

typed-code passthrough (sdd/ticket-lock D5)​

Usa un helper adicional handlerThatThrowsTypedError(status, code) cuyo cuerpo de error incluye data.code.

it() títuloEscenarioResultado esperadoMocks/spies
does NOT show the generic error toast when the error body carries a data.codeDado un 409 con data.code TICKET_LOCKED.showNotification no se llama.showNotification
rethrows the original HttpErrorResponse (not a generic Error) so callers can read error.error.data.codeDado un 409 con data.code TICKET_LOCKED.El error propagado es una instancia de HttpErrorResponse y err.error.data.code es TICKET_LOCKED.handler falso
still uses the generic error toast + wrapped Error when no data.code is presentDado un 500 sin data.code.Se llama showNotification y el error propagado no es HttpErrorResponse (se envuelve en un Error).showNotification

Total del archivo: 9 tests (6 + 3).

Total de la página​

6 archivos de spec, 31 tests (it()): 6 + 5 + 4 + 4 + 3 + 9.