1+ import {
2+ ActivatedRouteSnapshot ,
3+ RouterStateSnapshot
4+ } from '@angular/router' ;
15import { UserAuthGuard } from './user-auth-guard.service' ;
26import {
37 BehaviorSubject ,
4- EMPTY
8+ EMPTY ,
9+ ReplaySubject
510} from 'rxjs' ;
11+ import { UserManagementService } from './user-management.service' ;
12+
13+
14+ type UserStub = { id : string } | null ;
15+
16+ const route = { } as ActivatedRouteSnapshot ;
617
18+ function state ( url : string ) : RouterStateSnapshot {
19+ return { url} as RouterStateSnapshot ;
20+ }
21+
22+ function createSnackRef ( ) {
23+ return {
24+ onAction : jasmine . createSpy ( 'onAction' ) . and . returnValue ( EMPTY ) ,
25+ afterDismissed : jasmine . createSpy ( 'afterDismissed' ) . and . returnValue ( EMPTY ) ,
26+ dismiss : jasmine . createSpy ( 'dismiss' )
27+ } ;
28+ }
729
8- function buildGuard ( loggedUser : any = null ) {
9- const loggedUser$ = new BehaviorSubject < any > ( loggedUser ) ;
10- const snackRef = { onAction : jasmine . createSpy ( 'onAction' ) . and . returnValue ( EMPTY ) , _open : jasmine . createSpy ( '_open' ) } ;
30+ function buildGuard ( loggedUser : UserStub = null ) {
31+ const loggedUser$ = new BehaviorSubject < UserStub > ( loggedUser ) ;
32+ const authRestored$ = new BehaviorSubject < boolean > ( true ) ;
33+ const snackRef = createSnackRef ( ) ;
1134 const snackBar = jasmine . createSpyObj ( 'MatSnackBar' , { open : snackRef } ) ;
1235 const router = jasmine . createSpyObj ( 'Router' , [ 'navigate' ] ) ;
1336 const guard = new UserAuthGuard (
1437 snackBar , router ,
15- { loggedUser$} as any
38+ { loggedUser$, authRestored$ } as unknown as UserManagementService
1639 ) ;
17- return { guard, snackBar, router, snackRef} ;
40+ return { guard, snackBar, router, snackRef, authRestored$ , loggedUser$ } ;
1841}
1942
2043
2144describe ( 'UserAuthGuard' , ( ) => {
2245 it ( 'returns true when user is logged in' , ( done ) => {
2346 const { guard} = buildGuard ( { id : 'user-1' } ) ;
24- guard . canActivate ( { } as any , { url : '/protected' } as any ) . subscribe ( can => {
47+ guard . canActivate ( route , state ( '/protected' ) ) . subscribe ( can => {
2548 expect ( can ) . toBeTrue ( ) ;
2649 done ( ) ;
2750 } ) ;
2851 } ) ;
2952
3053 it ( 'returns false when user is not logged in' , ( done ) => {
3154 const { guard} = buildGuard ( null ) ;
32- guard . canActivate ( { } as any , { url : '/protected' } as any ) . subscribe ( can => {
55+ guard . canActivate ( route , state ( '/protected' ) ) . subscribe ( can => {
3356 expect ( can ) . toBeFalse ( ) ;
3457 done ( ) ;
3558 } ) ;
3659 } ) ;
3760
3861 it ( 'opens snackbar when user is not logged in' , ( done ) => {
3962 const { guard, snackBar} = buildGuard ( null ) ;
40- guard . canActivate ( { } as any , { url : '/protected' } as any ) . subscribe ( ( ) => {
63+ guard . canActivate ( route , state ( '/protected' ) ) . subscribe ( ( ) => {
4164 expect ( snackBar . open ) . toHaveBeenCalledWith ( 'Sign in to use this feature.' , 'Sign in' , jasmine . anything ( ) ) ;
4265 done ( ) ;
4366 } ) ;
4467 } ) ;
4568
4669 it ( 'navigates to login with returnUrl when user is not logged in' , ( done ) => {
4770 const { guard, router} = buildGuard ( null ) ;
48- guard . canActivate ( { } as any , { url : '/my-page' } as any ) . subscribe ( ( ) => {
71+ guard . canActivate ( route , state ( '/my-page' ) ) . subscribe ( ( ) => {
4972 expect ( router . navigate ) . toHaveBeenCalledWith ( [ '/auth/login' ] , jasmine . objectContaining ( { queryParams : { returnUrl : '/my-page' } } ) ) ;
5073 done ( ) ;
5174 } ) ;
5275 } ) ;
5376
5477 it ( 'does not navigate or open snackbar when user is logged in' , ( done ) => {
5578 const { guard, snackBar, router} = buildGuard ( { id : 'user-1' } ) ;
56- guard . canActivate ( { } as any , { url : '/protected' } as any ) . subscribe ( ( ) => {
79+ guard . canActivate ( route , state ( '/protected' ) ) . subscribe ( ( ) => {
80+ expect ( snackBar . open ) . not . toHaveBeenCalled ( ) ;
81+ expect ( router . navigate ) . not . toHaveBeenCalled ( ) ;
82+ done ( ) ;
83+ } ) ;
84+ } ) ;
85+
86+ it ( 'waits for auth restoration before allowing a restored user' , ( done ) => {
87+ const loggedUser$ = new ReplaySubject < UserStub > ( 1 ) ;
88+ const authRestored$ = new BehaviorSubject < boolean > ( false ) ;
89+ const snackRef = createSnackRef ( ) ;
90+ const snackBar = jasmine . createSpyObj ( 'MatSnackBar' , { open : snackRef } ) ;
91+ const router = jasmine . createSpyObj ( 'Router' , [ 'navigate' ] ) ;
92+ const guard = new UserAuthGuard ( snackBar , router , { loggedUser$, authRestored$} as unknown as UserManagementService ) ;
93+
94+ guard . canActivate ( route , state ( '/protected' ) ) . subscribe ( can => {
95+ expect ( can ) . toBeTrue ( ) ;
5796 expect ( snackBar . open ) . not . toHaveBeenCalled ( ) ;
5897 expect ( router . navigate ) . not . toHaveBeenCalled ( ) ;
5998 done ( ) ;
6099 } ) ;
100+
101+ loggedUser$ . next ( { id : 'user-1' } ) ;
102+ expect ( snackBar . open ) . not . toHaveBeenCalled ( ) ;
103+ expect ( router . navigate ) . not . toHaveBeenCalled ( ) ;
104+
105+ authRestored$ . next ( true ) ;
106+ } ) ;
107+
108+ it ( 'waits for auth restoration before rejecting a restored signed-out session' , ( done ) => {
109+ const loggedUser$ = new ReplaySubject < UserStub > ( 1 ) ;
110+ const authRestored$ = new BehaviorSubject < boolean > ( false ) ;
111+ const snackRef = createSnackRef ( ) ;
112+ const snackBar = jasmine . createSpyObj ( 'MatSnackBar' , { open : snackRef } ) ;
113+ const router = jasmine . createSpyObj ( 'Router' , [ 'navigate' ] ) ;
114+ const guard = new UserAuthGuard ( snackBar , router , { loggedUser$, authRestored$} as unknown as UserManagementService ) ;
115+
116+ guard . canActivate ( route , state ( '/protected' ) ) . subscribe ( can => {
117+ expect ( can ) . toBeFalse ( ) ;
118+ expect ( snackBar . open ) . toHaveBeenCalledTimes ( 1 ) ;
119+ expect ( router . navigate ) . toHaveBeenCalledWith ( [ '/auth/login' ] , jasmine . objectContaining ( { queryParams : { returnUrl : '/protected' } } ) ) ;
120+ done ( ) ;
121+ } ) ;
122+
123+ loggedUser$ . next ( null ) ;
124+ expect ( snackBar . open ) . not . toHaveBeenCalled ( ) ;
125+ expect ( router . navigate ) . not . toHaveBeenCalled ( ) ;
126+
127+ authRestored$ . next ( true ) ;
128+ } ) ;
129+
130+ it ( 'reuses an active sign-in snackbar across repeated guard denials' , ( done ) => {
131+ const { guard, snackBar} = buildGuard ( null ) ;
132+ let emissions = 0 ;
133+
134+ guard . canActivate ( route , state ( '/first' ) ) . subscribe ( ( ) => {
135+ emissions ++ ;
136+ } ) ;
137+ guard . canActivate ( route , state ( '/second' ) ) . subscribe ( ( ) => {
138+ emissions ++ ;
139+ expect ( emissions ) . toBe ( 2 ) ;
140+ expect ( snackBar . open ) . toHaveBeenCalledTimes ( 1 ) ;
141+ done ( ) ;
142+ } ) ;
61143 } ) ;
62144} ) ;
0 commit comments