"""Tests for async FastGPT clients.""" import asyncio from unittest.mock import Mock, AsyncMock, MagicMock, patch import pytest import httpx class AsyncContextManagerMock: """Helper class to mock async context managers.""" def __init__(self, return_value): self._return_value = return_value self.__aenter__ = AsyncMock(return_value=return_value) self.__aexit__ = AsyncMock(return_value=None) from fastgpt_client.async_client import ( AsyncFastGPTClient, AsyncChatClient, AsyncAppClient, ) from fastgpt_client.exceptions import ( APIError, AuthenticationError, RateLimitError, ValidationError, ) class TestAsyncFastGPTClient: """Test suite for AsyncFastGPTClient.""" @pytest.mark.asyncio async def test_init_default(self, api_key, base_url): """Test client initialization with defaults.""" client = AsyncFastGPTClient(api_key) assert client.api_key == api_key assert client.base_url == base_url assert client.timeout == 60.0 assert client.max_retries == 3 assert client.retry_delay == 1.0 assert client.enable_logging is False assert isinstance(client._client, httpx.AsyncClient) await client.close() @pytest.mark.asyncio async def test_async_context_manager(self, api_key): """Test using async client as context manager.""" async with AsyncFastGPTClient(api_key) as client: assert client.api_key == api_key assert not client._client.is_closed # Client should be closed after exiting context assert client._client.is_closed @pytest.mark.asyncio async def test_async_context_manager_with_exception(self, api_key): """Test async context manager properly closes on exception.""" client = AsyncFastGPTClient(api_key) try: async with client: raise ValueError("Test exception") except ValueError: pass # Client should still be closed even with exception assert client._client.is_closed @pytest.mark.asyncio async def test_close(self, api_key): """Test closing the async client.""" client = AsyncFastGPTClient(api_key) assert not client._client.is_closed await client.close() assert client._client.is_closed class TestAsyncFastGPTClientSendRequest: """Test suite for AsyncFastGPTClient._send_request method.""" @pytest.mark.asyncio async def test_send_request_get_success(self, api_key, mock_response): """Test successful async GET request.""" client = AsyncFastGPTClient(api_key) async def mock_request(*args, **kwargs): return mock_response with patch.object(client._client, 'request', mock_request): response = await client._send_request("GET", "/api/test") assert response.status_code == 200 await client.close() @pytest.mark.asyncio async def test_send_request_post_success(self, api_key, mock_response): """Test successful async POST request with JSON body.""" client = AsyncFastGPTClient(api_key) async def mock_request(*args, **kwargs): return mock_response with patch.object(client._client, 'request', mock_request) as mock_req: response = await client._send_request( "POST", "/api/test", json={"key": "value"} ) assert response.status_code == 200 await client.close() @pytest.mark.asyncio async def test_send_request_with_params(self, api_key, mock_response): """Test async request with query parameters.""" client = AsyncFastGPTClient(api_key) async def mock_request(*args, **kwargs): return mock_response with patch.object(client._client, 'request', mock_request): response = await client._send_request( "GET", "/api/test", params={"page": 1, "limit": 10} ) assert response.status_code == 200 await client.close() @pytest.mark.asyncio async def test_send_request_with_streaming(self, api_key, mock_stream_response): """Test async streaming request.""" client = AsyncFastGPTClient(api_key) mock_stream_context = AsyncContextManagerMock(mock_stream_response) # stream() is not async, it returns an async context manager def mock_stream(*args, **kwargs): return mock_stream_context with patch.object(client._client, 'stream', mock_stream): response = await client._send_request( "POST", "/api/test/stream", stream=True ) assert response.status_code == 200 mock_stream_context.__aenter__.assert_called_once() await client.close() @pytest.mark.asyncio async def test_send_request_stream_cleanup(self, api_key): """Test that async streaming response cleanup works correctly.""" client = AsyncFastGPTClient(api_key) mock_response = Mock(spec=httpx.Response) mock_response.status_code = 200 # Track if close was called on the response original_close_called = [] async def original_close(): original_close_called.append(True) mock_response.close = original_close mock_stream_context = AsyncContextManagerMock(mock_response) # stream() is not async, it returns an async context manager def mock_stream(*args, **kwargs): return mock_stream_context with patch.object(client._client, 'stream', mock_stream): response = await client._send_request("POST", "/api/stream", stream=True) await response.close() # Verify stream context exit was called mock_stream_context.__aexit__.assert_called_once_with(None, None, None) # Verify the original close was called assert len(original_close_called) == 1 await client.close() @pytest.mark.asyncio async def test_send_request_authentication_error(self, api_key, error_responses): """Test handling of 401 authentication error.""" client = AsyncFastGPTClient(api_key) mock_response = Mock(spec=httpx.Response) mock_response.status_code = 401 mock_response.json = Mock(return_value=error_responses['authentication_error']) async def mock_request(*args, **kwargs): return mock_response with patch.object(client._client, 'request', mock_request): with pytest.raises(AuthenticationError) as exc_info: await client._send_request("GET", "/api/test") assert exc_info.value.status_code == 401 await client.close() @pytest.mark.asyncio async def test_send_request_rate_limit_error(self, api_key, error_responses): """Test handling of 429 rate limit error.""" client = AsyncFastGPTClient(api_key) mock_response = Mock(spec=httpx.Response) mock_response.status_code = 429 mock_response.headers = {"Retry-After": "60"} mock_response.json = Mock(return_value=error_responses['rate_limit_error']) async def mock_request(*args, **kwargs): return mock_response with patch.object(client._client, 'request', mock_request): with pytest.raises(RateLimitError) as exc_info: await client._send_request("GET", "/api/test") assert exc_info.value.status_code == 429 assert exc_info.value.retry_after == "60" await client.close() @pytest.mark.asyncio async def test_send_request_validation_error(self, api_key, error_responses): """Test handling of 422 validation error.""" client = AsyncFastGPTClient(api_key) mock_response = Mock(spec=httpx.Response) mock_response.status_code = 422 mock_response.json = Mock(return_value=error_responses['validation_error']) async def mock_request(*args, **kwargs): return mock_response with patch.object(client._client, 'request', mock_request): with pytest.raises(ValidationError) as exc_info: await client._send_request("GET", "/api/test") assert exc_info.value.status_code == 422 await client.close() @pytest.mark.asyncio async def test_send_request_retry_on_server_error(self, api_key, mock_response): """Test that request is retried on 5xx errors.""" client = AsyncFastGPTClient(api_key, max_retries=2, retry_delay=0.01) error_response = Mock(spec=httpx.Response) error_response.status_code = 503 call_count = [0] async def mock_request(*args, **kwargs): call_count[0] += 1 if call_count[0] == 1: return error_response return mock_response with patch.object(client._client, 'request', mock_request): response = await client._send_request("GET", "/api/test") assert response.status_code == 200 assert call_count[0] == 2 await client.close() @pytest.mark.asyncio async def test_retry_request_exponential_backoff(self, api_key, mock_response): """Test exponential backoff in async retry logic.""" client = AsyncFastGPTClient(api_key, max_retries=4, retry_delay=0.05) error_response = Mock(spec=httpx.Response) error_response.status_code = 503 call_count = [0] async def mock_request(*args, **kwargs): call_count[0] += 1 if call_count[0] < 4: return error_response return mock_response import time start_time = time.time() with patch.object(client._client, 'request', mock_request): response = await client._send_request("GET", "/api/test") elapsed_time = time.time() - start_time assert response.status_code == 200 assert call_count[0] == 4 # Should have exponential backoff delays assert elapsed_time >= 0.25 await client.close() class TestAsyncChatClient: """Test suite for AsyncChatClient.""" @pytest.mark.asyncio async def test_create_chat_completion_basic(self, api_key, sample_chat_response): """Test basic async chat completion creation.""" client = AsyncChatClient(api_key) mock_response = Mock(spec=httpx.Response) mock_response.status_code = 200 mock_response.json = Mock(return_value=sample_chat_response) with patch.object(client, '_send_request', AsyncMock(return_value=mock_response)): response = await client.create_chat_completion( messages=[{"role": "user", "content": "Hello"}], stream=False ) assert response.status_code == 200 await client.close() @pytest.mark.asyncio async def test_create_chat_completion_with_chat_id(self, api_key): """Test async chat completion with chatId parameter.""" client = AsyncChatClient(api_key) mock_response = Mock(spec=httpx.Response) mock_response.status_code = 200 with patch.object(client, '_send_request', AsyncMock(return_value=mock_response)) as mock_send: response = await client.create_chat_completion( messages=[{"role": "user", "content": "Hello"}], chatId="chat-123" ) assert response.status_code == 200 assert mock_send.call_args[1]['json']['chatId'] == "chat-123" await client.close() @pytest.mark.asyncio async def test_create_chat_completion_streaming(self, api_key, mock_stream_response): """Test async streaming chat completion.""" client = AsyncChatClient(api_key) with patch.object(client, '_send_request', AsyncMock(return_value=mock_stream_response)): response = await client.create_chat_completion( messages=[{"role": "user", "content": "Hello"}], stream=True ) assert response.status_code == 200 await client.close() @pytest.mark.asyncio async def test_get_chat_histories_basic(self, api_key, sample_chat_histories_response): """Test getting chat histories with basic parameters.""" client = AsyncChatClient(api_key) mock_response = Mock(spec=httpx.Response) mock_response.status_code = 200 mock_response.json = Mock(return_value=sample_chat_histories_response) with patch.object(client, '_send_request', AsyncMock(return_value=mock_response)): response = await client.get_chat_histories(appId="app-123") assert response.status_code == 200 await client.close() @pytest.mark.asyncio async def test_get_chat_init(self, api_key): """Test getting chat initialization.""" client = AsyncChatClient(api_key) mock_response = Mock(spec=httpx.Response) mock_response.status_code = 200 with patch.object(client, '_send_request', AsyncMock(return_value=mock_response)): response = await client.get_chat_init(appId="app-123", chatId="chat-123") assert response.status_code == 200 await client.close() @pytest.mark.asyncio async def test_get_chat_records(self, api_key, sample_chat_records_response): """Test getting chat records.""" client = AsyncChatClient(api_key) mock_response = Mock(spec=httpx.Response) mock_response.status_code = 200 mock_response.json = Mock(return_value=sample_chat_records_response) with patch.object(client, '_send_request', AsyncMock(return_value=mock_response)): response = await client.get_chat_records(appId="app-123", chatId="chat-123") assert response.status_code == 200 await client.close() @pytest.mark.asyncio async def test_get_record_detail(self, api_key): """Test getting record detail.""" client = AsyncChatClient(api_key) mock_response = Mock(spec=httpx.Response) mock_response.status_code = 200 with patch.object(client, '_send_request', AsyncMock(return_value=mock_response)): response = await client.get_record_detail( appId="app-123", chatId="chat-123", dataId="data-123" ) assert response.status_code == 200 await client.close() @pytest.mark.asyncio async def test_update_chat_history(self, api_key): """Test updating chat history.""" client = AsyncChatClient(api_key) mock_response = Mock(spec=httpx.Response) mock_response.status_code = 200 with patch.object(client, '_send_request', AsyncMock(return_value=mock_response)): response = await client.update_chat_history( appId="app-123", chatId="chat-123", customTitle="New Title" ) assert response.status_code == 200 await client.close() @pytest.mark.asyncio async def test_delete_chat_history(self, api_key): """Test deleting a chat history.""" client = AsyncChatClient(api_key) mock_response = Mock(spec=httpx.Response) mock_response.status_code = 200 with patch.object(client, '_send_request', AsyncMock(return_value=mock_response)): response = await client.delete_chat_history(appId="app-123", chatId="chat-123") assert response.status_code == 200 await client.close() @pytest.mark.asyncio async def test_clear_chat_histories(self, api_key): """Test clearing all chat histories.""" client = AsyncChatClient(api_key) mock_response = Mock(spec=httpx.Response) mock_response.status_code = 200 with patch.object(client, '_send_request', AsyncMock(return_value=mock_response)): response = await client.clear_chat_histories(appId="app-123") assert response.status_code == 200 await client.close() @pytest.mark.asyncio async def test_delete_chat_record(self, api_key): """Test deleting a single chat record.""" client = AsyncChatClient(api_key) mock_response = Mock(spec=httpx.Response) mock_response.status_code = 200 with patch.object(client, '_send_request', AsyncMock(return_value=mock_response)): response = await client.delete_chat_record( appId="app-123", chatId="chat-123", contentId="content-123" ) assert response.status_code == 200 await client.close() @pytest.mark.asyncio async def test_send_feedback(self, api_key): """Test sending feedback.""" client = AsyncChatClient(api_key) mock_response = Mock(spec=httpx.Response) mock_response.status_code = 200 with patch.object(client, '_send_request', AsyncMock(return_value=mock_response)): response = await client.send_feedback( appId="app-123", chatId="chat-123", dataId="data-123", userGoodFeedback="Great!" ) assert response.status_code == 200 await client.close() @pytest.mark.asyncio async def test_get_suggested_questions(self, api_key): """Test getting suggested questions.""" client = AsyncChatClient(api_key) mock_response = Mock(spec=httpx.Response) mock_response.status_code = 200 with patch.object(client, '_send_request', AsyncMock(return_value=mock_response)): response = await client.get_suggested_questions( appId="app-123", chatId="chat-123" ) assert response.status_code == 200 await client.close() class TestAsyncAppClient: """Test suite for AsyncAppClient.""" @pytest.mark.asyncio async def test_get_app_logs_chart_basic(self, api_key, sample_app_logs_response): """Test getting app logs chart with basic parameters.""" client = AsyncAppClient(api_key) mock_response = Mock(spec=httpx.Response) mock_response.status_code = 200 mock_response.json = Mock(return_value=sample_app_logs_response) with patch.object(client, '_send_request', AsyncMock(return_value=mock_response)): response = await client.get_app_logs_chart( appId="app-123", dateStart="2024-01-01", dateEnd="2024-01-31" ) assert response.status_code == 200 await client.close() @pytest.mark.asyncio async def test_get_app_logs_chart_all_parameters(self, api_key): """Test getting app logs chart with all parameters.""" client = AsyncAppClient(api_key) mock_response = Mock(spec=httpx.Response) mock_response.status_code = 200 with patch.object(client, '_send_request', AsyncMock(return_value=mock_response)): response = await client.get_app_logs_chart( appId="app-123", dateStart="2024-01-01", dateEnd="2024-12-31", offset=10, source=["api", "online"], userTimespan="week", chatTimespan="month", appTimespan="day" ) assert response.status_code == 200 await client.close()