Add comprehensive unit test suite

- Add 111 unit tests covering all client classes and exceptions
- Test BaseClientMixin: initialization, validation, retry logic
- Test FastGPTClient: HTTP requests, streaming, error handling, context manager
- Test ChatClient: all chat operations (completion, histories, records, feedback)
- Test AppClient: app analytics and logs
- Test all exception classes with various configurations
- Add shared pytest fixtures in conftest.py

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
Xin Wang
2026-01-06 16:17:43 +08:00
parent 017bdc4b53
commit b322ef1d7a
6 changed files with 1938 additions and 0 deletions

168
tests/conftest.py Normal file
View File

@@ -0,0 +1,168 @@
"""Pytest configuration and fixtures for FastGPT client tests."""
import pytest
from unittest.mock import Mock, MagicMock
import httpx
@pytest.fixture
def api_key():
"""Test API key."""
return "fastgpt-test-key-12345"
@pytest.fixture
def base_url():
"""Test base URL."""
return "http://localhost:3000"
@pytest.fixture
def mock_response():
"""Create a mock httpx.Response object."""
response = Mock(spec=httpx.Response)
response.status_code = 200
response.headers = {}
response._content = b'{"data": "test"}'
response.request = Mock()
response.request.method = "GET"
response.request.url = "http://test.com"
return response
@pytest.fixture
def mock_stream_response():
"""Create a mock streaming httpx.Response object."""
response = Mock(spec=httpx.Response)
response.status_code = 200
response.headers = {}
response._content = b'data: {"content": "test"}\n\n'
response.request = Mock()
response.request.method = "POST"
response.request.url = "http://test.com/stream"
response.iter_lines = Mock(return_value=[b'data: {"content": "test"}\n\n'])
return response
@pytest.fixture
def mock_httpx_client():
"""Create a mock httpx.Client."""
client = Mock(spec=httpx.Client)
client.is_closed = False
return client
@pytest.fixture
def sample_chat_response():
"""Sample chat completion response data."""
return {
"id": "chatcmpl-123",
"object": "chat.completion",
"created": 1234567890,
"model": "gpt-3.5-turbo",
"choices": [
{
"index": 0,
"message": {
"role": "assistant",
"content": "Hello! How can I help you today?"
},
"finish_reason": "stop"
}
],
"usage": {
"prompt_tokens": 10,
"completion_tokens": 20,
"total_tokens": 30
}
}
@pytest.fixture
def sample_chat_histories_response():
"""Sample chat histories response data."""
return {
"data": [
{
"chatId": "chat-123",
"customTitle": "Test Chat",
"time": 1234567890,
"top": False
}
],
"total": 1
}
@pytest.fixture
def sample_chat_records_response():
"""Sample chat records response data."""
return {
"data": [
{
"dataId": "msg-123",
"content": {
"text": "Hello!"
},
"time": 1234567890,
"feedback": {
"userGoodFeedback": "Great!",
"userBadFeedback": None
}
}
]
}
@pytest.fixture
def sample_app_logs_response():
"""Sample app logs chart response data."""
return {
"data": {
"users": {
"day": [
{"time": "2024-01-01", "count": 10},
{"time": "2024-01-02", "count": 15}
]
},
"chats": {
"day": [
{"time": "2024-01-01", "count": 25},
{"time": "2024-01-02", "count": 30}
]
},
"app": {
"day": [
{"time": "2024-01-01", "count": 100},
{"time": "2024-01-02", "count": 120}
]
}
}
}
@pytest.fixture
def error_responses():
"""Sample error response data."""
return {
"authentication_error": {
"code": "invalid_api_key",
"message": "Invalid API key",
"status": 401
},
"rate_limit_error": {
"code": "rate_limit_exceeded",
"message": "Rate limit exceeded",
"status": 429
},
"validation_error": {
"code": "invalid_parameters",
"message": "Invalid parameters",
"status": 422
},
"server_error": {
"code": "internal_error",
"message": "Internal server error",
"status": 500
}
}

180
tests/test_app_client.py Normal file
View File

@@ -0,0 +1,180 @@
"""Tests for AppClient."""
from unittest.mock import Mock, patch
import pytest
import httpx
from fastgpt_client.client import AppClient
class TestAppClientGetAppLogsChart:
"""Test suite for AppClient.get_app_logs_chart method."""
def test_get_app_logs_chart_basic(self, api_key, sample_app_logs_response):
"""Test getting app logs chart with basic parameters."""
client = AppClient(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', return_value=mock_response) as mock_send:
response = client.get_app_logs_chart(
appId="app-123",
dateStart="2024-01-01",
dateEnd="2024-01-31"
)
assert response.status_code == 200
call_args = mock_send.call_args
assert call_args[0][0] == "POST"
assert call_args[0][1] == "/api/proApi/core/app/logs/getChartData"
json_data = call_args[1]['json']
assert json_data['appId'] == "app-123"
assert json_data['dateStart'] == "2024-01-01"
assert json_data['dateEnd'] == "2024-01-31"
# Default source should be ["api"]
assert json_data['source'] == ["api"]
def test_get_app_logs_chart_with_offset(self, api_key):
"""Test getting app logs chart with custom offset."""
client = AppClient(api_key)
mock_response = Mock(spec=httpx.Response)
mock_response.status_code = 200
with patch.object(client, '_send_request', return_value=mock_response) as mock_send:
response = client.get_app_logs_chart(
appId="app-123",
dateStart="2024-01-01",
dateEnd="2024-01-31",
offset=5
)
assert mock_send.call_args[1]['json']['offset'] == 5
def test_get_app_logs_chart_with_source_list(self, api_key):
"""Test getting app logs chart with custom source list."""
client = AppClient(api_key)
mock_response = Mock(spec=httpx.Response)
mock_response.status_code = 200
sources = ["api", "online", "share"]
with patch.object(client, '_send_request', return_value=mock_response) as mock_send:
response = client.get_app_logs_chart(
appId="app-123",
dateStart="2024-01-01",
dateEnd="2024-01-31",
source=sources
)
assert mock_send.call_args[1]['json']['source'] == sources
def test_get_app_logs_chart_with_all_timespans(self, api_key):
"""Test getting app logs chart with custom timespans."""
client = AppClient(api_key)
mock_response = Mock(spec=httpx.Response)
mock_response.status_code = 200
with patch.object(client, '_send_request', return_value=mock_response) as mock_send:
response = client.get_app_logs_chart(
appId="app-123",
dateStart="2024-01-01",
dateEnd="2024-01-31",
userTimespan="week",
chatTimespan="month",
appTimespan="day"
)
json_data = mock_send.call_args[1]['json']
assert json_data['userTimespan'] == "week"
assert json_data['chatTimespan'] == "month"
assert json_data['appTimespan'] == "day"
def test_get_app_logs_chart_all_parameters(self, api_key):
"""Test getting app logs chart with all parameters."""
client = AppClient(api_key)
mock_response = Mock(spec=httpx.Response)
mock_response.status_code = 200
with patch.object(client, '_send_request', return_value=mock_response) as mock_send:
response = client.get_app_logs_chart(
appId="app-123",
dateStart="2024-01-01",
dateEnd="2024-12-31",
offset=10,
source=["api", "online", "share", "test"],
userTimespan="month",
chatTimespan="week",
appTimespan="day"
)
json_data = mock_send.call_args[1]['json']
assert json_data == {
"appId": "app-123",
"dateStart": "2024-01-01",
"dateEnd": "2024-12-31",
"offset": 10,
"source": ["api", "online", "share", "test"],
"userTimespan": "month",
"chatTimespan": "week",
"appTimespan": "day"
}
def test_get_app_logs_chart_source_none(self, api_key):
"""Test that source=None results in default ['api']."""
client = AppClient(api_key)
mock_response = Mock(spec=httpx.Response)
mock_response.status_code = 200
with patch.object(client, '_send_request', return_value=mock_response) as mock_send:
response = client.get_app_logs_chart(
appId="app-123",
dateStart="2024-01-01",
dateEnd="2024-01-31",
source=None
)
# When source is None, it defaults to ["api"]
assert mock_send.call_args[1]['json']['source'] == ["api"]
def test_get_app_logs_chart_default_timespans(self, api_key):
"""Test that default timespans are 'day'."""
client = AppClient(api_key)
mock_response = Mock(spec=httpx.Response)
mock_response.status_code = 200
with patch.object(client, '_send_request', return_value=mock_response) as mock_send:
response = client.get_app_logs_chart(
appId="app-123",
dateStart="2024-01-01",
dateEnd="2024-01-31"
)
json_data = mock_send.call_args[1]['json']
assert json_data['userTimespan'] == "day"
assert json_data['chatTimespan'] == "day"
assert json_data['appTimespan'] == "day"
def test_get_app_logs_chart_default_offset(self, api_key):
"""Test that default offset is 1."""
client = AppClient(api_key)
mock_response = Mock(spec=httpx.Response)
mock_response.status_code = 200
with patch.object(client, '_send_request', return_value=mock_response) as mock_send:
response = client.get_app_logs_chart(
appId="app-123",
dateStart="2024-01-01",
dateEnd="2024-01-31"
)
assert mock_send.call_args[1]['json']['offset'] == 1

220
tests/test_base_client.py Normal file
View File

@@ -0,0 +1,220 @@
"""Tests for BaseClientMixin."""
import time
from unittest.mock import Mock, patch
import pytest
import httpx
from fastgpt_client.base_client import BaseClientMixin
from fastgpt_client.exceptions import APIError, ValidationError
class TestBaseClientMixin:
"""Test suite for BaseClientMixin."""
def test_init_default_parameters(self, api_key, base_url):
"""Test initialization with default parameters."""
mixin = BaseClientMixin(api_key, base_url)
assert mixin.api_key == api_key
assert mixin.base_url == base_url
assert mixin.timeout == 60.0
assert mixin.max_retries == 3
assert mixin.retry_delay == 1.0
assert mixin.enable_logging is False
def test_init_custom_parameters(self, api_key, base_url):
"""Test initialization with custom parameters."""
mixin = BaseClientMixin(
api_key,
base_url,
timeout=120.0,
max_retries=5,
retry_delay=2.0,
enable_logging=True
)
assert mixin.timeout == 120.0
assert mixin.max_retries == 5
assert mixin.retry_delay == 2.0
assert mixin.enable_logging is True
def test_validate_params_with_valid_params(self, api_key, base_url):
"""Test parameter validation with valid parameters."""
mixin = BaseClientMixin(api_key, base_url)
# Should not raise any exception
mixin._validate_params(
query="test query",
chatId="chat-123",
appId="app-123",
dataId="data-123",
content="some content"
)
def test_validate_params_with_empty_strings(self, api_key, base_url):
"""Test parameter validation with empty strings."""
mixin = BaseClientMixin(api_key, base_url)
with pytest.raises(ValidationError) as exc_info:
mixin._validate_params(query="")
assert "query must be a non-empty string" in str(exc_info.value)
def test_validate_params_with_whitespace_only(self, api_key, base_url):
"""Test parameter validation with whitespace-only strings."""
mixin = BaseClientMixin(api_key, base_url)
with pytest.raises(ValidationError) as exc_info:
mixin._validate_params(chatId=" ")
assert "chatId must be a non-empty string" in str(exc_info.value)
def test_validate_params_with_none_values(self, api_key, base_url):
"""Test parameter validation ignores None values."""
mixin = BaseClientMixin(api_key, base_url)
# Should not raise any exception for None values
mixin._validate_params(
query=None,
chatId=None,
appId="app-123" # Only one non-None value
)
def test_validate_params_with_empty_collections(self, api_key, base_url):
"""Test parameter validation with empty collections."""
mixin = BaseClientMixin(
api_key,
base_url,
enable_logging=True
)
# Should not raise exception, just log debug message
mixin._validate_params(
messages=[],
variables={}
)
def test_retry_request_success_on_first_attempt(self, api_key, base_url, mock_response):
"""Test retry request succeeds on first attempt."""
mixin = BaseClientMixin(api_key, base_url)
request_func = Mock(return_value=mock_response)
result = mixin._retry_request(request_func, "GET /api/test")
assert result == mock_response
assert request_func.call_count == 1
def test_retry_request_success_on_second_attempt(self, api_key, base_url, mock_response):
"""Test retry request succeeds on second attempt after 500 error."""
mixin = BaseClientMixin(api_key, base_url, max_retries=3, retry_delay=0.01)
# First call returns 500, second succeeds
error_response = Mock(spec=httpx.Response)
error_response.status_code = 500
request_func = Mock(side_effect=[error_response, mock_response])
start_time = time.time()
result = mixin._retry_request(request_func, "GET /api/test")
elapsed_time = time.time() - start_time
assert result == mock_response
assert request_func.call_count == 2
# Should have slept at least retry_delay seconds (with some tolerance for test execution)
assert elapsed_time >= 0.005 # Allow some tolerance
def test_retry_request_exhausted_with_server_errors(self, api_key, base_url):
"""Test retry request exhausted by server errors."""
mixin = BaseClientMixin(api_key, base_url, max_retries=2, retry_delay=0.01)
error_response = Mock(spec=httpx.Response)
error_response.status_code = 500
request_func = Mock(return_value=error_response)
with pytest.raises(APIError) as exc_info:
mixin._retry_request(request_func, "GET /api/test")
assert "failed after 2 attempts" in str(exc_info.value)
assert request_func.call_count == 2
def test_retry_request_with_network_exception(self, api_key, base_url):
"""Test retry request with network exception."""
mixin = BaseClientMixin(api_key, base_url, max_retries=2, retry_delay=0.01)
request_func = Mock(side_effect=httpx.ConnectError("Connection failed"))
with pytest.raises(APIError) as exc_info:
mixin._retry_request(request_func, "GET /api/test")
assert "failed after 2 attempts" in str(exc_info.value)
assert "Connection failed" in str(exc_info.value)
assert request_func.call_count == 2
def test_retry_request_exponential_backoff(self, api_key, base_url, mock_response):
"""Test exponential backoff in retry logic."""
mixin = BaseClientMixin(api_key, base_url, max_retries=4, retry_delay=0.05)
error_response = Mock(spec=httpx.Response)
error_response.status_code = 503
# First 3 attempts fail, last succeeds
request_func = Mock(side_effect=[
error_response,
error_response,
error_response,
mock_response
])
start_time = time.time()
result = mixin._retry_request(request_func, "GET /api/test")
elapsed_time = time.time() - start_time
assert result == mock_response
assert request_func.call_count == 4
# Exponential backoff: 0.05 + 0.10 + 0.20 = 0.35 seconds minimum
# Allow some tolerance for test execution time
assert elapsed_time >= 0.25
def test_retry_request_4xx_no_retry(self, api_key, base_url):
"""Test that 4xx errors don't trigger retries."""
mixin = BaseClientMixin(api_key, base_url, max_retries=3, retry_delay=0.01)
error_response = Mock(spec=httpx.Response)
error_response.status_code = 404
request_func = Mock(return_value=error_response)
result = mixin._retry_request(request_func, "GET /api/test")
# Should return the 4xx response without retrying
assert result == error_response
assert request_func.call_count == 1
def test_retry_request_mixed_exception_and_success(self, api_key, base_url, mock_response):
"""Test retry request with exception followed by success."""
mixin = BaseClientMixin(api_key, base_url, max_retries=3, retry_delay=0.01)
request_func = Mock(side_effect=[
httpx.TimeoutException("Timeout"),
mock_response
])
result = mixin._retry_request(request_func, "POST /api/test")
assert result == mock_response
assert request_func.call_count == 2
def test_retry_request_all_attempts_with_exceptions(self, api_key, base_url):
"""Test retry request when all attempts raise exceptions."""
mixin = BaseClientMixin(api_key, base_url, max_retries=2, retry_delay=0.01)
request_func = Mock(side_effect=httpx.TimeoutException("Timeout"))
with pytest.raises(APIError) as exc_info:
mixin._retry_request(request_func, "POST /api/test")
assert "failed after 2 attempts" in str(exc_info.value)
assert "Timeout" in str(exc_info.value)
assert request_func.call_count == 2

626
tests/test_chat_client.py Normal file
View File

@@ -0,0 +1,626 @@
"""Tests for ChatClient."""
from unittest.mock import Mock, patch
import pytest
import httpx
from fastgpt_client.client import ChatClient
from fastgpt_client.exceptions import ValidationError
class TestChatClientCreateChatCompletion:
"""Test suite for ChatClient.create_chat_completion method."""
def test_create_chat_completion_basic(self, api_key, sample_chat_response):
"""Test basic chat completion creation."""
client = ChatClient(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', return_value=mock_response) as mock_send:
response = client.create_chat_completion(
messages=[{"role": "user", "content": "Hello"}],
stream=False
)
assert response.status_code == 200
mock_send.assert_called_once()
call_args = mock_send.call_args
assert call_args[0][0] == "POST"
assert call_args[0][1] == "/api/v1/chat/completions"
assert call_args[1]['json']['messages'] == [{"role": "user", "content": "Hello"}]
assert call_args[1]['stream'] is False
def test_create_chat_completion_with_chat_id(self, api_key):
"""Test chat completion with chatId parameter."""
client = ChatClient(api_key)
mock_response = Mock(spec=httpx.Response)
mock_response.status_code = 200
with patch.object(client, '_send_request', return_value=mock_response) as mock_send:
response = 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"
def test_create_chat_completion_with_variables(self, api_key):
"""Test chat completion with variables parameter."""
client = ChatClient(api_key)
mock_response = Mock(spec=httpx.Response)
mock_response.status_code = 200
with patch.object(client, '_send_request', return_value=mock_response) as mock_send:
response = client.create_chat_completion(
messages=[{"role": "user", "content": "Hello"}],
variables={"name": "John", "city": "NYC"}
)
assert response.status_code == 200
assert mock_send.call_args[1]['json']['variables'] == {"name": "John", "city": "NYC"}
def test_create_chat_completion_with_detail(self, api_key):
"""Test chat completion with detail enabled."""
client = ChatClient(api_key)
mock_response = Mock(spec=httpx.Response)
mock_response.status_code = 200
with patch.object(client, '_send_request', return_value=mock_response) as mock_send:
response = client.create_chat_completion(
messages=[{"role": "user", "content": "Hello"}],
detail=True
)
assert response.status_code == 200
assert mock_send.call_args[1]['json']['detail'] is True
def test_create_chat_completion_with_response_chat_item_id(self, api_key):
"""Test chat completion with custom responseChatItemId."""
client = ChatClient(api_key)
mock_response = Mock(spec=httpx.Response)
mock_response.status_code = 200
with patch.object(client, '_send_request', return_value=mock_response) as mock_send:
response = client.create_chat_completion(
messages=[{"role": "user", "content": "Hello"}],
responseChatItemId="custom-id-123"
)
assert response.status_code == 200
assert mock_send.call_args[1]['json']['responseChatItemId'] == "custom-id-123"
def test_create_chat_completion_streaming(self, api_key, mock_stream_response):
"""Test streaming chat completion."""
client = ChatClient(api_key)
with patch.object(client, '_send_request', return_value=mock_stream_response) as mock_send:
response = client.create_chat_completion(
messages=[{"role": "user", "content": "Hello"}],
stream=True
)
assert response.status_code == 200
assert mock_send.call_args[1]['stream'] is True
def test_create_chat_completion_all_parameters(self, api_key):
"""Test chat completion with all parameters."""
client = ChatClient(api_key)
mock_response = Mock(spec=httpx.Response)
mock_response.status_code = 200
with patch.object(client, '_send_request', return_value=mock_response) as mock_send:
response = client.create_chat_completion(
messages=[{"role": "user", "content": "Hello"}],
stream=True,
chatId="chat-123",
detail=True,
variables={"key": "value"},
responseChatItemId="custom-id"
)
assert response.status_code == 200
json_data = mock_send.call_args[1]['json']
assert json_data['chatId'] == "chat-123"
assert json_data['detail'] is True
assert json_data['variables'] == {"key": "value"}
assert json_data['responseChatItemId'] == "custom-id"
def test_create_chat_completion_parameter_validation(self, api_key):
"""Test that parameter validation works for known fields."""
client = ChatClient(api_key)
mock_response = Mock(spec=httpx.Response)
mock_response.status_code = 200
# Test with valid chatId (should pass validation)
with patch.object(client, '_send_request', return_value=mock_response) as mock_send:
response = client.create_chat_completion(
messages=[{"role": "user", "content": "Hello"}],
chatId="valid-chat-id"
)
# Verify the request was sent successfully
assert response.status_code == 200
mock_send.assert_called_once()
class TestChatClientGetChatHistories:
"""Test suite for ChatClient.get_chat_histories method."""
def test_get_chat_histories_basic(self, api_key, sample_chat_histories_response):
"""Test getting chat histories with basic parameters."""
client = ChatClient(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', return_value=mock_response) as mock_send:
response = client.get_chat_histories(appId="app-123")
assert response.status_code == 200
call_args = mock_send.call_args
assert call_args[0][0] == "POST"
assert call_args[0][1] == "/api/core/chat/getHistories"
assert call_args[1]['json']['appId'] == "app-123"
def test_get_chat_histories_with_pagination(self, api_key):
"""Test getting chat histories with pagination."""
client = ChatClient(api_key)
mock_response = Mock(spec=httpx.Response)
mock_response.status_code = 200
with patch.object(client, '_send_request', return_value=mock_response) as mock_send:
response = client.get_chat_histories(
appId="app-123",
offset=10,
pageSize=50
)
json_data = mock_send.call_args[1]['json']
assert json_data['offset'] == 10
assert json_data['pageSize'] == 50
def test_get_chat_histories_with_source(self, api_key):
"""Test getting chat histories with source filter."""
client = ChatClient(api_key)
mock_response = Mock(spec=httpx.Response)
mock_response.status_code = 200
with patch.object(client, '_send_request', return_value=mock_response) as mock_send:
response = client.get_chat_histories(
appId="app-123",
source="online"
)
assert mock_send.call_args[1]['json']['source'] == "online"
def test_get_chat_histories_all_parameters(self, api_key):
"""Test getting chat histories with all parameters."""
client = ChatClient(api_key)
mock_response = Mock(spec=httpx.Response)
mock_response.status_code = 200
with patch.object(client, '_send_request', return_value=mock_response) as mock_send:
response = client.get_chat_histories(
appId="app-123",
offset=5,
pageSize=25,
source="share"
)
json_data = mock_send.call_args[1]['json']
assert json_data == {
"appId": "app-123",
"offset": 5,
"pageSize": 25,
"source": "share"
}
class TestChatClientGetChatInit:
"""Test suite for ChatClient.get_chat_init method."""
def test_get_chat_init(self, api_key):
"""Test getting chat initialization."""
client = ChatClient(api_key)
mock_response = Mock(spec=httpx.Response)
mock_response.status_code = 200
with patch.object(client, '_send_request', return_value=mock_response) as mock_send:
response = client.get_chat_init(appId="app-123", chatId="chat-123")
assert response.status_code == 200
call_args = mock_send.call_args
assert call_args[0][0] == "GET"
assert call_args[0][1] == "/api/core/chat/init"
assert call_args[1]['params'] == {"appId": "app-123", "chatId": "chat-123"}
class TestChatClientGetChatRecords:
"""Test suite for ChatClient.get_chat_records method."""
def test_get_chat_records_basic(self, api_key, sample_chat_records_response):
"""Test getting chat records with basic parameters."""
client = ChatClient(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', return_value=mock_response) as mock_send:
response = client.get_chat_records(appId="app-123", chatId="chat-123")
assert response.status_code == 200
call_args = mock_send.call_args
assert call_args[0][0] == "POST"
assert call_args[0][1] == "/api/core/chat/getPaginationRecords"
json_data = call_args[1]['json']
assert json_data['appId'] == "app-123"
assert json_data['chatId'] == "chat-123"
def test_get_chat_records_with_pagination(self, api_key):
"""Test getting chat records with pagination."""
client = ChatClient(api_key)
mock_response = Mock(spec=httpx.Response)
mock_response.status_code = 200
with patch.object(client, '_send_request', return_value=mock_response) as mock_send:
response = client.get_chat_records(
appId="app-123",
chatId="chat-123",
offset=20,
pageSize=30
)
json_data = mock_send.call_args[1]['json']
assert json_data['offset'] == 20
assert json_data['pageSize'] == 30
def test_get_chat_records_with_custom_feedbacks(self, api_key):
"""Test getting chat records with custom feedbacks loaded."""
client = ChatClient(api_key)
mock_response = Mock(spec=httpx.Response)
mock_response.status_code = 200
with patch.object(client, '_send_request', return_value=mock_response) as mock_send:
response = client.get_chat_records(
appId="app-123",
chatId="chat-123",
loadCustomFeedbacks=True
)
assert mock_send.call_args[1]['json']['loadCustomFeedbacks'] is True
def test_get_chat_records_all_parameters(self, api_key):
"""Test getting chat records with all parameters."""
client = ChatClient(api_key)
mock_response = Mock(spec=httpx.Response)
mock_response.status_code = 200
with patch.object(client, '_send_request', return_value=mock_response) as mock_send:
response = client.get_chat_records(
appId="app-123",
chatId="chat-123",
offset=10,
pageSize=20,
loadCustomFeedbacks=True
)
json_data = mock_send.call_args[1]['json']
assert json_data == {
"appId": "app-123",
"chatId": "chat-123",
"offset": 10,
"pageSize": 20,
"loadCustomFeedbacks": True
}
class TestChatClientGetRecordDetail:
"""Test suite for ChatClient.get_record_detail method."""
def test_get_record_detail(self, api_key):
"""Test getting record detail."""
client = ChatClient(api_key)
mock_response = Mock(spec=httpx.Response)
mock_response.status_code = 200
with patch.object(client, '_send_request', return_value=mock_response) as mock_send:
response = client.get_record_detail(
appId="app-123",
chatId="chat-123",
dataId="data-123"
)
assert response.status_code == 200
call_args = mock_send.call_args
assert call_args[0][0] == "GET"
assert call_args[0][1] == "/api/core/chat/getResData"
assert call_args[1]['params'] == {
"appId": "app-123",
"chatId": "chat-123",
"dataId": "data-123"
}
class TestChatClientUpdateChatHistory:
"""Test suite for ChatClient.update_chat_history method."""
def test_update_chat_history_title(self, api_key):
"""Test updating chat history title."""
client = ChatClient(api_key)
mock_response = Mock(spec=httpx.Response)
mock_response.status_code = 200
with patch.object(client, '_send_request', return_value=mock_response) as mock_send:
response = client.update_chat_history(
appId="app-123",
chatId="chat-123",
customTitle="New Title"
)
assert response.status_code == 200
json_data = mock_send.call_args[1]['json']
assert json_data['appId'] == "app-123"
assert json_data['chatId'] == "chat-123"
assert json_data['customTitle'] == "New Title"
assert 'top' not in json_data
def test_update_chat_history_pin(self, api_key):
"""Test pinning chat history."""
client = ChatClient(api_key)
mock_response = Mock(spec=httpx.Response)
mock_response.status_code = 200
with patch.object(client, '_send_request', return_value=mock_response) as mock_send:
response = client.update_chat_history(
appId="app-123",
chatId="chat-123",
top=True
)
json_data = mock_send.call_args[1]['json']
assert json_data['top'] is True
assert 'customTitle' not in json_data
def test_update_chat_history_both_parameters(self, api_key):
"""Test updating both title and pin status."""
client = ChatClient(api_key)
mock_response = Mock(spec=httpx.Response)
mock_response.status_code = 200
with patch.object(client, '_send_request', return_value=mock_response) as mock_send:
response = client.update_chat_history(
appId="app-123",
chatId="chat-123",
customTitle="Important Chat",
top=True
)
json_data = mock_send.call_args[1]['json']
assert json_data['customTitle'] == "Important Chat"
assert json_data['top'] is True
class TestChatClientDeleteChatHistory:
"""Test suite for ChatClient.delete_chat_history method."""
def test_delete_chat_history(self, api_key):
"""Test deleting a chat history."""
client = ChatClient(api_key)
mock_response = Mock(spec=httpx.Response)
mock_response.status_code = 200
with patch.object(client, '_send_request', return_value=mock_response) as mock_send:
response = client.delete_chat_history(appId="app-123", chatId="chat-123")
assert response.status_code == 200
call_args = mock_send.call_args
assert call_args[0][0] == "DELETE"
assert call_args[0][1] == "/api/core/chat/delHistory"
assert call_args[1]['params'] == {"appId": "app-123", "chatId": "chat-123"}
class TestChatClientClearChatHistories:
"""Test suite for ChatClient.clear_chat_histories method."""
def test_clear_chat_histories(self, api_key):
"""Test clearing all chat histories."""
client = ChatClient(api_key)
mock_response = Mock(spec=httpx.Response)
mock_response.status_code = 200
with patch.object(client, '_send_request', return_value=mock_response) as mock_send:
response = client.clear_chat_histories(appId="app-123")
assert response.status_code == 200
call_args = mock_send.call_args
assert call_args[0][0] == "DELETE"
assert call_args[0][1] == "/api/core/chat/clearHistories"
assert call_args[1]['params'] == {"appId": "app-123"}
class TestChatClientDeleteChatRecord:
"""Test suite for ChatClient.delete_chat_record method."""
def test_delete_chat_record(self, api_key):
"""Test deleting a single chat record."""
client = ChatClient(api_key)
mock_response = Mock(spec=httpx.Response)
mock_response.status_code = 200
with patch.object(client, '_send_request', return_value=mock_response) as mock_send:
response = client.delete_chat_record(
appId="app-123",
chatId="chat-123",
contentId="content-123"
)
assert response.status_code == 200
call_args = mock_send.call_args
assert call_args[0][0] == "DELETE"
assert call_args[0][1] == "/api/core/chat/item/delete"
assert call_args[1]['params'] == {
"appId": "app-123",
"chatId": "chat-123",
"contentId": "content-123"
}
class TestChatClientSendFeedback:
"""Test suite for ChatClient.send_feedback method."""
def test_send_feedback_good(self, api_key):
"""Test sending positive feedback."""
client = ChatClient(api_key)
mock_response = Mock(spec=httpx.Response)
mock_response.status_code = 200
with patch.object(client, '_send_request', return_value=mock_response) as mock_send:
response = client.send_feedback(
appId="app-123",
chatId="chat-123",
dataId="data-123",
userGoodFeedback="Great response!"
)
assert response.status_code == 200
call_args = mock_send.call_args
assert call_args[0][0] == "POST"
assert call_args[0][1] == "/api/core/chat/feedback/updateUserFeedback"
json_data = call_args[1]['json']
assert json_data['userGoodFeedback'] == "Great response!"
assert 'userBadFeedback' not in json_data
def test_send_feedback_bad(self, api_key):
"""Test sending negative feedback."""
client = ChatClient(api_key)
mock_response = Mock(spec=httpx.Response)
mock_response.status_code = 200
with patch.object(client, '_send_request', return_value=mock_response) as mock_send:
response = client.send_feedback(
appId="app-123",
chatId="chat-123",
dataId="data-123",
userBadFeedback="Not helpful"
)
json_data = mock_send.call_args[1]['json']
assert json_data['userBadFeedback'] == "Not helpful"
assert 'userGoodFeedback' not in json_data
def test_send_feedback_cancel_good(self, api_key):
"""Test canceling positive feedback by passing empty string."""
client = ChatClient(api_key)
mock_response = Mock(spec=httpx.Response)
mock_response.status_code = 200
with patch.object(client, '_send_request', return_value=mock_response) as mock_send:
response = client.send_feedback(
appId="app-123",
chatId="chat-123",
dataId="data-123",
userGoodFeedback=""
)
# Empty string is still sent (to cancel the feedback)
json_data = mock_send.call_args[1]['json']
assert 'userGoodFeedback' in json_data
def test_send_feedback_only_required_params(self, api_key):
"""Test sending feedback with only required parameters."""
client = ChatClient(api_key)
mock_response = Mock(spec=httpx.Response)
mock_response.status_code = 200
with patch.object(client, '_send_request', return_value=mock_response) as mock_send:
response = client.send_feedback(
appId="app-123",
chatId="chat-123",
dataId="data-123"
)
json_data = mock_send.call_args[1]['json']
assert json_data == {
"appId": "app-123",
"chatId": "chat-123",
"dataId": "data-123"
}
class TestChatClientGetSuggestedQuestions:
"""Test suite for ChatClient.get_suggested_questions method."""
def test_get_suggested_questions_basic(self, api_key):
"""Test getting suggested questions."""
client = ChatClient(api_key)
mock_response = Mock(spec=httpx.Response)
mock_response.status_code = 200
with patch.object(client, '_send_request', return_value=mock_response) as mock_send:
response = client.get_suggested_questions(
appId="app-123",
chatId="chat-123"
)
assert response.status_code == 200
call_args = mock_send.call_args
assert call_args[0][0] == "POST"
assert call_args[0][1] == "/api/core/ai/agent/v2/createQuestionGuide"
json_data = call_args[1]['json']
assert json_data['appId'] == "app-123"
assert json_data['chatId'] == "chat-123"
assert 'questionGuide' not in json_data
def test_get_suggested_questions_with_guide(self, api_key):
"""Test getting suggested questions with custom question guide."""
client = ChatClient(api_key)
mock_response = Mock(spec=httpx.Response)
mock_response.status_code = 200
question_guide = {
"maxQuestions": 5,
"contextWindow": 10
}
with patch.object(client, '_send_request', return_value=mock_response) as mock_send:
response = client.get_suggested_questions(
appId="app-123",
chatId="chat-123",
questionGuide=question_guide
)
json_data = mock_send.call_args[1]['json']
assert json_data['questionGuide'] == question_guide

387
tests/test_client.py Normal file
View File

@@ -0,0 +1,387 @@
"""Tests for FastGPTClient."""
from unittest.mock import Mock, MagicMock, patch
import pytest
import httpx
from fastgpt_client.client import FastGPTClient
from fastgpt_client.exceptions import (
APIError,
AuthenticationError,
RateLimitError,
ValidationError,
)
class TestFastGPTClient:
"""Test suite for FastGPTClient."""
def test_init_default(self, api_key, base_url):
"""Test client initialization with defaults."""
client = FastGPTClient(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.Client)
def test_init_custom_parameters(self, api_key):
"""Test client initialization with custom parameters."""
custom_url = "https://api.fastgpt.com"
client = FastGPTClient(
api_key,
base_url=custom_url,
timeout=120.0,
max_retries=5,
retry_delay=2.0,
enable_logging=True
)
assert client.api_key == api_key
assert client.base_url == custom_url
assert client.timeout == 120.0
assert client.max_retries == 5
assert client.retry_delay == 2.0
assert client.enable_logging is True
def test_context_manager(self, api_key):
"""Test using client as context manager."""
with FastGPTClient(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
def test_context_manager_with_exception(self, api_key):
"""Test context manager properly closes on exception."""
client = FastGPTClient(api_key)
try:
with client:
raise ValueError("Test exception")
except ValueError:
pass
# Client should still be closed even with exception
assert client._client.is_closed
def test_close(self, api_key):
"""Test closing the client."""
client = FastGPTClient(api_key)
assert not client._client.is_closed
client.close()
assert client._client.is_closed
def test_close_idempotent(self, api_key):
"""Test that close can be called multiple times safely."""
client = FastGPTClient(api_key)
client.close()
client.close() # Should not raise an exception
assert client._client.is_closed
class TestFastGPTClientSendRequest:
"""Test suite for FastGPTClient._send_request method."""
def test_send_request_get_success(self, api_key, mock_response):
"""Test successful GET request."""
client = FastGPTClient(api_key)
with patch.object(client._client, 'request', return_value=mock_response):
response = client._send_request("GET", "/api/test")
assert response.status_code == 200
def test_send_request_post_success(self, api_key, mock_response):
"""Test successful POST request with JSON body."""
client = FastGPTClient(api_key)
with patch.object(client._client, 'request', return_value=mock_response) as mock_request:
response = client._send_request(
"POST",
"/api/test",
json={"key": "value"}
)
assert response.status_code == 200
mock_request.assert_called_once()
call_kwargs = mock_request.call_args.kwargs
assert call_kwargs['json'] == {"key": "value"}
assert call_kwargs['headers']['Authorization'] == f"Bearer {api_key}"
def test_send_request_with_params(self, api_key, mock_response):
"""Test request with query parameters."""
client = FastGPTClient(api_key)
with patch.object(client._client, 'request', return_value=mock_response) as mock_request:
response = client._send_request(
"GET",
"/api/test",
params={"page": 1, "limit": 10}
)
assert response.status_code == 200
call_kwargs = mock_request.call_args.kwargs
assert call_kwargs['params'] == {"page": 1, "limit": 10}
def test_send_request_with_streaming(self, api_key, mock_stream_response):
"""Test streaming request."""
client = FastGPTClient(api_key)
mock_stream_context = MagicMock()
mock_stream_context.__enter__ = Mock(return_value=mock_stream_response)
mock_stream_context.__exit__ = Mock(return_value=None)
with patch.object(client._client, 'stream', return_value=mock_stream_context):
response = client._send_request(
"POST",
"/api/test/stream",
stream=True
)
assert response.status_code == 200
# Verify stream context was entered
mock_stream_context.__enter__.assert_called_once()
# Verify response has custom close method
assert hasattr(response, 'close')
def test_send_request_stream_cleanup(self, api_key):
"""Test that streaming response cleanup works correctly."""
client = FastGPTClient(api_key)
mock_response = Mock(spec=httpx.Response)
mock_response.status_code = 200
# Track if close was called on the response
original_close_called = []
def original_close():
original_close_called.append(True)
mock_response.close = original_close
mock_stream_context = MagicMock()
mock_stream_context.__enter__ = Mock(return_value=mock_response)
mock_stream_context.__exit__ = Mock(return_value=None)
with patch.object(client._client, 'stream', return_value=mock_stream_context):
response = client._send_request("POST", "/api/stream", stream=True)
response.close()
# Verify stream context exit was called (this is the key behavior)
mock_stream_context.__exit__.assert_called_once_with(None, None, None)
# Verify the original close was called (through the wrapper)
assert len(original_close_called) == 1
def test_send_request_authentication_error(self, api_key, error_responses):
"""Test handling of 401 authentication error."""
client = FastGPTClient(api_key)
mock_response = Mock(spec=httpx.Response)
mock_response.status_code = 401
mock_response.json = Mock(return_value=error_responses['authentication_error'])
with patch.object(client._client, 'request', return_value=mock_response):
with pytest.raises(AuthenticationError) as exc_info:
client._send_request("GET", "/api/test")
assert exc_info.value.status_code == 401
assert "Invalid API key" in exc_info.value.message
def test_send_request_rate_limit_error(self, api_key, error_responses):
"""Test handling of 429 rate limit error."""
client = FastGPTClient(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'])
with patch.object(client._client, 'request', return_value=mock_response):
with pytest.raises(RateLimitError) as exc_info:
client._send_request("GET", "/api/test")
assert exc_info.value.status_code == 429
assert exc_info.value.retry_after == "60"
def test_send_request_validation_error(self, api_key, error_responses):
"""Test handling of 422 validation error."""
client = FastGPTClient(api_key)
mock_response = Mock(spec=httpx.Response)
mock_response.status_code = 422
mock_response.json = Mock(return_value=error_responses['validation_error'])
with patch.object(client._client, 'request', return_value=mock_response):
with pytest.raises(ValidationError) as exc_info:
client._send_request("GET", "/api/test")
assert exc_info.value.status_code == 422
assert "Invalid parameters" in exc_info.value.message
def test_send_request_generic_api_error(self, api_key, error_responses):
"""Test handling of generic 4xx API error."""
client = FastGPTClient(api_key)
mock_response = Mock(spec=httpx.Response)
mock_response.status_code = 404
mock_response.json = Mock(return_value={"message": "Not found"})
with patch.object(client._client, 'request', return_value=mock_response):
with pytest.raises(APIError) as exc_info:
client._send_request("GET", "/api/test")
assert exc_info.value.status_code == 404
def test_send_request_with_invalid_json_error_response(self, api_key):
"""Test handling of error response with invalid JSON."""
client = FastGPTClient(api_key)
mock_response = Mock(spec=httpx.Response)
# Use 4xx to avoid retry logic (5xx triggers retries)
mock_response.status_code = 400
mock_response.json = Mock(side_effect=ValueError("Invalid JSON"))
with patch.object(client._client, 'request', return_value=mock_response):
# Should handle the error and raise APIError with status code
with pytest.raises(APIError) as exc_info:
client._send_request("GET", "/api/test")
assert exc_info.value.status_code == 400
assert "HTTP 400" in exc_info.value.message
def test_send_request_retry_on_server_error(self, api_key, mock_response):
"""Test that request is retried on 5xx errors."""
client = FastGPTClient(api_key, max_retries=2, retry_delay=0.01)
error_response = Mock(spec=httpx.Response)
error_response.status_code = 503
with patch.object(client._client, 'request', side_effect=[error_response, mock_response]):
response = client._send_request("GET", "/api/test")
assert response.status_code == 200
def test_send_request_validation_of_json_params(self, api_key, mock_response):
"""Test that JSON parameters are validated."""
client = FastGPTClient(api_key)
with patch.object(client._client, 'request', return_value=mock_response):
with pytest.raises(ValidationError):
client._send_request(
"POST",
"/api/test",
json={"query": ""} # Empty string should fail validation
)
def test_send_request_validation_of_query_params(self, api_key, mock_response):
"""Test that query parameters are validated."""
client = FastGPTClient(api_key)
with patch.object(client._client, 'request', return_value=mock_response):
with pytest.raises(ValidationError):
client._send_request(
"GET",
"/api/test",
params={"chatId": " "} # Whitespace-only should fail
)
class TestFastGPTClientHandleErrorResponse:
"""Test suite for FastGPTClient._handle_error_response method."""
def test_handle_success_response(self, api_key, mock_response):
"""Test handling of successful response (no error)."""
client = FastGPTClient(api_key)
mock_response.status_code = 200
# Should not raise any exception
client._handle_error_response(mock_response)
def test_handle_401_error(self, api_key, error_responses):
"""Test handling of 401 error."""
client = FastGPTClient(api_key)
mock_response = Mock(spec=httpx.Response)
mock_response.status_code = 401
mock_response.json = Mock(return_value=error_responses['authentication_error'])
with pytest.raises(AuthenticationError) as exc_info:
client._handle_error_response(mock_response)
assert exc_info.value.status_code == 401
assert exc_info.value.message == "Invalid API key"
def test_handle_429_error_without_retry_after(self, api_key, error_responses):
"""Test handling of 429 error without Retry-After header."""
client = FastGPTClient(api_key)
mock_response = Mock(spec=httpx.Response)
mock_response.status_code = 429
mock_response.headers = {}
mock_response.json = Mock(return_value=error_responses['rate_limit_error'])
with pytest.raises(RateLimitError) as exc_info:
client._handle_error_response(mock_response)
assert exc_info.value.status_code == 429
assert exc_info.value.retry_after is None
def test_handle_422_error(self, api_key, error_responses):
"""Test handling of 422 validation error."""
client = FastGPTClient(api_key)
mock_response = Mock(spec=httpx.Response)
mock_response.status_code = 422
mock_response.json = Mock(return_value=error_responses['validation_error'])
with pytest.raises(ValidationError) as exc_info:
client._handle_error_response(mock_response)
assert exc_info.value.status_code == 422
def test_handle_500_error(self, api_key):
"""Test handling of 500 server error."""
client = FastGPTClient(api_key)
mock_response = Mock(spec=httpx.Response)
mock_response.status_code = 500
mock_response.json = Mock(return_value={"message": "Internal error"})
with pytest.raises(APIError) as exc_info:
client._handle_error_response(mock_response)
assert exc_info.value.status_code == 500
def test_handle_error_without_message(self, api_key):
"""Test handling of error response without message field."""
client = FastGPTClient(api_key)
mock_response = Mock(spec=httpx.Response)
mock_response.status_code = 400
mock_response.json = Mock(return_value={})
with pytest.raises(APIError) as exc_info:
client._handle_error_response(mock_response)
assert "HTTP 400" in exc_info.value.message
def test_handle_error_with_json_parse_error(self, api_key):
"""Test handling of error response that can't be parsed as JSON."""
client = FastGPTClient(api_key)
mock_response = Mock(spec=httpx.Response)
mock_response.status_code = 503
mock_response.json = Mock(side_effect=ValueError("Invalid JSON"))
with pytest.raises(APIError) as exc_info:
client._handle_error_response(mock_response)
assert "HTTP 503" in exc_info.value.message

357
tests/test_exceptions.py Normal file
View File

@@ -0,0 +1,357 @@
"""Tests for FastGPT exception classes."""
import pytest
from fastgpt_client.exceptions import (
FastGPTError,
APIError,
AuthenticationError,
RateLimitError,
ValidationError,
StreamParseError,
)
class TestFastGPTError:
"""Test suite for FastGPTError base exception."""
def test_init_with_message_only(self):
"""Test initialization with message only."""
error = FastGPTError("Test error message")
assert error.message == "Test error message"
assert error.status_code is None
assert error.response_data == {}
def test_init_with_message_and_status_code(self):
"""Test initialization with message and status code."""
error = FastGPTError("Test error", status_code=404)
assert error.message == "Test error"
assert error.status_code == 404
assert error.response_data == {}
def test_init_with_all_parameters(self):
"""Test initialization with all parameters."""
response_data = {"code": "test_error", "details": "Something went wrong"}
error = FastGPTError(
"Test error",
status_code=500,
response_data=response_data
)
assert error.message == "Test error"
assert error.status_code == 500
assert error.response_data == response_data
def test_str_representation(self):
"""Test string representation of exception."""
error = FastGPTError("Test error message")
assert str(error) == "Test error message"
def test_inheritance(self):
"""Test that FastGPTError inherits from Exception."""
error = FastGPTError("Test")
assert isinstance(error, Exception)
assert isinstance(error, FastGPTError)
class TestAPIError:
"""Test suite for APIError exception."""
def test_api_error_inheritance(self):
"""Test that APIError inherits from FastGPTError."""
error = APIError("API error occurred")
assert isinstance(error, FastGPTError)
assert isinstance(error, APIError)
assert isinstance(error, Exception)
def test_api_error_basic(self):
"""Test basic APIError creation."""
error = APIError("API failed", status_code=500)
assert error.message == "API failed"
assert error.status_code == 500
def test_api_error_with_response_data(self):
"""Test APIError with response data."""
response_data = {"error": "Internal server error"}
error = APIError("Server error", status_code=500, response_data=response_data)
assert error.response_data == response_data
class TestAuthenticationError:
"""Test suite for AuthenticationError exception."""
def test_auth_error_inheritance(self):
"""Test that AuthenticationError inherits from FastGPTError."""
error = AuthenticationError("Invalid credentials")
assert isinstance(error, FastGPTError)
assert isinstance(error, AuthenticationError)
def test_auth_error_default(self):
"""Test AuthenticationError with default parameters."""
error = AuthenticationError("Invalid API key")
assert error.message == "Invalid API key"
assert error.status_code is None
assert error.response_data == {}
def test_auth_error_with_status_code(self):
"""Test AuthenticationError with status code."""
error = AuthenticationError(
"Authentication failed",
status_code=401
)
assert error.status_code == 401
def test_auth_error_full(self):
"""Test AuthenticationError with all parameters."""
response_data = {
"code": "invalid_api_key",
"message": "The provided API key is invalid"
}
error = AuthenticationError(
"Auth failed",
status_code=401,
response_data=response_data
)
assert error.message == "Auth failed"
assert error.status_code == 401
assert error.response_data == response_data
class TestRateLimitError:
"""Test suite for RateLimitError exception."""
def test_rate_limit_error_inheritance(self):
"""Test that RateLimitError inherits from FastGPTError."""
error = RateLimitError("Rate limit exceeded")
assert isinstance(error, FastGPTError)
assert isinstance(error, RateLimitError)
def test_rate_limit_error_basic(self):
"""Test RateLimitError with basic parameters."""
error = RateLimitError("Too many requests")
assert error.message == "Too many requests"
assert error.retry_after is None
assert error.status_code is None
def test_rate_limit_error_with_retry_after(self):
"""Test RateLimitError with retry_after parameter."""
error = RateLimitError(
"Rate limit exceeded",
retry_after="60"
)
assert error.message == "Rate limit exceeded"
assert error.retry_after == "60"
def test_rate_limit_error_full(self):
"""Test RateLimitError with all parameters."""
response_data = {
"code": "rate_limit_exceeded",
"limit": 100,
"remaining": 0
}
error = RateLimitError(
"Rate limit hit",
retry_after="120",
status_code=429,
response_data=response_data
)
assert error.message == "Rate limit hit"
assert error.retry_after == "120"
assert error.status_code == 429
assert error.response_data == response_data
def test_rate_limit_error_retry_after_integer(self):
"""Test RateLimitError with integer retry_after."""
error = RateLimitError(
"Rate limited",
retry_after=30
)
assert error.retry_after == 30
class TestValidationError:
"""Test suite for ValidationError exception."""
def test_validation_error_inheritance(self):
"""Test that ValidationError inherits from FastGPTError."""
error = ValidationError("Validation failed")
assert isinstance(error, FastGPTError)
assert isinstance(error, ValidationError)
def test_validation_error_basic(self):
"""Test ValidationError with basic parameters."""
error = ValidationError("Invalid parameter")
assert error.message == "Invalid parameter"
def test_validation_error_with_status(self):
"""Test ValidationError with status code."""
error = ValidationError(
"Invalid input",
status_code=422
)
assert error.status_code == 422
def test_validation_error_with_response_data(self):
"""Test ValidationError with response data."""
response_data = {
"code": "validation_error",
"field": "email",
"reason": "Invalid format"
}
error = ValidationError(
"Validation failed",
status_code=422,
response_data=response_data
)
assert error.response_data == response_data
class TestStreamParseError:
"""Test suite for StreamParseError exception."""
def test_stream_parse_error_inheritance(self):
"""Test that StreamParseError inherits from FastGPTError."""
error = StreamParseError("Stream parsing failed")
assert isinstance(error, FastGPTError)
assert isinstance(error, StreamParseError)
def test_stream_parse_error_basic(self):
"""Test StreamParseError with basic parameters."""
error = StreamParseError("Invalid stream format")
assert error.message == "Invalid stream format"
def test_stream_parse_error_with_status(self):
"""Test StreamParseError with status code."""
error = StreamParseError(
"Parse error",
status_code=500
)
assert error.status_code == 500
def test_stream_parse_error_with_response_data(self):
"""Test StreamParseError with response data."""
response_data = {
"error": "stream_error",
"details": "Unexpected EOF"
}
error = StreamParseError(
"Failed to parse stream",
status_code=502,
response_data=response_data
)
assert error.response_data == response_data
class TestExceptionUsagePatterns:
"""Test suite for common exception usage patterns."""
def test_catching_base_exception(self):
"""Test catching exceptions using base FastGPTError."""
with pytest.raises(FastGPTError) as exc_info:
raise APIError("API error")
assert isinstance(exc_info.value, FastGPTError)
assert str(exc_info.value) == "API error"
def test_catching_specific_exception(self):
"""Test catching specific exception types."""
with pytest.raises(AuthenticationError) as exc_info:
raise AuthenticationError("Auth failed", status_code=401)
assert exc_info.value.status_code == 401
def test_exception_chain(self):
"""Test exception chaining."""
try:
try:
raise ValueError("Original error")
except ValueError as e:
raise APIError("Wrapped error") from e
except APIError as caught:
assert caught.__cause__ is not None
assert isinstance(caught.__cause__, ValueError)
def test_raising_from_http_response(self):
"""Test raising exception from HTTP response data."""
response_data = {
"code": "invalid_request",
"message": "The request was invalid"
}
with pytest.raises(APIError) as exc_info:
raise APIError(
"Invalid request",
status_code=400,
response_data=response_data
)
assert exc_info.value.response_data == response_data
def test_exception_with_none_response_data(self):
"""Test exception when response_data is None."""
error = APIError("Error", status_code=500, response_data=None)
# None should default to empty dict
assert error.response_data == {}
def test_multiple_exception_types(self):
"""Test handling multiple exception types."""
def raise_specific_error(status_code):
if status_code == 401:
raise AuthenticationError("Unauthorized", status_code=status_code)
elif status_code == 429:
raise RateLimitError("Rate limited", status_code=status_code)
elif status_code == 422:
raise ValidationError("Invalid data", status_code=status_code)
else:
raise APIError("Generic error", status_code=status_code)
# Test each type
with pytest.raises(AuthenticationError):
raise_specific_error(401)
with pytest.raises(RateLimitError):
raise_specific_error(429)
with pytest.raises(ValidationError):
raise_specific_error(422)
with pytest.raises(APIError):
raise_specific_error(500)
def test_exception_message_formatting(self):
"""Test that exception messages are properly formatted."""
error1 = FastGPTError("Simple error")
assert str(error1) == "Simple error"
error2 = FastGPTError("Error with status", status_code=404)
assert str(error2) == "Error with status"
# Message should be accessible regardless of other parameters
error3 = APIError("Complex error", status_code=500, response_data={"key": "value"})
assert error3.message == "Complex error"
assert str(error3) == "Complex error"