backend/tests/test_libretranslate.py
Rama master · réplica de lectura del repositorio original.
"""Tests for translation.libretranslate — LibreTranslate translation backend."""
from unittest.mock import MagicMock, patch
import pytest
import requests
from translation.base import TranslationResult
def _make_backend(**config_overrides):
"""Create a LibreTranslateBackend instance without __init__ side effects."""
from translation.libretranslate import LibreTranslateBackend
backend = LibreTranslateBackend.__new__(LibreTranslateBackend)
backend.config = {
"url": "http://libretranslate:5000",
"api_key": "",
"request_timeout": "30",
**config_overrides,
}
return backend
class TestLibreTranslateAttributes:
"""Tests for class-level attributes and config."""
def test_name_and_display_name(self):
backend = _make_backend()
assert backend.name == "libretranslate"
assert backend.display_name == "LibreTranslate (Self-Hosted)"
def test_no_glossary_support(self):
backend = _make_backend()
assert backend.supports_glossary is False
def test_no_batch_support(self):
backend = _make_backend()
assert backend.supports_batch is False
assert backend.max_batch_size == 1
def test_config_fields_returned(self):
backend = _make_backend()
fields = backend.get_config_fields()
assert len(fields) == 3
keys = [f["key"] for f in fields]
assert "url" in keys
assert "api_key" in keys
assert "request_timeout" in keys
def test_get_usage_returns_empty_dict(self):
backend = _make_backend()
assert backend.get_usage() == {}
class TestTranslateBatch:
"""Tests for translate_batch method (line-by-line translation)."""
@patch("translation.libretranslate.requests.post")
def test_single_line_translation(self, mock_post):
backend = _make_backend()
mock_resp = MagicMock()
mock_resp.json.return_value = {"translatedText": "Hallo"}
mock_resp.raise_for_status = MagicMock()
mock_post.return_value = mock_resp
result = backend.translate_batch(["Hello"], "en", "de")
assert result.success is True
assert result.translated_lines == ["Hallo"]
assert result.backend_name == "libretranslate"
assert result.characters_used == 5
@patch("translation.libretranslate.requests.post")
def test_multiple_lines_translated_individually(self, mock_post):
backend = _make_backend()
responses = []
for text in ["Hallo", "Welt", "Test"]:
resp = MagicMock()
resp.json.return_value = {"translatedText": text}
resp.raise_for_status = MagicMock()
responses.append(resp)
mock_post.side_effect = responses
result = backend.translate_batch(["Hello", "World", "Test"], "en", "de")
assert result.success is True
assert result.translated_lines == ["Hallo", "Welt", "Test"]
assert mock_post.call_count == 3
assert result.characters_used == 14 # 5 + 5 + 4
@patch("translation.libretranslate.requests.post")
def test_api_key_included_in_payload(self, mock_post):
backend = _make_backend(api_key="my-secret-key")
mock_resp = MagicMock()
mock_resp.json.return_value = {"translatedText": "Hallo"}
mock_resp.raise_for_status = MagicMock()
mock_post.return_value = mock_resp
backend.translate_batch(["Hello"], "en", "de")
call_kwargs = mock_post.call_args[1]
payload = call_kwargs["json"]
assert payload["api_key"] == "my-secret-key"
@patch("translation.libretranslate.requests.post")
def test_no_api_key_not_in_payload(self, mock_post):
backend = _make_backend(api_key="")
mock_resp = MagicMock()
mock_resp.json.return_value = {"translatedText": "Hallo"}
mock_resp.raise_for_status = MagicMock()
mock_post.return_value = mock_resp
backend.translate_batch(["Hello"], "en", "de")
call_kwargs = mock_post.call_args[1]
payload = call_kwargs["json"]
assert "api_key" not in payload
@patch("translation.libretranslate.requests.post")
def test_correct_url_and_timeout(self, mock_post):
backend = _make_backend(url="http://my-libre:9000/", request_timeout="60")
mock_resp = MagicMock()
mock_resp.json.return_value = {"translatedText": "Hallo"}
mock_resp.raise_for_status = MagicMock()
mock_post.return_value = mock_resp
backend.translate_batch(["Hello"], "en", "de")
call_args = mock_post.call_args
assert call_args[0][0] == "http://my-libre:9000/translate"
assert call_args[1]["timeout"] == 60
@patch("translation.libretranslate.requests.post")
def test_trailing_slash_stripped_from_url(self, mock_post):
backend = _make_backend(url="http://libre:5000/")
mock_resp = MagicMock()
mock_resp.json.return_value = {"translatedText": "Hallo"}
mock_resp.raise_for_status = MagicMock()
mock_post.return_value = mock_resp
backend.translate_batch(["Hello"], "en", "de")
call_url = mock_post.call_args[0][0]
assert call_url == "http://libre:5000/translate"
@patch("translation.libretranslate.requests.post")
def test_missing_translated_text_uses_original(self, mock_post):
backend = _make_backend()
mock_resp = MagicMock()
mock_resp.json.return_value = {} # No "translatedText" key
mock_resp.raise_for_status = MagicMock()
mock_post.return_value = mock_resp
result = backend.translate_batch(["Hello"], "en", "de")
assert result.success is True
assert result.translated_lines == ["Hello"] # Falls back to original
@patch("translation.libretranslate.requests.post")
def test_http_error_returns_failure(self, mock_post):
backend = _make_backend()
mock_post.side_effect = requests.exceptions.HTTPError("500 Internal Server Error")
result = backend.translate_batch(["Hello"], "en", "de")
assert result.success is False
assert result.translated_lines == []
assert "500" in result.error
@patch("translation.libretranslate.requests.post")
def test_timeout_returns_failure(self, mock_post):
backend = _make_backend()
mock_post.side_effect = requests.exceptions.Timeout("Request timed out")
result = backend.translate_batch(["Hello"], "en", "de")
assert result.success is False
assert result.translated_lines == []
@patch("translation.libretranslate.requests.post")
def test_connection_error_returns_failure(self, mock_post):
backend = _make_backend()
mock_post.side_effect = requests.exceptions.ConnectionError("Connection refused")
result = backend.translate_batch(["Hello"], "en", "de")
assert result.success is False
assert "Connection refused" in result.error
class TestHealthCheck:
"""Tests for health_check method."""
@patch("translation.libretranslate.requests.get")
def test_healthy_with_language_count(self, mock_get):
backend = _make_backend()
mock_resp = MagicMock()
mock_resp.status_code = 200
mock_resp.json.return_value = [{"code": "en"}, {"code": "de"}, {"code": "fr"}]
mock_get.return_value = mock_resp
healthy, msg = backend.health_check()
assert healthy is True
assert "3 languages" in msg
@patch("translation.libretranslate.requests.get")
def test_unhealthy_on_non_200(self, mock_get):
backend = _make_backend()
mock_resp = MagicMock()
mock_resp.status_code = 503
mock_get.return_value = mock_resp
healthy, msg = backend.health_check()
assert healthy is False
assert "HTTP 503" in msg
@patch("translation.libretranslate.requests.get")
def test_unhealthy_on_connection_error(self, mock_get):
backend = _make_backend()
mock_get.side_effect = requests.exceptions.ConnectionError("refused")
healthy, msg = backend.health_check()
assert healthy is False
@patch("translation.libretranslate.requests.get")
def test_health_check_url_constructed_correctly(self, mock_get):
backend = _make_backend(url="http://my-libre:9000/")
mock_resp = MagicMock()
mock_resp.status_code = 200
mock_resp.json.return_value = []
mock_get.return_value = mock_resp
backend.health_check()
call_url = mock_get.call_args[0][0]
assert call_url == "http://my-libre:9000/languages"