Files
telegram-helper-bot/tests/test_rag_client.py
2026-02-02 00:54:23 +03:00

207 lines
8.6 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
Тесты для 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