@@ -10,8 +10,7 @@ import { AuthInterceptorService } from './auth-interceptor.service';
1010describe ( 'AuthInterceptorService' , ( ) => {
1111 const authService = {
1212 getInternalAccessToken : vi . fn < ( ) => string | null > ( ) ,
13- internalRefreshToken : vi . fn < ( ) => Observable < { accessToken : string ; refreshToken : string } > > ( ) ,
14- saveInternalTokens : vi . fn < ( accessToken : string , refreshToken : string ) => void > ( ) ,
13+ ensureAccessToken : vi . fn < ( options ?: { forceRefresh ?: boolean } ) => Observable < string > > ( ) ,
1514 logout : vi . fn < ( ) => void > ( ) ,
1615 } ;
1716
@@ -21,8 +20,7 @@ describe('AuthInterceptorService', () => {
2120 beforeEach ( ( ) => {
2221 vi . restoreAllMocks ( ) ;
2322 authService . getInternalAccessToken . mockReset ( ) ;
24- authService . internalRefreshToken . mockReset ( ) ;
25- authService . saveInternalTokens . mockReset ( ) ;
23+ authService . ensureAccessToken . mockReset ( ) ;
2624 authService . logout . mockReset ( ) ;
2725
2826 TestBed . resetTestingModule ( ) ;
@@ -82,7 +80,7 @@ describe('AuthInterceptorService', () => {
8280
8381 it ( 'retries a 401 request after a successful refresh' , async ( ) => {
8482 authService . getInternalAccessToken . mockReturnValue ( 'expired-token' ) ;
85- authService . internalRefreshToken . mockReturnValue ( of ( { accessToken : 'fresh-token' , refreshToken : 'refresh-token' } ) ) ;
83+ authService . ensureAccessToken . mockReturnValue ( of ( 'fresh-token' ) ) ;
8684
8785 const next = vi . fn ( ( request : HttpRequest < unknown > ) => {
8886 const authHeader = request . headers . get ( 'Authorization' ) ;
@@ -97,34 +95,47 @@ describe('AuthInterceptorService', () => {
9795 next
9896 ) ) ;
9997
100- expect ( authService . internalRefreshToken ) . toHaveBeenCalledOnce ( ) ;
101- expect ( authService . saveInternalTokens ) . toHaveBeenCalledWith ( 'fresh-token' , 'refresh-token' ) ;
98+ expect ( authService . ensureAccessToken ) . toHaveBeenCalledWith ( { forceRefresh : true } ) ;
10299 expect ( ( response as HttpResponse < string > ) . body ) . toBe ( 'Bearer fresh-token' ) ;
103100 } ) ;
104101
105- it ( 'retries a 401 request without persisting tokens when the refresh response is incomplete ' , async ( ) => {
102+ it ( 'logs out when AuthService cannot refresh a complete token pair ' , async ( ) => {
106103 authService . getInternalAccessToken . mockReturnValue ( 'expired-token' ) ;
107- authService . internalRefreshToken . mockReturnValue ( of ( { accessToken : 'fresh-token' , refreshToken : '' } ) ) ;
104+ authService . ensureAccessToken . mockReturnValue ( throwError ( ( ) => new Error ( 'Authentication response did not include a complete token pair' ) ) ) ;
105+ const next = vi . fn ( ( ) => throwError ( ( ) => new HttpErrorResponse ( { status : 401 } ) ) ) ;
106+
107+ await expect ( firstValueFrom ( interceptor (
108+ new HttpRequest ( 'GET' , `${ apiUrl } /books` ) ,
109+ next
110+ ) ) ) . rejects . toBeInstanceOf ( Error ) ;
111+
112+ expect ( authService . ensureAccessToken ) . toHaveBeenCalledWith ( { forceRefresh : true } ) ;
113+ expect ( authService . logout ) . toHaveBeenCalledOnce ( ) ;
114+ } ) ;
115+
116+ it ( 'does not log out when the retried request fails after refresh succeeds' , async ( ) => {
117+ authService . getInternalAccessToken . mockReturnValue ( 'expired-token' ) ;
118+ authService . ensureAccessToken . mockReturnValue ( of ( 'fresh-token' ) ) ;
108119
109120 const next = vi . fn ( ( request : HttpRequest < unknown > ) => {
110121 if ( request . headers . get ( 'Authorization' ) === 'Bearer fresh-token' ) {
111- return of ( new HttpResponse ( { status : 200 , body : request . headers . get ( 'Authorization' ) } ) ) ;
122+ return throwError ( ( ) => new HttpErrorResponse ( { status : 500 } ) ) ;
112123 }
113124 return throwError ( ( ) => new HttpErrorResponse ( { status : 401 } ) ) ;
114125 } ) ;
115126
116- const response = await firstValueFrom ( interceptor (
127+ await expect ( firstValueFrom ( interceptor (
117128 new HttpRequest ( 'GET' , `${ apiUrl } /books` ) ,
118129 next
119- ) ) ;
130+ ) ) ) . rejects . toBeInstanceOf ( HttpErrorResponse ) ;
120131
121- expect ( authService . saveInternalTokens ) . not . toHaveBeenCalled ( ) ;
122- expect ( ( response as HttpResponse < string > ) . body ) . toBe ( 'Bearer fresh-token' ) ;
132+ expect ( authService . ensureAccessToken ) . toHaveBeenCalledWith ( { forceRefresh : true } ) ;
133+ expect ( authService . logout ) . not . toHaveBeenCalled ( ) ;
123134 } ) ;
124135
125136 it ( 'logs out when the refresh request fails' , async ( ) => {
126137 authService . getInternalAccessToken . mockReturnValue ( 'expired-token' ) ;
127- authService . internalRefreshToken . mockReturnValue (
138+ authService . ensureAccessToken . mockReturnValue (
128139 throwError ( ( ) => new HttpErrorResponse ( { status : 500 } ) )
129140 ) ;
130141
@@ -147,14 +158,14 @@ describe('AuthInterceptorService', () => {
147158 next
148159 ) ) ) . rejects . toBeInstanceOf ( HttpErrorResponse ) ;
149160
150- expect ( authService . internalRefreshToken ) . not . toHaveBeenCalled ( ) ;
161+ expect ( authService . ensureAccessToken ) . not . toHaveBeenCalled ( ) ;
151162 expect ( authService . logout ) . not . toHaveBeenCalled ( ) ;
152163 } ) ;
153164
154- it ( 'queues concurrent 401s behind a single refresh operation ' , async ( ) => {
165+ it ( 'delegates concurrent 401 refresh coordination to AuthService ' , async ( ) => {
155166 authService . getInternalAccessToken . mockReturnValue ( 'expired-token' ) ;
156- const refreshSubject = new Subject < { accessToken : string ; refreshToken : string } > ( ) ;
157- authService . internalRefreshToken . mockReturnValue ( refreshSubject . asObservable ( ) ) ;
167+ const refreshSubject = new Subject < string > ( ) ;
168+ authService . ensureAccessToken . mockReturnValue ( refreshSubject . asObservable ( ) ) ;
158169
159170 const next = vi . fn ( ( request : HttpRequest < unknown > ) => {
160171 const authHeader = request . headers . get ( 'Authorization' ) ;
@@ -167,12 +178,13 @@ describe('AuthInterceptorService', () => {
167178 const firstRequest = firstValueFrom ( interceptor ( new HttpRequest ( 'GET' , `${ apiUrl } /books` ) , next ) ) ;
168179 const secondRequest = firstValueFrom ( interceptor ( new HttpRequest ( 'GET' , `${ apiUrl } /libraries` ) , next ) ) ;
169180
170- refreshSubject . next ( { accessToken : 'refreshed-token' , refreshToken : 'refresh-token' } ) ;
181+ refreshSubject . next ( 'refreshed-token' ) ;
171182 refreshSubject . complete ( ) ;
172183
173184 const [ firstResponse , secondResponse ] = await Promise . all ( [ firstRequest , secondRequest ] ) ;
174185
175- expect ( authService . internalRefreshToken ) . toHaveBeenCalledOnce ( ) ;
186+ expect ( authService . ensureAccessToken ) . toHaveBeenCalledTimes ( 2 ) ;
187+ expect ( authService . ensureAccessToken ) . toHaveBeenCalledWith ( { forceRefresh : true } ) ;
176188 expect ( ( firstResponse as HttpResponse < string > ) . body ) . toBe ( 'Bearer refreshed-token' ) ;
177189 expect ( ( secondResponse as HttpResponse < string > ) . body ) . toBe ( 'Bearer refreshed-token' ) ;
178190 } ) ;
0 commit comments