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:
TestBedconGlobalServicereemplazado por{ profile: profile$ }(unReplaySubject<User>(1)) yRoutercomojasmine.createSpyObj('Router', ['navigate']). El helperemitUser(userType)emite un usuario con eseuserType.
AdminGuard
| it() título | Escenario | Resultado esperado | Mocks/spies |
|---|---|---|---|
should allow activation when the user is admin | Dado un perfil con userType admin, cuando se suscribe a canActivate(), entonces emite. | Emite true. | profile$ (ReplaySubject) |
should block activation when the user is NOT admin | Dado un perfil con userType atc (no admin), cuando se suscribe a canActivate(). | Emite false. | profile$ |
should block activation when the user is agent | Dado un perfil con userType agent, cuando se suscribe a canActivate(). | Emite false. | profile$ |
should redirect to /dashboard when the user is NOT admin | Dado 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 admin | Dado un perfil admin, cuando se evalúa el guard. | router.navigate no se llama. | Router.navigate spy |
should treat userType comparison as case-insensitive | Dado 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:
TestBedconprovideMockStore(),AuthServicecomocreateSpyObj(['isLoggedOut']),RoutercomocreateSpyObj(['navigate'])eIframeDetectionServicecomo{ isIframe: () => false }.MockStore.dispatchse espía conspyOndonde hace falta.
AuthGuard
| it() título | Escenario | Resultado esperado | Mocks/spies |
|---|---|---|---|
should allow activation when the user is logged in | Dado isLoggedOut() devuelve false, cuando se llama canActivate(). | Devuelve true. | AuthService.isLoggedOut |
should block activation when the user is logged out | Dado isLoggedOut() devuelve true, cuando se llama canActivate(). | Devuelve false. | AuthService.isLoggedOut |
should redirect to /auth/login when the user is logged out | Dado 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 out | Dado 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 in | Dado 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:
TestBedconAuthServicecomocreateSpyObj(['isLoggedIn'])yRoutercomocreateSpyObj(['navigateByUrl']).
LoggedInAuthGuard
| it() título | Escenario | Resultado esperado | Mocks/spies |
|---|---|---|---|
should allow access to the login page when the user is NOT logged in | Dado isLoggedIn() devuelve false, cuando se llama canActivate(). | Devuelve true. | AuthService.isLoggedIn |
should block access to the login page when the user IS already logged in | Dado isLoggedIn() devuelve true, cuando se llama canActivate(). | Devuelve false. | AuthService.isLoggedIn |
should redirect to / when the user is already logged in | Dado 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 in | Dado 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 leeroute.data.permission). - Mocking:
TestBedconRouter(createSpyObj(['navigate'])) yMyPermissionsService(createSpyObj(['hasPermission']), que devuelveof(true)oof(false)). El helperrunGuard(permiso)construye unActivatedRouteSnapshotfalso y ejecuta el guard conTestBed.runInInjectionContext.
permissionGuard
| it() título | Escenario | Resultado esperado | Mocks/spies |
|---|---|---|---|
should allow activation when the resolved set has the required acronym | Dado 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 acronym | Dado hasPermission resuelve false para CONTACT_DELETE. | Emite false. | MyPermissionsService.hasPermission |
should redirect to /profile/profile-detail when access is denied | Dado acceso denegado (false). | router.navigate se llama con ['/profile/profile-detail']. | MyPermissionsService.hasPermission, Router.navigate |
should NOT redirect when access is granted | Dado 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 cabecerasX-Device-TokenyX-Lang). - Mocking:
TestBedconDeviceTokenServicecomo{ getOrCreateDeviceToken: () => 'device-123' }yLanguageServicecomo{ code }(unBehaviorSubject<LanguageCode>inicialES). ElHttpHandlerfalso captura la petición recibida enhandled.
DeviceTokenInterceptor
| it() título | Escenario | Resultado esperado | Mocks/spies |
|---|---|---|---|
sends X-Lang with the tenant language from the store | Dado 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 language | Dado 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 caller | Dado 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 desdehttp-error.interceptor.ts). - Mocking:
TestBedconprovideMockStore(),ToastrNotificationService(showNotification,getMessageTestque devuelve el mismo mensaje),TranslateService(instantdevuelve la clave),Router(navigate),IframeDetectionService(isIframedevuelvefalse) yFrameBridgeServicereal.store.dispatchse espía. El helperhandlerThatThrows(status, message?)devuelve unHttpHandlerque lanza unHttpErrorResponsecon ese estado.
ErrorInterceptor
| it() título | Escenario | Resultado esperado | Mocks/spies |
|---|---|---|---|
should show a distinct "forbidden" toast on a 403 response | Dado 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 response | Dado un 403. | No se despacha UserActions.userLogout(). | store.dispatch spy |
should NOT navigate away on a 403 response | Dado 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 403 | Dado 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 response | Dado 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ítulo | Escenario | Resultado esperado | Mocks/spies |
|---|---|---|---|
does NOT show the generic error toast when the error body carries a data.code | Dado 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.code | Dado 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 present | Dado 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.