import json import httpx import pytest from app.mywhoosh.client import ( MyWhooshAuthError, MyWhooshClient, MyWhooshDeviceConflictError, MyWhooshIntegrationError, MyWhooshTransientError, ) from app.mywhoosh.models import MyWhooshToken from app.mywhoosh.tokenstore import MyWhooshTokenStore @pytest.mark.asyncio async def test_login_saves_access_refresh_and_whoosh_id(tmp_path) -> None: async def handler(request: httpx.Request) -> httpx.Response: assert request.url.path == "/http-service/api/login" return httpx.Response( 200, json={ "Success": True, "AccessToken": "new-access", "RefreshToken": "new-refresh", "WhooshId": "w-1", }, ) store = MyWhooshTokenStore(tmp_path / "mywhoosh.json") client = MyWhooshClient(store, http_client=httpx.AsyncClient(transport=httpx.MockTransport(handler))) await client.login("rider@example.com", "secret") assert store.load() == MyWhooshToken("new-access", "new-refresh", "w-1") @pytest.mark.asyncio async def test_invalid_credentials_raise_auth_error(tmp_path) -> None: async def handler(request: httpx.Request) -> httpx.Response: return httpx.Response(200, json={"Success": False, "Message": "Invalid credentials"}) client = MyWhooshClient( MyWhooshTokenStore(tmp_path / "mywhoosh.json"), http_client=httpx.AsyncClient(transport=httpx.MockTransport(handler)), ) with pytest.raises(MyWhooshAuthError): await client.login("rider@example.com", "bad") @pytest.mark.asyncio async def test_device_conflict_message_raises_device_conflict_error(tmp_path) -> None: async def handler(request: httpx.Request) -> httpx.Response: return httpx.Response( 200, json={"Success": False, "Message": "You are already logged in from another device."} ) client = MyWhooshClient( MyWhooshTokenStore(tmp_path / "mywhoosh.json"), http_client=httpx.AsyncClient(transport=httpx.MockTransport(handler)), ) with pytest.raises(MyWhooshDeviceConflictError): await client.login("rider@example.com", "secret") @pytest.mark.asyncio async def test_login_reuses_stable_device_id_across_calls(tmp_path) -> None: seen_device_ids = [] async def handler(request: httpx.Request) -> httpx.Response: seen_device_ids.append(json.loads(request.content)["DeviceId"]) return httpx.Response( 200, json={"Success": True, "AccessToken": "access", "RefreshToken": "refresh", "WhooshId": "w-1"}, ) store = MyWhooshTokenStore(tmp_path / "mywhoosh.json") client = MyWhooshClient(store, http_client=httpx.AsyncClient(transport=httpx.MockTransport(handler))) await client.login("rider@example.com", "secret") await client.login("rider@example.com", "secret") assert len(seen_device_ids) == 2 assert seen_device_ids[0] == seen_device_ids[1] assert seen_device_ids[0] == store.get_or_create_device_id() @pytest.mark.asyncio async def test_login_returns_json_array_raises_integration_error(tmp_path) -> None: async def handler(request: httpx.Request) -> httpx.Response: return httpx.Response(200, json=["not", "an", "object"]) client = MyWhooshClient( MyWhooshTokenStore(tmp_path / "mywhoosh.json"), http_client=httpx.AsyncClient(transport=httpx.MockTransport(handler)), ) with pytest.raises(MyWhooshIntegrationError): await client.login("rider@example.com", "bad") @pytest.mark.asyncio async def test_login_5xx_raises_transient_error(tmp_path) -> None: async def handler(request: httpx.Request) -> httpx.Response: return httpx.Response(503, text="Service Unavailable") client = MyWhooshClient( MyWhooshTokenStore(tmp_path / "mywhoosh.json"), http_client=httpx.AsyncClient(transport=httpx.MockTransport(handler)), ) with pytest.raises(MyWhooshTransientError): await client.login("rider@example.com", "secret") @pytest.mark.asyncio async def test_second_401_after_reauth_raises_auth_error(tmp_path) -> None: login_call_count = 0 async def handler(request: httpx.Request) -> httpx.Response: nonlocal login_call_count if request.url.path.endswith("/activities"): return httpx.Response(401, json={"message": "expired"}) if request.url.path.endswith("/login"): login_call_count += 1 return httpx.Response( 200, json={ "Success": True, "AccessToken": "fresh-access", "RefreshToken": "fresh-refresh", "WhooshId": "w-1", }, ) raise AssertionError(request.url) store = MyWhooshTokenStore(tmp_path / "mywhoosh.json") store.save(MyWhooshToken(access_token="stale", refresh_token=None, whoosh_id=None)) client = MyWhooshClient(store, http_client=httpx.AsyncClient(transport=httpx.MockTransport(handler))) with pytest.raises(MyWhooshAuthError): await client.list_activities("rider@example.com", "secret") assert login_call_count == 1 @pytest.mark.asyncio async def test_aclose_closes_self_owned_http_client(tmp_path) -> None: store = MyWhooshTokenStore(tmp_path / "mywhoosh.json") client = MyWhooshClient(store) await client.aclose() assert client.http.is_closed is True @pytest.mark.asyncio async def test_aclose_does_not_close_injected_http_client(tmp_path) -> None: store = MyWhooshTokenStore(tmp_path / "mywhoosh.json") injected = httpx.AsyncClient() client = MyWhooshClient(store, http_client=injected) await client.aclose() assert injected.is_closed is False await injected.aclose() @pytest.mark.asyncio async def test_client_usable_as_async_context_manager(tmp_path) -> None: store = MyWhooshTokenStore(tmp_path / "mywhoosh.json") async with MyWhooshClient(store) as client: http_client = client.http assert http_client.is_closed is False assert http_client.is_closed is True