from unittest.mock import MagicMock, patch import httpx import pytest from openai import RateLimitError import translate from translate import translate_feed, translate_html # ── Helpers ─────────────────────────────────────────────────────────────────── def _openai_response(text: str) -> MagicMock: m = MagicMock() m.choices = [MagicMock()] m.choices[0].message.content = text return m def _rate_limit_error() -> RateLimitError: req = httpx.Request("POST", "https://api.openai.com/v1/chat/completions") resp = httpx.Response(429, request=req) return RateLimitError("Rate limit exceeded", response=resp, body=None) # ── translate() ─────────────────────────────────────────────────────────────── def test_translate_empty_string_unchanged(db_conn): with patch("translate._client") as mock_client: result = translate.translate("", "sk", db_conn) assert result == "" mock_client.chat.completions.create.assert_not_called() def test_translate_whitespace_unchanged(db_conn): with patch("translate._client") as mock_client: result = translate.translate(" ", "sk", db_conn) assert result == " " mock_client.chat.completions.create.assert_not_called() def test_translate_calls_openai_on_cache_miss(db_conn): with patch("translate._client") as mock_client: mock_client.chat.completions.create.return_value = _openai_response("Ahoj") result = translate.translate("Hello", "sk", db_conn) assert result == "Ahoj" mock_client.chat.completions.create.assert_called_once() def test_translate_returns_cache_on_hit(db_conn): with patch("translate._client") as mock_client: mock_client.chat.completions.create.return_value = _openai_response("Ahoj") translate.translate("Hello", "sk", db_conn) result = translate.translate("Hello", "sk", db_conn) assert result == "Ahoj" assert mock_client.chat.completions.create.call_count == 1 def test_translate_on_error_returns_original(db_conn): with patch("translate._client") as mock_client: mock_client.chat.completions.create.side_effect = Exception("API down") result = translate.translate("Hello", "sk", db_conn) assert result == "Hello" def test_translate_error_not_cached(db_conn): with patch("translate._client") as mock_client: mock_client.chat.completions.create.side_effect = Exception("API down") translate.translate("Hello", "sk", db_conn) mock_client.chat.completions.create.side_effect = None mock_client.chat.completions.create.return_value = _openai_response("Ahoj") result = translate.translate("Hello", "sk", db_conn) assert result == "Ahoj" assert mock_client.chat.completions.create.call_count == 2 # ── _call_openai() retry / backoff ──────────────────────────────────────────── def test_call_openai_retries_on_rate_limit(db_conn): with patch("translate.time.sleep"), \ patch("translate._client") as mock_client: mock_client.chat.completions.create.side_effect = [ _rate_limit_error(), _rate_limit_error(), _openai_response("Preložený"), ] result = translate._call_openai("Hello", "sk") assert result == "Preložený" assert mock_client.chat.completions.create.call_count == 3 def test_call_openai_raises_after_max_retries(): with patch("translate.time.sleep"), \ patch("translate._client") as mock_client: mock_client.chat.completions.create.side_effect = _rate_limit_error() with pytest.raises(RateLimitError): translate._call_openai("Hello", "sk") assert mock_client.chat.completions.create.call_count == 5 def test_call_openai_backoff_increases(): with patch("translate.time.sleep") as mock_sleep, \ patch("translate._client") as mock_client: mock_client.chat.completions.create.side_effect = [ _rate_limit_error(), _rate_limit_error(), _rate_limit_error(), _openai_response("ok"), ] translate._call_openai("Hello", "sk") delays = [c.args[0] for c in mock_sleep.call_args_list] assert delays == [1.0, 2.0, 4.0] # ── translate_html() ────────────────────────────────────────────────────────── def test_translate_html_preserves_img_tags(db_conn): with patch("translate._client") as mock_client: mock_client.chat.completions.create.return_value = _openai_response("

Ahoj

") result = translate_html('

Hello

x', "sk", db_conn) assert 'x' in result assert "Ahoj" in result def test_translate_html_strips_img_before_api_call(db_conn): with patch("translate._client") as mock_client: mock_client.chat.completions.create.return_value = _openai_response("

Ahoj

") translate_html('

Hello

', "sk", db_conn) sent = mock_client.chat.completions.create.call_args[1]["messages"][1]["content"] assert " Channel Hello World """ _ATOM = b""" Feed Hello World """ _EMPTY_TAGS = b""" """ def test_translate_feed_rss_translates_content(db_conn): with patch("translate._client") as mock_client: mock_client.chat.completions.create.return_value = _openai_response("Ahoj") result = translate_feed(_RSS, "sk", db_conn) assert b"Ahoj" in result def test_translate_feed_rss_calls_openai_per_tag(db_conn): with patch("translate._client") as mock_client: mock_client.chat.completions.create.return_value = _openai_response("X") translate_feed(_RSS, "sk", db_conn) # title (plain) + description (html) = 2 distinct texts → 2 calls assert mock_client.chat.completions.create.call_count == 2 def test_translate_feed_invalid_xml_raises(db_conn): with pytest.raises(ValueError, match="Invalid XML"): translate_feed(b"not xml <<<", "sk", db_conn) def test_translate_feed_empty_tags_no_crash(db_conn): with patch("translate._client") as mock_client: mock_client.chat.completions.create.return_value = _openai_response("X") result = translate_feed(_EMPTY_TAGS, "sk", db_conn) assert b"