174 lines
6.0 KiB
Python
174 lines
6.0 KiB
Python
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
|