@@ -13,7 +13,14 @@ vi.mock('../../src/service/auth-refresh.svc.ts', () => ({
1313 refreshTokens : vi . fn ( ) ,
1414} ) ) ;
1515
16- import { getAccessToken , logoutLocally , persistTokenResponse , requireAccessToken } from '../../src/service/auth.svc.ts' ;
16+ import {
17+ AuthError ,
18+ getAccessToken ,
19+ logoutLocally ,
20+ persistTokenResponse ,
21+ requireAccessToken ,
22+ requireAccessTokenForScan ,
23+ } from '../../src/service/auth.svc.ts' ;
1724import { refreshTokens } from '../../src/service/auth-refresh.svc.ts' ;
1825import {
1926 clearStoredTokens ,
@@ -77,4 +84,67 @@ describe('auth.svc', () => {
7784 await logoutLocally ( ) ;
7885 expect ( clearStoredTokens ) . toHaveBeenCalled ( ) ;
7986 } ) ;
87+
88+ describe ( 'requireAccessTokenForScan' , ( ) => {
89+ it ( 'returns token when access token is valid' , async ( ) => {
90+ ( getStoredTokens as Mock ) . mockResolvedValue ( { accessToken : 'valid-token' } ) ;
91+ ( isAccessTokenExpired as Mock ) . mockReturnValue ( false ) ;
92+
93+ const token = await requireAccessTokenForScan ( ) ;
94+ expect ( token ) . toBe ( 'valid-token' ) ;
95+ expect ( refreshTokens ) . not . toHaveBeenCalled ( ) ;
96+ } ) ;
97+
98+ it ( 'auto-refreshes when access token expired with valid refresh token' , async ( ) => {
99+ ( getStoredTokens as Mock ) . mockResolvedValue ( { accessToken : 'expired' , refreshToken : 'refresh-1' } ) ;
100+ ( isAccessTokenExpired as Mock ) . mockReturnValue ( true ) ;
101+ ( refreshTokens as Mock ) . mockResolvedValue ( { access_token : 'new-token' , refresh_token : 'refresh-2' } ) ;
102+
103+ const token = await requireAccessTokenForScan ( ) ;
104+ expect ( token ) . toBe ( 'new-token' ) ;
105+ expect ( refreshTokens ) . toHaveBeenCalledWith ( 'refresh-1' ) ;
106+ expect ( saveTokens ) . toHaveBeenCalledWith ( { accessToken : 'new-token' , refreshToken : 'refresh-2' } ) ;
107+ } ) ;
108+
109+ it ( 'throws AuthError with NOT_LOGGED_IN when no tokens exist' , async ( ) => {
110+ ( getStoredTokens as Mock ) . mockResolvedValue ( undefined ) ;
111+
112+ await expect ( requireAccessTokenForScan ( ) ) . rejects . toThrow ( AuthError ) ;
113+ await expect ( requireAccessTokenForScan ( ) ) . rejects . toMatchObject ( {
114+ code : 'NOT_LOGGED_IN' ,
115+ message : 'Please log in to perform a scan. To authenticate, run "hd auth login".' ,
116+ } ) ;
117+ } ) ;
118+
119+ it ( 'throws AuthError with NOT_LOGGED_IN when access token is missing' , async ( ) => {
120+ ( getStoredTokens as Mock ) . mockResolvedValue ( { refreshToken : 'refresh-only' } ) ;
121+
122+ await expect ( requireAccessTokenForScan ( ) ) . rejects . toThrow ( AuthError ) ;
123+ await expect ( requireAccessTokenForScan ( ) ) . rejects . toMatchObject ( {
124+ code : 'NOT_LOGGED_IN' ,
125+ } ) ;
126+ } ) ;
127+
128+ it ( 'throws AuthError with SESSION_EXPIRED when refresh fails' , async ( ) => {
129+ ( getStoredTokens as Mock ) . mockResolvedValue ( { accessToken : 'expired' , refreshToken : 'invalid-refresh' } ) ;
130+ ( isAccessTokenExpired as Mock ) . mockReturnValue ( true ) ;
131+ ( refreshTokens as Mock ) . mockRejectedValue ( new Error ( 'refresh failed' ) ) ;
132+
133+ await expect ( requireAccessTokenForScan ( ) ) . rejects . toThrow ( AuthError ) ;
134+ await expect ( requireAccessTokenForScan ( ) ) . rejects . toMatchObject ( {
135+ code : 'SESSION_EXPIRED' ,
136+ message : 'Your session is no longer valid. To re-authenticate, run "hd auth login".' ,
137+ } ) ;
138+ } ) ;
139+
140+ it ( 'throws AuthError with SESSION_EXPIRED when access token expired and no refresh token' , async ( ) => {
141+ ( getStoredTokens as Mock ) . mockResolvedValue ( { accessToken : 'expired' } ) ;
142+ ( isAccessTokenExpired as Mock ) . mockReturnValue ( true ) ;
143+
144+ await expect ( requireAccessTokenForScan ( ) ) . rejects . toThrow ( AuthError ) ;
145+ await expect ( requireAccessTokenForScan ( ) ) . rejects . toMatchObject ( {
146+ code : 'SESSION_EXPIRED' ,
147+ } ) ;
148+ } ) ;
149+ } ) ;
80150} ) ;
0 commit comments