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:
168
tests/conftest.py
Normal file
168
tests/conftest.py
Normal 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
180
tests/test_app_client.py
Normal 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
220
tests/test_base_client.py
Normal 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
626
tests/test_chat_client.py
Normal 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
387
tests/test_client.py
Normal 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
357
tests/test_exceptions.py
Normal 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"
|
||||||
Reference in New Issue
Block a user