from collections.abc import AsyncIterator, Iterator import httpx import pytest import respx from pydantic import BaseModel from fansifter_common.httpclient import AsyncHTTPClient, HTTPClient, HTTPClientError class ItemResponse(BaseModel): id: int name: str class ErrorResponse(BaseModel): error: str class TestHTTPClient: @pytest.fixture(scope="module") def client(self) -> Iterator[HTTPClient]: with HTTPClient() as client: yield client def test_request(self, client: HTTPClient, respx_mock: respx.MockRouter) -> None: respx_mock.get("https://sonumusic-pde.com/get").respond( status_code=200, json={"url": "https://sonumusic-pde.com/get"} ) response = client.request("GET", "https://sonumusic-pde.com/get") assert response.status_code == 200 assert response.json() == { "url": "https://sonumusic-pde.com/get", } def test_request_type( self, client: HTTPClient, respx_mock: respx.MockRouter ) -> None: respx_mock.get("https://sonumusic-pde.com/get").respond( status_code=200, json={"id": 100, "name": "Test"} ) response = client.request( "GET", "https://sonumusic-pde.com/get", type=ItemResponse ) assert response.id == 100 assert response.name == "Test" def test_request_error( self, client: HTTPClient, respx_mock: respx.MockRouter ) -> None: respx_mock.get("https://sonumusic-pde.com/get").respond( status_code=400, json={"error": "INVALID"} ) with pytest.raises(HTTPClientError) as exc_info: client.request("GET", "https://sonumusic-pde.com/get") assert exc_info.value.status_code == 400 assert exc_info.value.log_msg == "Invalid HTTPClient API response status: 400" assert exc_info.value.log_extra == { "http_client_request_method": "GET", "http_client_request_path": "/get", "http_client_response_status_code": 400, } def test_request_type_error( self, client: HTTPClient, respx_mock: respx.MockRouter ) -> None: respx_mock.get("https://sonumusic-pde.com/get").respond( status_code=200, json={"error": "INVALID"} ) with pytest.raises(HTTPClientError) as exc_info: client.request("GET", "https://sonumusic-pde.com/get", type=ItemResponse) assert exc_info.value.status_code == 200 assert exc_info.value.log_msg == ( "Failed to deserialize HTTPClient API response into type ItemResponse" ) assert exc_info.value.log_extra == { "http_client_request_method": "GET", "http_client_request_path": "/get", "http_client_response_status_code": 200, } def test_request_error_sanitized( self, client: HTTPClient, respx_mock: respx.MockRouter ) -> None: client.sensitive_params = {"password", "token"} respx_mock.get("https://sonumusic-pde.com/get").respond(status_code=400) with pytest.raises(HTTPClientError) as exc_info: client.request( "GET", "https://sonumusic-pde.com/get", params={"password": "secret", "token": "hashed", "other": "ok"}, ) assert exc_info.value.status_code == 400 assert exc_info.value.response assert exc_info.value.response.url == httpx.URL( "https://sonumusic-pde.com/get?other=ok" ) class TestAsyncHTTPClient: @pytest.fixture async def client(self) -> AsyncIterator[AsyncHTTPClient]: async with AsyncHTTPClient() as client: yield client @pytest.mark.anyio async def test_request( self, client: AsyncHTTPClient, respx_mock: respx.MockRouter ) -> None: respx_mock.get("https://sonumusic-pde.com/get").respond( status_code=200, json={"url": "https://sonumusic-pde.com/get"} ) response = await client.request("GET", "https://sonumusic-pde.com/get") assert response.status_code == 200 assert response.json() == { "url": "https://sonumusic-pde.com/get", } @pytest.mark.anyio async def test_request_type( self, client: AsyncHTTPClient, respx_mock: respx.MockRouter ) -> None: respx_mock.get("https://sonumusic-pde.com/get").respond( status_code=200, json={"id": 100, "name": "Test"} ) response = await client.request( "GET", "https://sonumusic-pde.com/get", type=ItemResponse ) assert response.id == 100 assert response.name == "Test" @pytest.mark.anyio async def test_request_error( self, client: AsyncHTTPClient, respx_mock: respx.MockRouter ) -> None: respx_mock.get("https://sonumusic-pde.com/get").respond( status_code=400, json={"error": "INVALID"} ) with pytest.raises(HTTPClientError) as exc_info: await client.request("GET", "https://sonumusic-pde.com/get") assert exc_info.value.status_code == 400 assert exc_info.value.log_msg == ( "Invalid AsyncHTTPClient API response status: 400" ) assert exc_info.value.log_extra == { "http_client_request_method": "GET", "http_client_request_path": "/get", "http_client_response_status_code": 400, } @pytest.mark.anyio async def test_request_type_error( self, client: AsyncHTTPClient, respx_mock: respx.MockRouter ) -> None: respx_mock.get("https://sonumusic-pde.com/get").respond( status_code=200, json={"error": "INVALID"} ) with pytest.raises(HTTPClientError) as exc_info: await client.request( "GET", "https://sonumusic-pde.com/get", type=ItemResponse ) assert exc_info.value.status_code == 200 assert exc_info.value.log_msg == ( "Failed to deserialize AsyncHTTPClient API response into type ItemResponse" ) assert exc_info.value.log_extra == { "http_client_request_method": "GET", "http_client_request_path": "/get", "http_client_response_status_code": 200, } @pytest.mark.anyio async def test_request_error_sanitized( self, client: AsyncHTTPClient, respx_mock: respx.MockRouter ) -> None: client.sensitive_params = {"password", "token"} respx_mock.get("https://sonumusic-pde.com/get").respond(status_code=400) with pytest.raises(HTTPClientError) as exc_info: await client.request( "GET", "https://sonumusic-pde.com/get", params={"password": "secret", "token": "hashed", "other": "ok"}, ) assert exc_info.value.status_code == 400 assert exc_info.value.response assert exc_info.value.response.url == httpx.URL( "https://sonumusic-pde.com/get?other=ok" )