191 lines
7.0 KiB
Python
191 lines
7.0 KiB
Python
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()
|