""" Тесты для helper_bot.services.scoring.rag_client (RagApiClient). """ from unittest.mock import AsyncMock, MagicMock, patch import pytest from helper_bot.services.scoring.exceptions import (InsufficientExamplesError, ScoringError, TextTooShortError) from helper_bot.services.scoring.rag_client import RagApiClient @pytest.mark.unit class TestRagApiClientInit: """Тесты инициализации RagApiClient.""" def test_init_strips_trailing_slash(self): """api_url без trailing slash.""" with patch("helper_bot.services.scoring.rag_client.httpx.AsyncClient"): client = RagApiClient(api_url="http://api/v1/", api_key="key") assert client.api_url == "http://api/v1" def test_source_name_is_rag(self): """source_name возвращает 'rag'.""" with patch("helper_bot.services.scoring.rag_client.httpx.AsyncClient"): client = RagApiClient(api_url="http://api", api_key="key") assert client.source_name == "rag" def test_is_enabled_true_by_default(self): """is_enabled True по умолчанию.""" with patch("helper_bot.services.scoring.rag_client.httpx.AsyncClient"): client = RagApiClient(api_url="http://api", api_key="key") assert client.is_enabled is True def test_is_enabled_false_when_disabled(self): """is_enabled False при enabled=False.""" with patch("helper_bot.services.scoring.rag_client.httpx.AsyncClient"): client = RagApiClient(api_url="http://api", api_key="key", enabled=False) assert client.is_enabled is False @pytest.mark.unit @pytest.mark.asyncio class TestRagApiClientCalculateScore: """Тесты calculate_score.""" @pytest.fixture def client(self): """Клиент с замоканным _client.""" with patch("helper_bot.services.scoring.rag_client.httpx.AsyncClient"): c = RagApiClient(api_url="http://rag/api", api_key="key") c._client = MagicMock() return c async def test_disabled_raises_scoring_error(self, client): """При отключённом клиенте выбрасывается ScoringError.""" client._enabled = False with pytest.raises(ScoringError, match="отключен"): await client.calculate_score("text") async def test_empty_text_raises_text_too_short(self, client): """Пустой текст — TextTooShortError.""" with pytest.raises(TextTooShortError, match="пустой"): await client.calculate_score(" ") async def test_success_200_returns_scoring_result(self, client): """Успешный ответ 200 возвращает ScoringResult.""" mock_response = MagicMock() mock_response.status_code = 200 mock_response.json.return_value = { "rag_score": 0.85, "rag_confidence": 0.9, "rag_score_pos_only": 0.82, "meta": {"model": "test-model"}, } client._client.post = AsyncMock(return_value=mock_response) result = await client.calculate_score("Post text") assert result.score == 0.85 assert result.source == "rag" assert result.model == "test-model" assert result.confidence == 0.9 assert result.metadata["rag_score_pos_only"] == 0.82 async def test_400_insufficient_raises_insufficient_examples(self, client): """400 с 'недостаточно' в detail — InsufficientExamplesError.""" mock_response = MagicMock() mock_response.status_code = 400 mock_response.json.return_value = {"detail": "Недостаточно примеров"} client._client.post = AsyncMock(return_value=mock_response) with pytest.raises(InsufficientExamplesError): await client.calculate_score("text") async def test_400_short_raises_text_too_short(self, client): """400 с 'коротк' в detail — TextTooShortError.""" mock_response = MagicMock() mock_response.status_code = 400 mock_response.json.return_value = {"detail": "Текст слишком короткий"} client._client.post = AsyncMock(return_value=mock_response) with pytest.raises(TextTooShortError): await client.calculate_score("ab") async def test_401_raises_scoring_error(self, client): """401 — ScoringError про аутентификацию.""" mock_response = MagicMock() mock_response.status_code = 401 client._client.post = AsyncMock(return_value=mock_response) with pytest.raises(ScoringError, match="аутентификации"): await client.calculate_score("text") async def test_500_raises_scoring_error(self, client): """5xx — ScoringError.""" mock_response = MagicMock() mock_response.status_code = 500 client._client.post = AsyncMock(return_value=mock_response) with pytest.raises(ScoringError, match="сервера"): await client.calculate_score("text") async def test_timeout_raises_scoring_error(self, client): """Таймаут запроса — ScoringError.""" class FakeTimeoutException(Exception): pass with patch("helper_bot.services.scoring.rag_client.httpx") as mock_httpx: mock_httpx.TimeoutException = FakeTimeoutException client._client.post = AsyncMock(side_effect=FakeTimeoutException("timeout")) with pytest.raises(ScoringError, match="Таймаут"): await client.calculate_score("text") @pytest.mark.unit @pytest.mark.asyncio class TestRagApiClientExamplesAndStats: """Тесты add_positive_example, add_negative_example, get_stats, get_stats_sync.""" @pytest.fixture def client(self): """Клиент с замоканным _client.""" with patch("helper_bot.services.scoring.rag_client.httpx.AsyncClient"): c = RagApiClient(api_url="http://rag/api", api_key="key") c._client = MagicMock() return c async def test_add_positive_example_disabled_returns_early(self, client): """При отключённом клиенте add_positive_example ничего не делает.""" client._enabled = False await client.add_positive_example("text") client._client.post.assert_not_called() async def test_add_positive_example_success(self, client): """Успешное добавление положительного примера.""" mock_response = MagicMock() mock_response.status_code = 200 client._client.post = AsyncMock(return_value=mock_response) await client.add_positive_example("Good post") client._client.post.assert_called_once() call_kwargs = client._client.post.call_args[1] assert call_kwargs["json"] == {"text": "Good post"} async def test_add_positive_example_test_mode_sends_header(self): """При test_mode=True отправляется заголовок X-Test-Mode.""" with patch("helper_bot.services.scoring.rag_client.httpx.AsyncClient"): c = RagApiClient(api_url="http://rag", api_key="k", test_mode=True) c._client = MagicMock() c._client.post = AsyncMock(return_value=MagicMock(status_code=200)) await c.add_positive_example("t") call_kwargs = c._client.post.call_args[1] assert call_kwargs.get("headers", {}).get("X-Test-Mode") == "true" async def test_get_stats_200_returns_json(self, client): """get_stats при 200 возвращает json.""" mock_response = MagicMock() mock_response.status_code = 200 mock_response.json.return_value = {"total": 10} client._client.get = AsyncMock(return_value=mock_response) result = await client.get_stats() assert result == {"total": 10} async def test_get_stats_disabled_returns_empty(self, client): """При отключённом клиенте get_stats возвращает {}.""" client._enabled = False result = await client.get_stats() assert result == {} def test_get_stats_sync_returns_dict(self): """get_stats_sync возвращает словарь с enabled, api_url, timeout.""" with patch("helper_bot.services.scoring.rag_client.httpx.AsyncClient"): client = RagApiClient(api_url="http://api", api_key="k", timeout=15) result = client.get_stats_sync() assert result["enabled"] is True assert result["api_url"] == "http://api" assert result["timeout"] == 15