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