from __future__ import annotations import json as stdlib_json from unittest.mock import AsyncMock, MagicMock, patch import httpx import pytest from yuxi.channel.common.http import ChannelHttpClient class TestChannelHttpClientLifecycle: async def test_creates_internal_client_when_none_provided(self): client = ChannelHttpClient() assert client._own_client is True assert isinstance(client._client, httpx.AsyncClient) await client._client.aclose() async def test_internal_client_closed_on_aexit(self): client = ChannelHttpClient() inner = client._client async with client: pass assert inner.is_closed async def test_external_client_not_closed_on_aexit(self): external = httpx.AsyncClient() client = ChannelHttpClient(client=external) assert client._own_client is False async with client: pass assert not external.is_closed await external.aclose() async def test_aenter_returns_self(self): client = ChannelHttpClient() async with client as entered: assert entered is client class TestChannelHttpClientRequestJson: async def test_request_json_returns_parsed_response(self): mock_client = MagicMock() mock_response = MagicMock() mock_response.json.return_value = {"ok": True} mock_response.text = "" mock_client.request = AsyncMock(return_value=mock_response) client = ChannelHttpClient(client=mock_client) result = await client.request_json("GET", "https://example.com/api") assert result == {"ok": True} mock_client.request.assert_awaited_once_with( "GET", "https://example.com/api", json=None, params=None, headers=None, files=None, timeout=60.0, ) mock_response.raise_for_status.assert_called_once() async def test_post_json_forwards_arguments(self): mock_client = MagicMock() mock_response = MagicMock() mock_response.json.return_value = {"id": "123"} mock_response.text = "" mock_client.request = AsyncMock(return_value=mock_response) client = ChannelHttpClient(client=mock_client) result = await client.post_json( "https://example.com/api", json={"foo": "bar"}, params={"k": "v"}, headers={"Authorization": "Bearer token"}, files=[("file", b"data")], timeout=10.0, ) assert result == {"id": "123"} mock_client.request.assert_awaited_once_with( "POST", "https://example.com/api", json={"foo": "bar"}, params={"k": "v"}, headers={"Authorization": "Bearer token"}, files=[("file", b"data")], timeout=10.0, ) async def test_get_json_forwards_arguments(self): mock_client = MagicMock() mock_response = MagicMock() mock_response.json.return_value = {"items": []} mock_response.text = "" mock_client.request = AsyncMock(return_value=mock_response) client = ChannelHttpClient(client=mock_client) result = await client.get_json( "https://example.com/api", params={"page": "1"}, headers={"Accept": "application/json"}, timeout=5.0, ) assert result == {"items": []} mock_client.request.assert_awaited_once_with( "GET", "https://example.com/api", json=None, params={"page": "1"}, headers={"Accept": "application/json"}, files=None, timeout=5.0, ) async def test_request_json_raises_on_http_status_error(self): mock_client = MagicMock() mock_response = MagicMock() mock_response.status_code = 500 mock_response.text = "internal error" mock_client.request = AsyncMock( side_effect=httpx.HTTPStatusError("error", request=MagicMock(), response=mock_response) ) client = ChannelHttpClient(client=mock_client) with pytest.raises(httpx.HTTPStatusError): await client.request_json("GET", "https://example.com/api") async def test_request_json_raises_on_request_error(self): mock_client = MagicMock() mock_client.request = AsyncMock(side_effect=httpx.RequestError("connection refused", request=MagicMock())) client = ChannelHttpClient(client=mock_client) with pytest.raises(httpx.RequestError): await client.request_json("GET", "https://example.com/api") async def test_request_json_raises_on_invalid_json(self): mock_client = MagicMock() mock_response = MagicMock() mock_response.json.side_effect = stdlib_json.JSONDecodeError("boom", "not json", 0) mock_response.text = "not json" mock_client.request = AsyncMock(return_value=mock_response) client = ChannelHttpClient(client=mock_client) with pytest.raises(stdlib_json.JSONDecodeError): await client.request_json("GET", "https://example.com/api", json=stdlib_json) async def test_logs_on_http_status_error(self): mock_client = MagicMock() mock_response = MagicMock() mock_response.status_code = 503 mock_response.text = "service unavailable" mock_client.request = AsyncMock( side_effect=httpx.HTTPStatusError("error", request=MagicMock(), response=mock_response) ) client = ChannelHttpClient(client=mock_client) with patch("yuxi.channel.common.http.logger") as mock_logger: with pytest.raises(httpx.HTTPStatusError): await client.request_json("POST", "https://example.com/api", json={"k": "v"}) mock_logger.error.assert_called_once() message = mock_logger.error.call_args[0][0] assert "Channel HTTP" in message async def test_logs_on_request_error(self): mock_client = MagicMock() mock_client.request = AsyncMock(side_effect=httpx.RequestError("timeout", request=MagicMock())) client = ChannelHttpClient(client=mock_client) with patch("yuxi.channel.common.http.logger") as mock_logger: with pytest.raises(httpx.RequestError): await client.request_json("GET", "https://example.com/api") mock_logger.error.assert_called_once() async def test_logs_on_invalid_json(self): mock_client = MagicMock() mock_response = MagicMock() mock_response.json.side_effect = stdlib_json.JSONDecodeError("boom", "x", 0) mock_response.text = "x" mock_client.request = AsyncMock(return_value=mock_response) client = ChannelHttpClient(client=mock_client) with patch("yuxi.channel.common.http.logger") as mock_logger: with pytest.raises(stdlib_json.JSONDecodeError): await client.request_json("GET", "https://example.com/api", json=stdlib_json) mock_logger.error.assert_called_once()