Files
mywhoosh2garmin/tests/mywhoosh/test_client_auth.py
Bastian Wagner 7c9e19ba0b errors
2026-08-15 20:31:57 +02:00

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