@@ -123,3 +123,71 @@ def test_auth_flow_adds_authorization_header(mocker, httpx_mock: HTTPXMock):
123123 client = httpx .Client (auth = auth_instance )
124124 response = client .get ("http://example.com" )
125125 assert response .request .headers ["Authorization" ] == "auth_class_token"
126+
127+
128+ def test_auth_flow_retries_on_401_with_fresh_token (mocker , httpx_mock : HTTPXMock ):
129+ """When the server returns 401, auth_flow should force-refresh the token and retry."""
130+ mock_get_ansyId_token = mocker .patch (
131+ "ansys.conceptev.core.auth.get_ansyId_token" ,
132+ side_effect = ["expired_token" , "fresh_token" ],
133+ )
134+ auth_instance = auth .AnsysIDAuth ()
135+ httpx_mock .add_response (url = "http://example.com" , status_code = 401 )
136+ httpx_mock .add_response (url = "http://example.com" , status_code = 200 )
137+
138+ client = httpx .Client (auth = auth_instance )
139+ response = client .get ("http://example.com" )
140+
141+ assert response .status_code == 200
142+ assert mock_get_ansyId_token .call_count == 2
143+ # Second call must use force=True to bypass the MSAL cache
144+ assert mock_get_ansyId_token .call_args_list [1 ] == mocker .call (auth_instance .app , force = True )
145+ assert response .request .headers ["Authorization" ] == "fresh_token"
146+
147+
148+ def test_auth_flow_does_not_retry_on_other_errors (mocker , httpx_mock : HTTPXMock ):
149+ """Non-401 errors should not trigger a token refresh retry."""
150+ mock_get_ansyId_token = mocker .patch (
151+ "ansys.conceptev.core.auth.get_ansyId_token" , return_value = "auth_class_token"
152+ )
153+ auth_instance = auth .AnsysIDAuth ()
154+ httpx_mock .add_response (url = "http://example.com" , status_code = 403 )
155+
156+ client = httpx .Client (auth = auth_instance )
157+ response = client .get ("http://example.com" )
158+
159+ assert response .status_code == 403
160+ assert mock_get_ansyId_token .call_count == 1
161+
162+
163+ def test_auth_flow_token_expires_mid_sequence (mocker , httpx_mock : HTTPXMock ):
164+ """When a token expires mid-sequence, the failing request retries with a fresh token
165+ and all subsequent requests in the sequence also use the fresh token."""
166+ # Requests 1 & 2 succeed with the original token.
167+ # Request 3 gets a 401 (token just expired), then retries with a fresh token.
168+ # Request 4 should use the now-cached fresh token.
169+ token_sequence = [
170+ "original_token" , # req 1 – initial fetch
171+ "original_token" , # req 2 – initial fetch
172+ "original_token" , # req 3 – initial fetch (will be rejected)
173+ "fresh_token" , # req 3 – force-refresh after 401
174+ "fresh_token" , # req 4 – silent fetch (MSAL cache now holds fresh token)
175+ ]
176+ mock_get_ansyId_token = mocker .patch (
177+ "ansys.conceptev.core.auth.get_ansyId_token" , side_effect = token_sequence
178+ )
179+ auth_instance = auth .AnsysIDAuth ()
180+
181+ httpx_mock .add_response (url = "http://example.com" , status_code = 200 ) # req 1
182+ httpx_mock .add_response (url = "http://example.com" , status_code = 200 ) # req 2
183+ httpx_mock .add_response (url = "http://example.com" , status_code = 401 ) # req 3 – token expired
184+ httpx_mock .add_response (url = "http://example.com" , status_code = 200 ) # req 3 – retry
185+ httpx_mock .add_response (url = "http://example.com" , status_code = 200 ) # req 4
186+
187+ client = httpx .Client (auth = auth_instance )
188+ responses = [client .get ("http://example.com" ) for _ in range (4 )]
189+
190+ assert [r .status_code for r in responses ] == [200 , 200 , 200 , 200 ]
191+ assert mock_get_ansyId_token .call_count == 5
192+ # The force-refresh must have been triggered on the 4th call (req 3 retry)
193+ assert mock_get_ansyId_token .call_args_list [3 ] == mocker .call (auth_instance .app , force = True )
0 commit comments