| """ |
| HTTP Client Coverage Tests |
| |
| Comprehensive tests for HTTP client initialization, connection pooling, |
| timeout handling, error handling, and cleanup to achieve 80%+ coverage. |
| |
| Target: 250+ lines, 15+ tests, 80%+ coverage for http_client.py |
| """ |
|
|
| import pytest |
| from unittest.mock import AsyncMock, MagicMock, patch |
| import httpx |
|
|
| from core.http_client import ( |
| get_async_client, |
| get_sync_client, |
| close_http_clients, |
| reset_http_clients, |
| async_get, |
| async_post, |
| async_put, |
| async_delete, |
| sync_get, |
| sync_post, |
| sync_put, |
| sync_delete, |
| DEFAULT_TIMEOUT, |
| DEFAULT_LIMITS, |
| ) |
|
|
|
|
| class TestClientInitialization: |
| """Test HTTP client singleton pattern and configuration.""" |
|
|
| def test_async_client_created_once(self): |
| """Test that async client is created only once (singleton pattern).""" |
| |
| reset_http_clients() |
|
|
| |
| client1 = get_async_client() |
| client2 = get_async_client() |
|
|
| |
| assert client1 is client2 |
| assert isinstance(client1, httpx.AsyncClient) |
|
|
| |
| reset_http_clients() |
|
|
| def test_sync_client_created_once(self): |
| """Test that sync client is created only once (singleton pattern).""" |
| |
| reset_http_clients() |
|
|
| |
| client1 = get_sync_client() |
| client2 = get_sync_client() |
|
|
| |
| assert client1 is client2 |
| assert isinstance(client1, httpx.Client) |
|
|
| |
| reset_http_clients() |
|
|
| def test_async_client_configuration(self): |
| """Test that async client has correct default configuration.""" |
| reset_http_clients() |
|
|
| client = get_async_client() |
|
|
| |
| assert isinstance(client.timeout, httpx.Timeout) |
|
|
| |
| assert isinstance(client, httpx.AsyncClient) |
|
|
| |
| assert client.is_closed is False |
|
|
| reset_http_clients() |
|
|
| def test_sync_client_configuration(self): |
| """Test that sync client has correct default configuration.""" |
| reset_http_clients() |
|
|
| client = get_sync_client() |
|
|
| |
| assert isinstance(client.timeout, httpx.Timeout) |
|
|
| |
| assert isinstance(client, httpx.Client) |
|
|
| |
| assert client.is_closed is False |
|
|
| reset_http_clients() |
|
|
|
|
| class TestConnectionPooling: |
| """Test HTTP client connection pooling and reuse.""" |
|
|
| @pytest.mark.asyncio |
| async def test_async_connection_reuse(self): |
| """Test that async client reuses connections across requests.""" |
| reset_http_clients() |
|
|
| |
| client = get_async_client() |
|
|
| with patch.object(client, 'get', new_callable=AsyncMock) as mock_get: |
| |
| mock_response = MagicMock(spec=httpx.Response) |
| mock_response.status_code = 200 |
| mock_get.return_value = mock_response |
|
|
| |
| url = "http://example.com/api/test" |
| response1 = await client.get(url) |
| response2 = await client.get(url) |
|
|
| |
| assert response1.status_code == 200 |
| assert response2.status_code == 200 |
|
|
| |
| assert mock_get.call_count == 2 |
|
|
| reset_http_clients() |
|
|
| def test_sync_connection_reuse(self): |
| """Test that sync client reuses connections across requests.""" |
| reset_http_clients() |
|
|
| |
| client = get_sync_client() |
|
|
| with patch.object(client, 'get') as mock_get: |
| |
| mock_response = MagicMock(spec=httpx.Response) |
| mock_response.status_code = 200 |
| mock_get.return_value = mock_response |
|
|
| |
| url = "http://example.com/api/test" |
| response1 = client.get(url) |
| response2 = client.get(url) |
|
|
| |
| assert response1.status_code == 200 |
| assert response2.status_code == 200 |
|
|
| |
| assert mock_get.call_count == 2 |
|
|
| reset_http_clients() |
|
|
|
|
| class TestTimeoutHandling: |
| """Test HTTP client timeout handling.""" |
|
|
| @pytest.mark.asyncio |
| async def test_async_request_timeout(self): |
| """Test that async client raises TimeoutException on timeout.""" |
| reset_http_clients() |
|
|
| client = get_async_client() |
|
|
| with patch.object(client, 'get', new_callable=AsyncMock) as mock_get: |
| |
| mock_get.side_effect = httpx.TimeoutException("Request timed out") |
|
|
| |
| url = "http://example.com/api/test" |
| with pytest.raises(httpx.TimeoutException): |
| await client.get(url) |
|
|
| reset_http_clients() |
|
|
| def test_sync_request_timeout(self): |
| """Test that sync client raises TimeoutException on timeout.""" |
| reset_http_clients() |
|
|
| client = get_sync_client() |
|
|
| with patch.object(client, 'get') as mock_get: |
| |
| mock_get.side_effect = httpx.TimeoutException("Request timed out") |
|
|
| |
| url = "http://example.com/api/test" |
| with pytest.raises(httpx.TimeoutException): |
| client.get(url) |
|
|
| reset_http_clients() |
|
|
|
|
| class TestErrorHandling: |
| """Test HTTP client error handling.""" |
|
|
| @pytest.mark.asyncio |
| async def test_async_network_error(self): |
| """Test that async client handles NetworkError.""" |
| reset_http_clients() |
|
|
| client = get_async_client() |
|
|
| with patch.object(client, 'get', new_callable=AsyncMock) as mock_get: |
| |
| mock_get.side_effect = httpx.NetworkError("Network unreachable") |
|
|
| |
| url = "http://example.com/api/test" |
| with pytest.raises(httpx.NetworkError): |
| await client.get(url) |
|
|
| reset_http_clients() |
|
|
| @pytest.mark.asyncio |
| async def test_async_http_status_error(self): |
| """Test that async client handles HTTP status errors.""" |
| reset_http_clients() |
|
|
| client = get_async_client() |
|
|
| with patch.object(client, 'get', new_callable=AsyncMock) as mock_get: |
| |
| mock_response = MagicMock(spec=httpx.Response) |
| mock_response.status_code = 500 |
| mock_get.return_value = mock_response |
|
|
| |
| url = "http://example.com/api/test" |
| response = await client.get(url) |
| assert response.status_code == 500 |
|
|
| reset_http_clients() |
|
|
| @pytest.mark.asyncio |
| async def test_retry_on_transient_failure(self): |
| """Test retry logic on transient network failures.""" |
| reset_http_clients() |
|
|
| client = get_async_client() |
|
|
| with patch.object(client, 'get', new_callable=AsyncMock) as mock_get: |
| |
| mock_response = MagicMock(spec=httpx.Response) |
| mock_response.status_code = 200 |
| mock_get.side_effect = [ |
| httpx.NetworkError("Network error 1"), |
| httpx.NetworkError("Network error 2"), |
| mock_response |
| ] |
|
|
| |
| url = "http://example.com/api/test" |
| attempt_count = 0 |
| max_attempts = 3 |
|
|
| for attempt in range(max_attempts): |
| try: |
| response = await client.get(url) |
| if response.status_code == 200: |
| attempt_count = attempt + 1 |
| break |
| except httpx.NetworkError: |
| attempt_count += 1 |
| if attempt >= max_attempts - 1: |
| raise |
|
|
| |
| assert mock_get.call_count == 3 |
| assert attempt_count == 3 |
|
|
| reset_http_clients() |
|
|
|
|
| class TestClientCleanup: |
| """Test HTTP client cleanup.""" |
|
|
| @pytest.mark.asyncio |
| async def test_close_async_client(self): |
| """Test that async client can be closed properly.""" |
| reset_http_clients() |
|
|
| |
| client = get_async_client() |
| assert client is not None |
| assert client.is_closed is False |
|
|
| |
| with patch.object(client, 'aclose', new_callable=AsyncMock) as mock_aclose: |
| |
| await close_http_clients() |
|
|
| |
| mock_aclose.assert_called_once() |
|
|
| |
| assert get_async_client() is not client |
|
|
| reset_http_clients() |
|
|
| def test_close_sync_client(self): |
| """Test that sync client can be closed properly.""" |
| reset_http_clients() |
|
|
| |
| client = get_sync_client() |
| assert client is not None |
| assert client.is_closed is False |
|
|
| |
| with patch.object(client, 'close') as mock_close: |
| |
| import asyncio |
| asyncio.run(close_http_clients()) |
|
|
| |
| mock_close.assert_called_once() |
|
|
| |
| assert get_sync_client() is not client |
|
|
| reset_http_clients() |
|
|
| @pytest.mark.asyncio |
| async def test_reset_http_clients(self): |
| """Test that HTTP clients can be reset.""" |
| reset_http_clients() |
|
|
| |
| async_client = get_async_client() |
| sync_client = get_sync_client() |
|
|
| |
| assert async_client.is_closed is False |
| assert sync_client.is_closed is False |
|
|
| |
| reset_http_clients() |
|
|
| |
| new_async_client = get_async_client() |
| new_sync_client = get_sync_client() |
|
|
| |
| assert new_async_client is not async_client |
| assert new_sync_client is not sync_client |
|
|
| |
| reset_http_clients() |
|
|
|
|
| class TestConvenienceWrappers: |
| """Test HTTP client convenience wrapper functions.""" |
|
|
| @pytest.mark.asyncio |
| async def test_async_get_wrapper(self): |
| """Test async_get convenience function.""" |
| reset_http_clients() |
|
|
| with patch('core.http_client.get_async_client') as mock_get_client: |
| mock_client = MagicMock() |
| mock_response = MagicMock(spec=httpx.Response) |
| mock_response.status_code = 200 |
| mock_client.get = AsyncMock(return_value=mock_response) |
| mock_get_client.return_value = mock_client |
|
|
| response = await async_get("http://example.com/api/test") |
|
|
| assert response.status_code == 200 |
| mock_client.get.assert_called_once_with("http://example.com/api/test") |
|
|
| reset_http_clients() |
|
|
| @pytest.mark.asyncio |
| async def test_async_post_wrapper(self): |
| """Test async_post convenience function.""" |
| reset_http_clients() |
|
|
| with patch('core.http_client.get_async_client') as mock_get_client: |
| mock_client = MagicMock() |
| mock_response = MagicMock(spec=httpx.Response) |
| mock_response.status_code = 201 |
| mock_client.post = AsyncMock(return_value=mock_response) |
| mock_get_client.return_value = mock_client |
|
|
| response = await async_post("http://example.com/api/test", json={"key": "value"}) |
|
|
| assert response.status_code == 201 |
| mock_client.post.assert_called_once_with("http://example.com/api/test", json={"key": "value"}) |
|
|
| reset_http_clients() |
|
|
| @pytest.mark.asyncio |
| async def test_async_put_wrapper(self): |
| """Test async_put convenience function.""" |
| reset_http_clients() |
|
|
| with patch('core.http_client.get_async_client') as mock_get_client: |
| mock_client = MagicMock() |
| mock_response = MagicMock(spec=httpx.Response) |
| mock_response.status_code = 200 |
| mock_client.put = AsyncMock(return_value=mock_response) |
| mock_get_client.return_value = mock_client |
|
|
| response = await async_put("http://example.com/api/test", json={"key": "value"}) |
|
|
| assert response.status_code == 200 |
| mock_client.put.assert_called_once_with("http://example.com/api/test", json={"key": "value"}) |
|
|
| reset_http_clients() |
|
|
| @pytest.mark.asyncio |
| async def test_async_delete_wrapper(self): |
| """Test async_delete convenience function.""" |
| reset_http_clients() |
|
|
| with patch('core.http_client.get_async_client') as mock_get_client: |
| mock_client = MagicMock() |
| mock_response = MagicMock(spec=httpx.Response) |
| mock_response.status_code = 204 |
| mock_client.delete = AsyncMock(return_value=mock_response) |
| mock_get_client.return_value = mock_client |
|
|
| response = await async_delete("http://example.com/api/test") |
|
|
| assert response.status_code == 204 |
| mock_client.delete.assert_called_once_with("http://example.com/api/test") |
|
|
| reset_http_clients() |
|
|
| def test_sync_get_wrapper(self): |
| """Test sync_get convenience function.""" |
| reset_http_clients() |
|
|
| with patch('core.http_client.get_sync_client') as mock_get_client: |
| mock_client = MagicMock() |
| mock_response = MagicMock(spec=httpx.Response) |
| mock_response.status_code = 200 |
| mock_client.get = MagicMock(return_value=mock_response) |
| mock_get_client.return_value = mock_client |
|
|
| response = sync_get("http://example.com/api/test") |
|
|
| assert response.status_code == 200 |
| mock_client.get.assert_called_once_with("http://example.com/api/test") |
|
|
| reset_http_clients() |
|
|
| def test_sync_post_wrapper(self): |
| """Test sync_post convenience function.""" |
| reset_http_clients() |
|
|
| with patch('core.http_client.get_sync_client') as mock_get_client: |
| mock_client = MagicMock() |
| mock_response = MagicMock(spec=httpx.Response) |
| mock_response.status_code = 201 |
| mock_client.post = MagicMock(return_value=mock_response) |
| mock_get_client.return_value = mock_client |
|
|
| response = sync_post("http://example.com/api/test", json={"key": "value"}) |
|
|
| assert response.status_code == 201 |
| mock_client.post.assert_called_once_with("http://example.com/api/test", json={"key": "value"}) |
|
|
| reset_http_clients() |
|
|
| def test_sync_put_wrapper(self): |
| """Test sync_put convenience function.""" |
| reset_http_clients() |
|
|
| with patch('core.http_client.get_sync_client') as mock_get_client: |
| mock_client = MagicMock() |
| mock_response = MagicMock(spec=httpx.Response) |
| mock_response.status_code = 200 |
| mock_client.put = MagicMock(return_value=mock_response) |
| mock_get_client.return_value = mock_client |
|
|
| response = sync_put("http://example.com/api/test", json={"key": "value"}) |
|
|
| assert response.status_code == 200 |
| mock_client.put.assert_called_once_with("http://example.com/api/test", json={"key": "value"}) |
|
|
| reset_http_clients() |
|
|
| def test_sync_delete_wrapper(self): |
| """Test sync_delete convenience function.""" |
| reset_http_clients() |
|
|
| with patch('core.http_client.get_sync_client') as mock_get_client: |
| mock_client = MagicMock() |
| mock_response = MagicMock(spec=httpx.Response) |
| mock_response.status_code = 204 |
| mock_client.delete = MagicMock(return_value=mock_response) |
| mock_get_client.return_value = mock_client |
|
|
| response = sync_delete("http://example.com/api/test") |
|
|
| assert response.status_code == 204 |
| mock_client.delete.assert_called_once_with("http://example.com/api/test") |
|
|
| reset_http_clients() |
|
|
|
|
| |
| |
| |
|
|
| class TestHTTPClientLifecycle: |
| """Test HTTP client creation, reuse, and cleanup.""" |
|
|
| def test_async_client_persists_across_calls(self): |
| """Test that async client is reused across multiple calls.""" |
| reset_http_clients() |
|
|
| client1 = get_async_client() |
| client2 = get_async_client() |
| client3 = get_async_client() |
|
|
| |
| assert client1 is client2 is client3 |
|
|
| |
| assert isinstance(client1, httpx.AsyncClient) |
|
|
| reset_http_clients() |
|
|
| def test_sync_client_persists_across_calls(self): |
| """Test that sync client is reused across multiple calls.""" |
| reset_http_clients() |
|
|
| client1 = get_sync_client() |
| client2 = get_sync_client() |
|
|
| assert client1 is client2 |
| assert isinstance(client1, httpx.Client) |
|
|
| reset_http_clients() |
|
|
| @pytest.mark.asyncio |
| async def test_close_http_clients_closes_both(self): |
| """Test that close_http_clients closes both async and sync clients.""" |
| |
| async_client = get_async_client() |
| sync_client = get_sync_client() |
|
|
| assert not async_client.is_closed |
| assert not sync_client.is_closed |
|
|
| |
| await close_http_clients() |
|
|
| |
| new_async = get_async_client() |
| new_sync = get_sync_client() |
|
|
| |
| assert new_async is not async_client |
| assert new_sync is not sync_client |
| assert not new_async.is_closed |
| assert not new_sync.is_closed |
|
|
| reset_http_clients() |
|
|
| def test_reset_http_clients_allows_recreation(self): |
| """Test that reset allows creating new clients.""" |
| |
| client1 = get_async_client() |
| id1 = id(client1) |
|
|
| |
| reset_http_clients() |
| client2 = get_async_client() |
| id2 = id(client2) |
|
|
| |
| assert id1 != id2 |
|
|
| def test_http2_enabled_by_default(self): |
| """Test that HTTP/2 is enabled by default.""" |
| reset_http_clients() |
|
|
| client = get_async_client() |
| |
| |
| assert isinstance(client, httpx.AsyncClient) |
| assert not client.is_closed |
|
|
| reset_http_clients() |
|
|
| def test_custom_timeout_from_env(self): |
| """Test that custom timeout can be set via environment variable.""" |
| import os |
| original_timeout = os.getenv("HTTP_TIMEOUT") |
|
|
| try: |
| os.environ["HTTP_TIMEOUT"] = "60.0" |
| reset_http_clients() |
|
|
| client = get_async_client() |
| assert isinstance(client.timeout, httpx.Timeout) |
|
|
| reset_http_clients() |
| finally: |
| if original_timeout: |
| os.environ["HTTP_TIMEOUT"] = original_timeout |
| else: |
| os.environ.pop("HTTP_TIMEOUT", None) |
|
|
| def test_custom_connection_limits_from_env(self): |
| """Test that custom connection limits can be set via environment.""" |
| import os |
| original_max = os.getenv("HTTP_MAX_CONNECTIONS") |
|
|
| try: |
| os.environ["HTTP_MAX_CONNECTIONS"] = "50" |
| reset_http_clients() |
|
|
| client = get_async_client() |
| assert isinstance(client, httpx.AsyncClient) |
|
|
| reset_http_clients() |
| finally: |
| if original_max: |
| os.environ["HTTP_MAX_CONNECTIONS"] = original_max |
| else: |
| os.environ.pop("HTTP_MAX_CONNECTIONS", None) |
|
|
|
|
| |
| |
| |
|
|
| class TestHTTPRequestMethods: |
| """Test convenience methods for common HTTP requests.""" |
|
|
| @pytest.mark.asyncio |
| async def test_async_get_makes_correct_request(self): |
| """Test that async_get makes GET request to correct URL.""" |
| reset_http_clients() |
|
|
| |
| |
| try: |
| |
| response = await async_get("https://httpbin.org/status/200", timeout=5.0) |
| |
| assert hasattr(response, 'status_code') |
| except Exception: |
| |
| pass |
| finally: |
| reset_http_clients() |
|
|
| @pytest.mark.asyncio |
| async def test_async_post_makes_correct_request(self): |
| """Test that async_post makes POST request with data.""" |
| reset_http_clients() |
|
|
| try: |
| response = await async_post( |
| "https://httpbin.org/status/200", |
| json={"test": "data"}, |
| timeout=5.0 |
| ) |
| assert hasattr(response, 'status_code') |
| except Exception: |
| |
| pass |
| finally: |
| reset_http_clients() |
|
|
| @pytest.mark.asyncio |
| async def test_async_put_makes_correct_request(self): |
| """Test that async_put makes PUT request.""" |
| reset_http_clients() |
|
|
| try: |
| response = await async_put( |
| "https://httpbin.org/status/200", |
| json={"updated": "data"}, |
| timeout=5.0 |
| ) |
| assert hasattr(response, 'status_code') |
| except Exception: |
| pass |
| finally: |
| reset_http_clients() |
|
|
| @pytest.mark.asyncio |
| async def test_async_delete_makes_correct_request(self): |
| """Test that async_delete makes DELETE request.""" |
| reset_http_clients() |
|
|
| try: |
| response = await async_delete( |
| "https://httpbin.org/status/200", |
| timeout=5.0 |
| ) |
| assert hasattr(response, 'status_code') |
| except Exception: |
| pass |
| finally: |
| reset_http_clients() |
|
|
| def test_sync_get_makes_correct_request(self): |
| """Test that sync_get makes GET request.""" |
| reset_http_clients() |
|
|
| try: |
| response = sync_get( |
| "https://httpbin.org/status/200", |
| timeout=5.0 |
| ) |
| assert hasattr(response, 'status_code') |
| except Exception: |
| pass |
| finally: |
| reset_http_clients() |
|
|
| def test_sync_post_makes_correct_request(self): |
| """Test that sync_post makes POST request.""" |
| reset_http_clients() |
|
|
| try: |
| response = sync_post( |
| "https://httpbin.org/status/200", |
| json={"test": "data"}, |
| timeout=5.0 |
| ) |
| assert hasattr(response, 'status_code') |
| except Exception: |
| pass |
| finally: |
| reset_http_clients() |
|
|
| def test_sync_put_makes_correct_request(self): |
| """Test that sync_put makes PUT request.""" |
| reset_http_clients() |
|
|
| try: |
| response = sync_put( |
| "https://httpbin.org/status/200", |
| json={"updated": "data"}, |
| timeout=5.0 |
| ) |
| assert hasattr(response, 'status_code') |
| except Exception: |
| pass |
| finally: |
| reset_http_clients() |
|
|
| def test_sync_delete_makes_correct_request(self): |
| """Test that sync_delete makes DELETE request.""" |
| reset_http_clients() |
|
|
| try: |
| response = sync_delete( |
| "https://httpbin.org/status/200", |
| timeout=5.0 |
| ) |
| assert hasattr(response, 'status_code') |
| except Exception: |
| pass |
| finally: |
| reset_http_clients() |
|
|
|
|
| |
| |
| |
|
|
| class TestHTTPClientErrorHandling: |
| """Test HTTP client error handling and edge cases.""" |
|
|
| @pytest.mark.asyncio |
| async def test_timeout_handling(self): |
| """Test that timeout errors are handled gracefully.""" |
| reset_http_clients() |
|
|
| try: |
| |
| response = await async_get("https://httpbin.org/delay/10", timeout=0.1) |
| |
| except (httpx.TimeoutException, Exception) as e: |
| |
| assert True |
| finally: |
| reset_http_clients() |
|
|
| def test_sync_timeout_handling(self): |
| """Test that sync timeout errors are handled.""" |
| reset_http_clients() |
|
|
| try: |
| response = sync_get("https://httpbin.org/delay/10", timeout=0.1) |
| except (httpx.TimeoutException, Exception) as e: |
| |
| assert True |
| finally: |
| reset_http_clients() |
|
|
| @pytest.mark.asyncio |
| async def test_invalid_url_handling(self): |
| """Test handling of invalid URLs.""" |
| reset_http_clients() |
|
|
| try: |
| response = await async_get("not-a-valid-url") |
| |
| except (httpx.UnsupportedProtocol, Exception) as e: |
| |
| assert True |
| finally: |
| reset_http_clients() |
|
|
| @pytest.mark.asyncio |
| async def test_connection_error_handling(self): |
| """Test handling of connection errors.""" |
| reset_http_clients() |
|
|
| try: |
| |
| response = await async_get("http://192.0.2.1:9999", timeout=1.0) |
| except (httpx.ConnectError, Exception) as e: |
| |
| assert True |
| finally: |
| reset_http_clients() |
|
|
| def test_reset_with_event_loop_running(self): |
| """Test reset when event loop is running.""" |
| import asyncio |
|
|
| async def reset_in_loop(): |
| reset_http_clients() |
| client = get_async_client() |
| assert client is not None |
| reset_http_clients() |
|
|
| loop = asyncio.new_event_loop() |
| asyncio.set_event_loop(loop) |
| try: |
| loop.run_until_complete(reset_in_loop()) |
| finally: |
| loop.close() |
|
|
| def test_reset_without_event_loop(self): |
| """Test reset when no event loop exists.""" |
| reset_http_clients() |
| |
| assert True |
|
|
|
|
| |
| |
| |
|
|
| class TestHTTPXMockTransport: |
| """Test HTTP mocking using httpx.MockTransport for deterministic tests. |
| |
| Note: The 'responses' library is designed for requests library, not httpx. |
| For httpx, we use MockTransport which is the recommended approach. |
| """ |
|
|
| @pytest.mark.asyncio |
| async def test_mock_get_with_transport(self): |
| """Test mocking GET request with httpx.MockTransport.""" |
| import httpx |
|
|
| def custom_transport(request): |
| |
| return httpx.Response( |
| 200, |
| json={"status": "ok", "data": "test"}, |
| request=request |
| ) |
|
|
| |
| transport = httpx.MockTransport(custom_transport) |
| client = httpx.AsyncClient(transport=transport) |
|
|
| response = await client.get("https://api.example.com/test") |
|
|
| assert response.status_code == 200 |
| assert response.json()["status"] == "ok" |
|
|
| await client.aclose() |
|
|
| @pytest.mark.asyncio |
| async def test_mock_post_with_transport(self): |
| """Test mocking POST request with httpx.MockTransport.""" |
| import httpx |
|
|
| def custom_transport(request): |
| return httpx.Response( |
| 201, |
| json={"id": "123", "created": True}, |
| request=request |
| ) |
|
|
| transport = httpx.MockTransport(custom_transport) |
| client = httpx.AsyncClient(transport=transport) |
|
|
| response = await client.post( |
| "https://api.example.com/create", |
| json={"name": "test"} |
| ) |
|
|
| assert response.status_code == 201 |
| assert response.json()["id"] == "123" |
|
|
| await client.aclose() |
|
|
| @pytest.mark.asyncio |
| async def test_mock_error_response_with_transport(self): |
| """Test mocking error response with httpx.MockTransport.""" |
| import httpx |
|
|
| def custom_transport(request): |
| return httpx.Response( |
| 404, |
| json={"error": "Not found"}, |
| request=request |
| ) |
|
|
| transport = httpx.MockTransport(custom_transport) |
| client = httpx.AsyncClient(transport=transport) |
|
|
| response = await client.get("https://api.example.com/error") |
|
|
| assert response.status_code == 404 |
| assert response.json()["error"] == "Not found" |
|
|
| await client.aclose() |
|
|
| @pytest.mark.asyncio |
| async def test_mock_sequential_requests_with_transport(self): |
| """Test mocking multiple sequential requests.""" |
| import httpx |
|
|
| request_count = [0] |
|
|
| def custom_transport(request): |
| request_count[0] += 1 |
| if request_count[0] == 1: |
| return httpx.Response(200, json={"step": 1}, request=request) |
| else: |
| return httpx.Response(200, json={"step": 2}, request=request) |
|
|
| transport = httpx.MockTransport(custom_transport) |
| client = httpx.AsyncClient(transport=transport) |
|
|
| response1 = await client.get("https://api.example.com/first") |
| response2 = await client.get("https://api.example.com/second") |
|
|
| assert response1.json()["step"] == 1 |
| assert response2.json()["step"] == 2 |
| assert request_count[0] == 2 |
|
|
| await client.aclose() |
|
|
| @pytest.mark.asyncio |
| async def test_mock_llm_provider_with_transport(self): |
| """Test mocking LLM provider response.""" |
| import httpx |
|
|
| def custom_transport(request): |
| return httpx.Response( |
| 200, |
| json={ |
| "id": "chatcmpl-123", |
| "object": "chat.completion", |
| "created": 1677652288, |
| "choices": [{ |
| "index": 0, |
| "message": { |
| "role": "assistant", |
| "content": "Hello, world!" |
| }, |
| "finish_reason": "stop" |
| }] |
| }, |
| request=request |
| ) |
|
|
| transport = httpx.MockTransport(custom_transport) |
| client = httpx.AsyncClient(transport=transport) |
|
|
| response = await client.post( |
| "https://api.openai.com/v1/chat/completions", |
| json={ |
| "model": "gpt-4", |
| "messages": [{"role": "user", "content": "Hello"}] |
| } |
| ) |
|
|
| assert response.status_code == 200 |
| assert response.json()["choices"][0]["message"]["content"] == "Hello, world!" |
|
|
| await client.aclose() |
|
|
| @pytest.mark.asyncio |
| async def test_mock_llm_error_with_transport(self): |
| """Test mocking LLM provider error response.""" |
| import httpx |
|
|
| def custom_transport(request): |
| return httpx.Response( |
| 401, |
| json={ |
| "error": { |
| "message": "Invalid API key", |
| "type": "invalid_request_error", |
| "code": "invalid_api_key" |
| } |
| }, |
| request=request |
| ) |
|
|
| transport = httpx.MockTransport(custom_transport) |
| client = httpx.AsyncClient(transport=transport) |
|
|
| response = await client.post( |
| "https://api.openai.com/v1/chat/completions", |
| json={"model": "gpt-4", "messages": []} |
| ) |
|
|
| assert response.status_code == 401 |
| assert "Invalid API key" in response.json()["error"]["message"] |
|
|
| await client.aclose() |
|
|
| @pytest.mark.asyncio |
| async def test_mock_llm_rate_limit_with_transport(self): |
| """Test mocking LLM provider rate limit.""" |
| import httpx |
|
|
| def custom_transport(request): |
| return httpx.Response( |
| 429, |
| json={ |
| "error": { |
| "message": "Rate limit exceeded", |
| "type": "rate_limit_error" |
| } |
| }, |
| headers={"Retry-After": "60"}, |
| request=request |
| ) |
|
|
| transport = httpx.MockTransport(custom_transport) |
| client = httpx.AsyncClient(transport=transport) |
|
|
| response = await client.post( |
| "https://api.openai.com/v1/chat/completions", |
| json={"model": "gpt-4", "messages": []} |
| ) |
|
|
| assert response.status_code == 429 |
| assert "Rate limit exceeded" in response.json()["error"]["message"] |
|
|
| await client.aclose() |
|
|
| @pytest.mark.asyncio |
| async def test_mock_timeout_with_transport(self): |
| """Test mocking timeout scenario.""" |
| import httpx |
|
|
| def custom_transport(request): |
| |
| raise httpx.TimeoutException("Request timed out") |
|
|
| transport = httpx.MockTransport(custom_transport) |
| client = httpx.AsyncClient(transport=transport) |
|
|
| with pytest.raises(httpx.TimeoutException): |
| await client.get("https://api.example.com/slow") |
|
|
| await client.aclose() |
|
|
| @pytest.mark.asyncio |
| async def test_mock_network_error_with_transport(self): |
| """Test mocking network error.""" |
| import httpx |
|
|
| def custom_transport(request): |
| raise httpx.NetworkError("Network unreachable") |
|
|
| transport = httpx.MockTransport(custom_transport) |
| client = httpx.AsyncClient(transport=transport) |
|
|
| with pytest.raises(httpx.NetworkError): |
| await client.get("https://api.example.com/fail") |
|
|
| await client.aclose() |
|
|
| def test_sync_mock_with_transport(self): |
| """Test mocking synchronous requests.""" |
| import httpx |
|
|
| def custom_transport(request): |
| return httpx.Response( |
| 200, |
| json={"sync": "true"}, |
| request=request |
| ) |
|
|
| transport = httpx.MockTransport(custom_transport) |
| client = httpx.Client(transport=transport) |
|
|
| response = client.get("https://api.example.com/test") |
|
|
| assert response.status_code == 200 |
| assert response.json()["sync"] == "true" |
|
|
| client.close() |
|
|