Major changes across backend, frontend, infrastructure: - Provider system with model selection (Google, DeepL, OpenAI, Ollama, Google Cloud) - Admin panel: user management, pricing, settings - Glossary system with CSV import/export - Subscription and tier quota management - Security hardening (rate limiting, API key auth, path traversal fixes) - Docker compose for dev, prod, and IONOS deployment - Alembic migrations for new tables - Frontend: dashboard, pricing page, landing page, i18n (en/fr) - Test suite and verification scripts Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
729 lines
25 KiB
Python
729 lines
25 KiB
Python
"""
|
|
Tests for the OpenAITranslationProvider.
|
|
"""
|
|
|
|
import pytest
|
|
from unittest.mock import patch, MagicMock
|
|
from requests.exceptions import Timeout, ConnectionError as RequestsConnectionError
|
|
|
|
from services.providers.openai_provider import (
|
|
OpenAITranslationProvider,
|
|
OpenAIProviderError,
|
|
get_openai_provider,
|
|
register_openai_provider,
|
|
reset_openai_provider,
|
|
_build_system_prompt,
|
|
_get_language_name,
|
|
OPENAI_RATE_LIMITED,
|
|
OPENAI_INVALID_KEY,
|
|
OPENAI_QUOTA_EXCEEDED,
|
|
OPENAI_TIMEOUT,
|
|
OPENAI_SERVICE_ERROR,
|
|
OPENAI_CONTEXT_TOO_LONG,
|
|
)
|
|
from services.providers.schemas import TranslationRequest, TranslationResponse
|
|
|
|
|
|
class TestOpenAIProviderError:
|
|
"""Tests for OpenAIProviderError exception."""
|
|
|
|
def test_error_creation(self):
|
|
"""Test error creation with all fields."""
|
|
error = OpenAIProviderError(
|
|
code=OPENAI_RATE_LIMITED,
|
|
message="Rate limited",
|
|
details={"retry_after": 20},
|
|
)
|
|
|
|
assert error.code == OPENAI_RATE_LIMITED
|
|
assert error.message == "Rate limited"
|
|
assert error.details == {"retry_after": 20}
|
|
|
|
def test_error_to_dict(self):
|
|
"""Test error serialization."""
|
|
error = OpenAIProviderError(
|
|
code=OPENAI_INVALID_KEY,
|
|
message="Invalid key",
|
|
details={"provider": "openai"},
|
|
)
|
|
|
|
result = error.to_dict()
|
|
|
|
assert result["error"] == OPENAI_INVALID_KEY
|
|
assert result["message"] == "Invalid key"
|
|
assert result["details"]["provider"] == "openai"
|
|
|
|
def test_error_to_dict_no_details(self):
|
|
"""Test error serialization without details."""
|
|
error = OpenAIProviderError(
|
|
code=OPENAI_SERVICE_ERROR,
|
|
message="Service error",
|
|
)
|
|
|
|
result = error.to_dict()
|
|
|
|
assert result["error"] == OPENAI_SERVICE_ERROR
|
|
assert result["message"] == "Service error"
|
|
assert "details" not in result
|
|
|
|
|
|
class TestHelperFunctions:
|
|
"""Tests for helper functions."""
|
|
|
|
def test_get_language_name_common(self):
|
|
"""Test language name lookup for common languages."""
|
|
assert _get_language_name("en") == "English"
|
|
assert _get_language_name("fr") == "French"
|
|
assert _get_language_name("es") == "Spanish"
|
|
assert _get_language_name("de") == "German"
|
|
assert _get_language_name("zh") == "Chinese"
|
|
assert _get_language_name("ja") == "Japanese"
|
|
|
|
def test_get_language_name_with_variant(self):
|
|
"""Test language name lookup with variant codes."""
|
|
assert _get_language_name("en-US") == "English"
|
|
assert _get_language_name("pt-BR") == "Portuguese"
|
|
|
|
def test_get_language_name_unknown(self):
|
|
"""Test language name lookup for unknown codes."""
|
|
assert _get_language_name("xx") == "xx"
|
|
|
|
def test_build_system_prompt_default(self):
|
|
"""Test default system prompt generation."""
|
|
prompt = _build_system_prompt("English", "French")
|
|
|
|
assert "English" in prompt
|
|
assert "French" in prompt
|
|
assert "translator" in prompt.lower()
|
|
|
|
def test_build_system_prompt_custom(self):
|
|
"""Test custom system prompt."""
|
|
custom = "Translate this text formally for business context."
|
|
prompt = _build_system_prompt("English", "French", custom)
|
|
|
|
assert prompt == custom
|
|
|
|
|
|
class TestOpenAITranslationProvider:
|
|
"""Tests for OpenAITranslationProvider."""
|
|
|
|
@pytest.fixture
|
|
def provider(self):
|
|
"""Create an OpenAI provider instance."""
|
|
return OpenAITranslationProvider(
|
|
api_key="test-api-key",
|
|
model="gpt-4o-mini",
|
|
timeout=60,
|
|
max_retries=0,
|
|
)
|
|
|
|
@pytest.fixture
|
|
def provider_with_retries(self):
|
|
"""Create an OpenAI provider with retries."""
|
|
return OpenAITranslationProvider(
|
|
api_key="test-api-key",
|
|
model="gpt-4o-mini",
|
|
timeout=60,
|
|
max_retries=2,
|
|
retry_delay=0.01,
|
|
)
|
|
|
|
def test_init(self, provider):
|
|
"""Test provider initialization."""
|
|
assert provider._api_key == "test-api-key"
|
|
assert provider._model == "gpt-4o-mini"
|
|
assert provider._base_url == "https://api.openai.com/v1"
|
|
assert provider._timeout == 60
|
|
assert provider._provider_name == "openai"
|
|
|
|
def test_init_with_custom_base_url(self):
|
|
"""Test provider initialization with custom base URL."""
|
|
provider = OpenAITranslationProvider(
|
|
api_key="test-api-key",
|
|
model="gpt-4",
|
|
base_url="https://custom.openai.com/v1",
|
|
)
|
|
assert provider._base_url == "https://custom.openai.com/v1"
|
|
|
|
def test_get_name(self, provider):
|
|
"""Test provider name."""
|
|
assert provider.get_name() == "openai"
|
|
|
|
def test_translate_text_empty(self, provider):
|
|
"""Test translating empty text."""
|
|
request = TranslationRequest(text="", target_language="fr")
|
|
response = provider.translate_text(request)
|
|
|
|
assert response.translated_text == ""
|
|
assert response.provider_name == "openai"
|
|
assert response.from_cache is False
|
|
|
|
def test_translate_text_whitespace(self, provider):
|
|
"""Test translating whitespace-only text."""
|
|
request = TranslationRequest(text=" ", target_language="fr")
|
|
response = provider.translate_text(request)
|
|
|
|
assert response.translated_text == " "
|
|
|
|
@patch("requests.post")
|
|
def test_translate_text_success(self, mock_post, provider):
|
|
"""Test successful translation."""
|
|
mock_response = MagicMock()
|
|
mock_response.status_code = 200
|
|
mock_response.json.return_value = {
|
|
"id": "chatcmpl-123",
|
|
"choices": [{"message": {"content": "Bonjour"}}],
|
|
"usage": {"total_tokens": 10},
|
|
}
|
|
mock_post.return_value = mock_response
|
|
|
|
request = TranslationRequest(text="Hello", target_language="fr")
|
|
response = provider.translate_text(request)
|
|
|
|
assert response.translated_text == "Bonjour"
|
|
assert response.provider_name == "openai"
|
|
assert response.from_cache is False
|
|
|
|
@patch("requests.post")
|
|
def test_translate_text_with_custom_prompt(self, mock_post, provider):
|
|
"""Test translation with custom system prompt."""
|
|
mock_response = MagicMock()
|
|
mock_response.status_code = 200
|
|
mock_response.json.return_value = {
|
|
"id": "chatcmpl-123",
|
|
"choices": [{"message": {"content": "Bonjour (formal)"}}],
|
|
"usage": {"total_tokens": 15},
|
|
}
|
|
mock_post.return_value = mock_response
|
|
|
|
request = TranslationRequest(
|
|
text="Hello",
|
|
target_language="fr",
|
|
metadata={"custom_prompt": "Translate formally for business"},
|
|
)
|
|
response = provider.translate_text(request)
|
|
|
|
assert response.translated_text == "Bonjour (formal)"
|
|
# Verify custom prompt was used in API call
|
|
call_args = mock_post.call_args
|
|
assert (
|
|
"Translate formally for business"
|
|
in call_args[1]["json"]["messages"][0]["content"]
|
|
)
|
|
|
|
def test_translate_batch_empty(self, provider):
|
|
"""Test batch translation with empty list."""
|
|
responses = provider.translate_batch([])
|
|
assert responses == []
|
|
|
|
@patch.object(OpenAITranslationProvider, "translate_text")
|
|
def test_translate_batch(self, mock_translate, provider):
|
|
"""Test batch translation."""
|
|
mock_translate.side_effect = [
|
|
TranslationResponse(translated_text="Bonjour", provider_name="openai"),
|
|
TranslationResponse(translated_text="Au revoir", provider_name="openai"),
|
|
]
|
|
|
|
requests = [
|
|
TranslationRequest(text="Hello", target_language="fr"),
|
|
TranslationRequest(text="Goodbye", target_language="fr"),
|
|
]
|
|
responses = provider.translate_batch(requests)
|
|
|
|
assert len(responses) == 2
|
|
assert responses[0].translated_text == "Bonjour"
|
|
assert responses[1].translated_text == "Au revoir"
|
|
|
|
|
|
class TestOpenAIErrorHandling:
|
|
"""Tests for OpenAI error handling."""
|
|
|
|
@pytest.fixture
|
|
def provider(self):
|
|
return OpenAITranslationProvider(
|
|
api_key="test-key",
|
|
model="gpt-4o-mini",
|
|
timeout=60,
|
|
max_retries=0,
|
|
)
|
|
|
|
@patch("requests.post")
|
|
def test_rate_limit_error(self, mock_post, provider):
|
|
"""Test rate limit error handling."""
|
|
mock_response = MagicMock()
|
|
mock_response.status_code = 429
|
|
mock_response.json.return_value = {
|
|
"error": {"code": "rate_limit_exceeded", "message": "Rate limit exceeded"}
|
|
}
|
|
mock_response.headers = {"retry-after": "20"}
|
|
mock_post.return_value = mock_response
|
|
|
|
request = TranslationRequest(text="Hello", target_language="fr")
|
|
response = provider.translate_text(request)
|
|
|
|
assert response.error_code == OPENAI_RATE_LIMITED
|
|
assert "20" in response.error or "Limite" in response.error
|
|
|
|
@patch("requests.post")
|
|
def test_invalid_api_key_error(self, mock_post, provider):
|
|
"""Test invalid API key error handling."""
|
|
mock_response = MagicMock()
|
|
mock_response.status_code = 401
|
|
mock_response.json.return_value = {
|
|
"error": {"code": "invalid_api_key", "message": "Invalid API key"}
|
|
}
|
|
mock_post.return_value = mock_response
|
|
|
|
request = TranslationRequest(text="Hello", target_language="fr")
|
|
response = provider.translate_text(request)
|
|
|
|
assert response.error_code == OPENAI_INVALID_KEY
|
|
assert "invalide" in response.error.lower() or "Invalid" in response.error
|
|
|
|
@patch("requests.post")
|
|
def test_quota_exceeded_error(self, mock_post, provider):
|
|
"""Test quota exceeded error handling."""
|
|
mock_response = MagicMock()
|
|
mock_response.status_code = 429
|
|
mock_response.json.return_value = {
|
|
"error": {
|
|
"code": "insufficient_quota",
|
|
"message": "You exceeded your current quota",
|
|
}
|
|
}
|
|
mock_post.return_value = mock_response
|
|
|
|
request = TranslationRequest(text="Hello", target_language="fr")
|
|
response = provider.translate_text(request)
|
|
|
|
assert response.error_code == OPENAI_QUOTA_EXCEEDED
|
|
assert "quota" in response.error.lower() or "Quota" in response.error
|
|
|
|
@patch("requests.post")
|
|
def test_context_too_long_error(self, mock_post, provider):
|
|
"""Test context length exceeded error."""
|
|
mock_response = MagicMock()
|
|
mock_response.status_code = 400
|
|
mock_response.json.return_value = {
|
|
"error": {
|
|
"code": "context_length_exceeded",
|
|
"message": "This model's maximum context length is 4097 tokens",
|
|
}
|
|
}
|
|
mock_post.return_value = mock_response
|
|
|
|
request = TranslationRequest(text="Hello", target_language="fr")
|
|
response = provider.translate_text(request)
|
|
|
|
assert response.error_code == OPENAI_CONTEXT_TOO_LONG
|
|
assert "trop long" in response.error.lower() or "long" in response.error.lower()
|
|
|
|
@patch("requests.post")
|
|
def test_service_error(self, mock_post, provider):
|
|
"""Test OpenAI service error (500)."""
|
|
mock_response = MagicMock()
|
|
mock_response.status_code = 500
|
|
mock_response.json.return_value = {
|
|
"error": {"code": "server_error", "message": "The server had an error"}
|
|
}
|
|
mock_post.return_value = mock_response
|
|
|
|
request = TranslationRequest(text="Hello", target_language="fr")
|
|
response = provider.translate_text(request)
|
|
|
|
assert response.error_code == OPENAI_SERVICE_ERROR
|
|
assert response.error is not None
|
|
|
|
@patch("requests.post")
|
|
def test_timeout_error(self, mock_post, provider):
|
|
"""Test timeout error handling."""
|
|
mock_post.side_effect = Timeout("Request timed out")
|
|
|
|
request = TranslationRequest(text="Hello", target_language="fr")
|
|
response = provider.translate_text(request)
|
|
|
|
assert response.error_code == OPENAI_TIMEOUT
|
|
assert "délai" in response.error.lower() or "timeout" in response.error.lower()
|
|
|
|
@patch("requests.post")
|
|
def test_connection_error(self, mock_post, provider):
|
|
"""Test connection error handling."""
|
|
mock_post.side_effect = RequestsConnectionError("Connection failed")
|
|
|
|
request = TranslationRequest(text="Hello", target_language="fr")
|
|
response = provider.translate_text(request)
|
|
|
|
# Connection errors are mapped to service error
|
|
assert response.error is not None
|
|
assert response.error_code is not None
|
|
|
|
|
|
class TestOpenAIRetryLogic:
|
|
"""Tests for retry logic."""
|
|
|
|
@pytest.fixture
|
|
def provider_with_retries(self):
|
|
return OpenAITranslationProvider(
|
|
api_key="test-key",
|
|
model="gpt-4o-mini",
|
|
timeout=60,
|
|
max_retries=2,
|
|
retry_delay=0.01, # Fast for testing
|
|
)
|
|
|
|
@patch("requests.post")
|
|
def test_retry_on_rate_limit(self, mock_post, provider_with_retries):
|
|
"""Test retry on rate limit error."""
|
|
# First call fails with rate limit, second succeeds
|
|
error_response = MagicMock()
|
|
error_response.status_code = 429
|
|
error_response.json.return_value = {
|
|
"error": {"code": "rate_limit_exceeded", "message": "Rate limited"}
|
|
}
|
|
error_response.headers = {}
|
|
|
|
success_response = MagicMock()
|
|
success_response.status_code = 200
|
|
success_response.json.return_value = {
|
|
"id": "chatcmpl-123",
|
|
"choices": [{"message": {"content": "Bonjour"}}],
|
|
"usage": {"total_tokens": 10},
|
|
}
|
|
|
|
mock_post.side_effect = [error_response, success_response]
|
|
|
|
request = TranslationRequest(text="Hello", target_language="fr")
|
|
response = provider_with_retries.translate_text(request)
|
|
|
|
assert response.translated_text == "Bonjour"
|
|
assert response.error is None
|
|
assert mock_post.call_count == 2
|
|
|
|
@patch("requests.post")
|
|
def test_retry_exhausted(self, mock_post, provider_with_retries):
|
|
"""Test that retry eventually gives up."""
|
|
error_response = MagicMock()
|
|
error_response.status_code = 429
|
|
error_response.json.return_value = {
|
|
"error": {"code": "rate_limit_exceeded", "message": "Rate limited"}
|
|
}
|
|
error_response.headers = {}
|
|
|
|
mock_post.return_value = error_response
|
|
|
|
request = TranslationRequest(text="Hello", target_language="fr")
|
|
response = provider_with_retries.translate_text(request)
|
|
|
|
assert response.error is not None
|
|
assert response.error_code == OPENAI_RATE_LIMITED
|
|
assert mock_post.call_count == 3 # Initial + 2 retries
|
|
|
|
|
|
class TestOpenAIHealthCheck:
|
|
"""Tests for health check functionality."""
|
|
|
|
@pytest.fixture
|
|
def provider(self):
|
|
return OpenAITranslationProvider(
|
|
api_key="test-key",
|
|
model="gpt-4o-mini",
|
|
timeout=60,
|
|
)
|
|
|
|
@patch("requests.get")
|
|
def test_is_available_success(self, mock_get, provider):
|
|
"""Test is_available when API is reachable."""
|
|
mock_response = MagicMock()
|
|
mock_response.status_code = 200
|
|
mock_response.json.return_value = {"data": [{"id": "gpt-4"}]}
|
|
mock_get.return_value = mock_response
|
|
|
|
assert provider.is_available() is True
|
|
|
|
@patch("requests.get")
|
|
def test_is_available_failure(self, mock_get, provider):
|
|
"""Test is_available when API is unreachable."""
|
|
mock_get.side_effect = RequestsConnectionError("Connection failed")
|
|
|
|
assert provider.is_available() is False
|
|
|
|
@patch("requests.get")
|
|
def test_health_check_success(self, mock_get, provider):
|
|
"""Test health check success."""
|
|
mock_response = MagicMock()
|
|
mock_response.status_code = 200
|
|
mock_response.json.return_value = {"data": [{"id": "gpt-4o-mini"}]}
|
|
mock_get.return_value = mock_response
|
|
|
|
status = provider.health_check()
|
|
|
|
assert status.name == "openai"
|
|
assert status.available is True
|
|
assert status.latency_ms is not None
|
|
|
|
@patch("requests.get")
|
|
def test_health_check_failure(self, mock_get, provider):
|
|
"""Test health check failure."""
|
|
mock_get.side_effect = RequestsConnectionError("Connection failed")
|
|
|
|
status = provider.health_check()
|
|
|
|
assert status.name == "openai"
|
|
assert status.available is False
|
|
assert status.error is not None
|
|
|
|
@patch("requests.get")
|
|
def test_health_check_caching(self, mock_get, provider):
|
|
"""Test that health check results are cached."""
|
|
mock_response = MagicMock()
|
|
mock_response.status_code = 200
|
|
mock_response.json.return_value = {"data": []}
|
|
mock_get.return_value = mock_response
|
|
|
|
# First call should hit the API
|
|
provider.health_check()
|
|
assert mock_get.call_count == 1
|
|
|
|
# Second call should use cache
|
|
provider.health_check()
|
|
assert mock_get.call_count == 1 # No additional call
|
|
|
|
|
|
class TestOpenAIRegistryIntegration:
|
|
"""Tests for registry integration."""
|
|
|
|
def test_register_openai_provider(self):
|
|
"""Test provider registration."""
|
|
from services.providers.registry import registry
|
|
|
|
registry.unregister("openai")
|
|
|
|
with patch(
|
|
"services.providers.openai_provider.get_openai_provider"
|
|
) as mock_get:
|
|
mock_provider = MagicMock()
|
|
mock_get.return_value = mock_provider
|
|
|
|
result = register_openai_provider()
|
|
|
|
assert result == mock_provider
|
|
assert "openai" in registry
|
|
registry.unregister("openai")
|
|
|
|
def test_get_openai_provider_singleton(self):
|
|
"""Test that get_openai_provider returns a singleton."""
|
|
import services.providers.openai_provider as openai_module
|
|
|
|
# Reset singleton
|
|
openai_module._provider_instance = None
|
|
|
|
# Create first provider directly without mocking
|
|
# (singleton pattern is simple enough to test directly)
|
|
provider1 = OpenAITranslationProvider(
|
|
api_key="test-key",
|
|
model="gpt-4o-mini",
|
|
)
|
|
|
|
# Manually set the singleton
|
|
openai_module._provider_instance = provider1
|
|
|
|
# Second call should return same instance
|
|
provider2 = get_openai_provider()
|
|
|
|
assert provider1 is provider2
|
|
|
|
# Reset singleton after test
|
|
openai_module._provider_instance = None
|
|
|
|
def test_reset_openai_provider(self):
|
|
"""Test reset_openai_provider clears the singleton."""
|
|
import services.providers.openai_provider as openai_module
|
|
|
|
# Set up a singleton
|
|
openai_module._provider_instance = OpenAITranslationProvider(
|
|
api_key="test-key",
|
|
model="gpt-4o-mini",
|
|
)
|
|
|
|
# Verify it's set
|
|
assert openai_module._provider_instance is not None
|
|
|
|
# Reset
|
|
reset_openai_provider()
|
|
|
|
# Verify it's cleared
|
|
assert openai_module._provider_instance is None
|
|
|
|
|
|
class TestOpenAIValidation:
|
|
"""Tests for input validation."""
|
|
|
|
def test_empty_api_key_raises_error(self):
|
|
"""Test that empty API key raises ValueError."""
|
|
with pytest.raises(ValueError, match="API key cannot be empty"):
|
|
OpenAITranslationProvider(api_key="")
|
|
|
|
def test_whitespace_api_key_raises_error(self):
|
|
"""Test that whitespace-only API key raises ValueError."""
|
|
with pytest.raises(ValueError, match="API key cannot be empty"):
|
|
OpenAITranslationProvider(api_key=" ")
|
|
|
|
def test_text_too_long_preemptive_check(self):
|
|
"""Test preemptive check for text exceeding token limit."""
|
|
provider = OpenAITranslationProvider(
|
|
api_key="test-key",
|
|
model="gpt-4o-mini",
|
|
max_retries=0,
|
|
)
|
|
|
|
# Create text longer than 16000 chars (~4000 tokens)
|
|
long_text = "x" * 17000
|
|
request = TranslationRequest(text=long_text, target_language="fr")
|
|
response = provider.translate_text(request)
|
|
|
|
assert response.error_code == OPENAI_CONTEXT_TOO_LONG
|
|
assert response.error is not None
|
|
assert "trop long" in response.error.lower()
|
|
|
|
|
|
class TestOpenAIMalformedResponses:
|
|
"""Tests for malformed API response handling."""
|
|
|
|
@pytest.fixture
|
|
def provider(self):
|
|
return OpenAITranslationProvider(
|
|
api_key="test-key",
|
|
model="gpt-4o-mini",
|
|
timeout=60,
|
|
max_retries=0,
|
|
)
|
|
|
|
@patch("requests.post")
|
|
def test_empty_choices_array(self, mock_post, provider):
|
|
"""Test handling of empty choices array."""
|
|
mock_response = MagicMock()
|
|
mock_response.status_code = 200
|
|
mock_response.json.return_value = {
|
|
"id": "chatcmpl-123",
|
|
"choices": [],
|
|
"usage": {"total_tokens": 10},
|
|
}
|
|
mock_post.return_value = mock_response
|
|
|
|
request = TranslationRequest(text="Hello", target_language="fr")
|
|
response = provider.translate_text(request)
|
|
|
|
assert response.error_code == OPENAI_SERVICE_ERROR
|
|
assert "vide" in response.error.lower()
|
|
|
|
@patch("requests.post")
|
|
def test_missing_message_content(self, mock_post, provider):
|
|
"""Test handling of missing message content."""
|
|
mock_response = MagicMock()
|
|
mock_response.status_code = 200
|
|
mock_response.json.return_value = {
|
|
"id": "chatcmpl-123",
|
|
"choices": [{"message": {}}],
|
|
"usage": {"total_tokens": 10},
|
|
}
|
|
mock_post.return_value = mock_response
|
|
|
|
request = TranslationRequest(text="Hello", target_language="fr")
|
|
response = provider.translate_text(request)
|
|
|
|
assert response.error_code == OPENAI_SERVICE_ERROR
|
|
|
|
@patch("requests.post")
|
|
def test_missing_message_key(self, mock_post, provider):
|
|
"""Test handling of missing message key in choice."""
|
|
mock_response = MagicMock()
|
|
mock_response.status_code = 200
|
|
mock_response.json.return_value = {
|
|
"id": "chatcmpl-123",
|
|
"choices": [{"finish_reason": "stop"}],
|
|
"usage": {"total_tokens": 10},
|
|
}
|
|
mock_post.return_value = mock_response
|
|
|
|
request = TranslationRequest(text="Hello", target_language="fr")
|
|
response = provider.translate_text(request)
|
|
|
|
assert response.error_code == OPENAI_SERVICE_ERROR
|
|
|
|
@patch("requests.post")
|
|
def test_empty_content_string(self, mock_post, provider):
|
|
"""Test handling of empty content string."""
|
|
mock_response = MagicMock()
|
|
mock_response.status_code = 200
|
|
mock_response.json.return_value = {
|
|
"id": "chatcmpl-123",
|
|
"choices": [{"message": {"content": ""}}],
|
|
"usage": {"total_tokens": 10},
|
|
}
|
|
mock_post.return_value = mock_response
|
|
|
|
request = TranslationRequest(text="Hello", target_language="fr")
|
|
response = provider.translate_text(request)
|
|
|
|
assert response.error_code == OPENAI_SERVICE_ERROR
|
|
|
|
|
|
class TestOpenAIHealthCheckModelInfo:
|
|
"""Tests for health check model info."""
|
|
|
|
@pytest.fixture
|
|
def provider(self):
|
|
return OpenAITranslationProvider(
|
|
api_key="test-key",
|
|
model="gpt-4o-mini",
|
|
timeout=60,
|
|
health_check_timeout=5,
|
|
)
|
|
|
|
@patch("requests.get")
|
|
def test_health_check_includes_model(self, mock_get, provider):
|
|
"""Test that health check includes model info."""
|
|
mock_response = MagicMock()
|
|
mock_response.status_code = 200
|
|
mock_response.json.return_value = {
|
|
"data": [
|
|
{"id": "gpt-4o-mini"},
|
|
{"id": "gpt-4"},
|
|
]
|
|
}
|
|
mock_get.return_value = mock_response
|
|
|
|
status = provider.health_check()
|
|
|
|
assert status.model == "gpt-4o-mini"
|
|
assert status.model_available is True
|
|
|
|
@patch("requests.get")
|
|
def test_health_check_model_not_available(self, mock_get, provider):
|
|
"""Test health check when configured model not in list."""
|
|
mock_response = MagicMock()
|
|
mock_response.status_code = 200
|
|
mock_response.json.return_value = {
|
|
"data": [
|
|
{"id": "gpt-4"},
|
|
{"id": "gpt-3.5-turbo"},
|
|
]
|
|
}
|
|
mock_get.return_value = mock_response
|
|
|
|
status = provider.health_check()
|
|
|
|
assert status.model == "gpt-4o-mini"
|
|
assert status.model_available is False
|
|
|
|
@patch("requests.get")
|
|
def test_health_check_unavailable_includes_model(self, mock_get, provider):
|
|
"""Test that health check includes model even when unavailable."""
|
|
mock_get.side_effect = RequestsConnectionError("Connection failed")
|
|
|
|
status = provider.health_check()
|
|
|
|
assert status.available is False
|
|
assert status.model == "gpt-4o-mini"
|
|
assert status.model_available is False
|