diff --git a/tests/test_client_library.py b/tests/test_client_library.py index d6c2fa4..485fa25 100644 --- a/tests/test_client_library.py +++ b/tests/test_client_library.py @@ -29,6 +29,7 @@ import pytest import respx +from virl2_client.exceptions import APIError from virl2_client.models import Lab from virl2_client.virl2_client import ( ClientLibrary, @@ -224,6 +225,97 @@ def initial_different_response( assert respx.calls.call_count == 6 +@respx.mock +def test_jwt_only_valid_token_does_not_call_password_auth( + client_library_server_current: MagicMock, +): + _ = client_library_server_current + + auth_route = respx.get(f"{FAKE_URL}api/v0/authentication").respond( + 200, + json={ + "username": "jwt_user", + "admin": False, + "id": "6c7dd461-1cbe-428f-bdd5-545a0d766ed7", + "token": "VALID_TOKEN", + "error": None, + }, + ) + password_auth_route = respx.post(f"{FAKE_URL}api/v0/authenticate").respond( + json="SHOULD_NOT_BE_USED" + ) + respx.get(f"{FAKE_URL}api/v0/labs").respond(json=[]) + + cl = ClientLibrary(url=FAKE_URL, jwtoken="VALID_TOKEN") + cl.all_labs() + + assert auth_route.called + assert not password_auth_route.called + + +@respx.mock +def test_jwt_expired_with_credentials_reauths_using_password_auth( + client_library_server_current: MagicMock, +): + _ = client_library_server_current + + auth_route = respx.get(f"{FAKE_URL}api/v0/authentication") + auth_route.side_effect = [ + httpx.Response(401), + httpx.Response( + 200, + json={ + "username": "test", + "admin": True, + "id": "6c7dd461-1cbe-428f-bdd5-545a0d766ed7", + "token": "REFRESHED_TOKEN", + "error": None, + }, + ), + ] + password_auth_route = respx.post(f"{FAKE_URL}api/v0/authenticate").respond( + json="REFRESHED_TOKEN" + ) + + ClientLibrary( + url=FAKE_URL, + username="test", + password="pa$$", + jwtoken="EXPIRED_TOKEN", + ) + + assert auth_route.called + assert auth_route.call_count == 2 + assert password_auth_route.called + assert json.loads(password_auth_route.calls[0].request.content) == { + "username": "test", + "password": "pa$$", + } + + +@respx.mock +def test_jwt_reauth_without_credentials_fails_cleanly( + client_library_server_current: MagicMock, + reset_env: None, +): + _ = client_library_server_current, reset_env + + auth_route = respx.get(f"{FAKE_URL}api/v0/authentication").respond(401) + password_auth_route = respx.post(f"{FAKE_URL}api/v0/authenticate").respond( + json="SHOULD_NOT_BE_USED" + ) + + with pytest.raises( + APIError, + match="JWT token expired and automatic re-authentication is not possible", + ): + ClientLibrary(url=FAKE_URL, jwtoken="EXPIRED_TOKEN") + + assert auth_route.called + assert auth_route.call_count == 1 + assert not password_auth_route.called + + @respx.mock def test_old_auth_url_used_with_cml_2_9( client_library_server_2_9_0: MagicMock, monkeypatch: pytest.MonkeyPatch diff --git a/virl2_client/models/authentication.py b/virl2_client/models/authentication.py index 61a2196..5d97251 100644 --- a/virl2_client/models/authentication.py +++ b/virl2_client/models/authentication.py @@ -123,6 +123,14 @@ def auth_flow( if response.status_code == 401: _LOGGER.warning("re-auth called on 401 unauthorized") self.token = None + if not (self.client_library.username and self.client_library.password): + raise APIError( + "JWT token expired and automatic re-authentication is not " + "possible because username/password are not configured. " + "Set client.jwtoken, or initialize with username/password.", + request=response.request, + response=response, + ) request.headers["Authorization"] = f"Bearer {self.token}" response = yield request