feat: add submitted collection, /similar and /submitted endpoints (Stage 4)

Made-with: Cursor
This commit is contained in:
2026-02-28 19:00:22 +03:00
parent 955f518429
commit a1d6d2d860
15 changed files with 1308 additions and 400 deletions

View File

@@ -0,0 +1,169 @@
"""
Тесты API endpoints /similar и /submitted.
"""
from unittest.mock import AsyncMock, MagicMock
import pytest
from fastapi.testclient import TestClient
from app.main import app
@pytest.fixture
def mock_rag_service():
"""Mock RAGService для тестов API без загрузки модели."""
service = MagicMock()
service.is_model_loaded = True
service.vector_store.submitted_count = 0
return service
@pytest.fixture
def client(mock_rag_service):
"""TestClient с переопределённым RAG сервисом."""
from app.api.routes import get_service
def override_get_service():
return mock_rag_service
app.dependency_overrides[get_service] = override_get_service
# get_rag_service используется при создании сервиса - get_service вызывает get_rag_service
# Смотрю routes: get_service возвращает get_rag_service(). Значит override get_service достаточно.
with TestClient(app) as c:
yield c
app.dependency_overrides.clear()
def test_similar_endpoint(client, mock_rag_service):
"""POST /api/v1/similar возвращает похожие посты."""
mock_rag_service.find_similar_posts = AsyncMock(
return_value=[
{
"similarity": 0.95,
"created_at": 1706270000,
"post_id": 123,
"text": "Похожий пост",
"rag_score": 0.85,
}
]
)
response = client.post(
"/api/v1/similar",
json={"text": "Текст для поиска", "threshold": 0.9, "hours": 24},
)
assert response.status_code == 200
data = response.json()
assert data["similar_count"] == 1
assert len(data["similar_posts"]) == 1
assert data["similar_posts"][0]["similarity"] == 0.95
assert data["similar_posts"][0]["post_id"] == 123
assert data["similar_posts"][0]["text"] == "Похожий пост"
assert data["similar_posts"][0]["rag_score"] == 0.85
def test_similar_endpoint_empty(client, mock_rag_service):
"""POST /api/v1/similar с пустым результатом."""
mock_rag_service.find_similar_posts = AsyncMock(return_value=[])
response = client.post(
"/api/v1/similar",
json={"text": "Уникальный текст", "threshold": 0.99, "hours": 1},
)
assert response.status_code == 200
assert response.json()["similar_count"] == 0
assert response.json()["similar_posts"] == []
def test_similar_endpoint_default_params(client, mock_rag_service):
"""POST /api/v1/similar с дефолтными параметрами."""
mock_rag_service.find_similar_posts = AsyncMock(return_value=[])
response = client.post(
"/api/v1/similar",
json={"text": "Текст"},
)
assert response.status_code == 200
mock_rag_service.find_similar_posts.assert_called_once_with(
text="Текст",
threshold=0.9,
hours=24,
)
def test_submitted_endpoint_success(client, mock_rag_service):
"""POST /api/v1/submitted успешно добавляет пост."""
mock_rag_service.add_submitted_post = AsyncMock(return_value=True)
mock_rag_service.vector_store.submitted_count = 5
response = client.post(
"/api/v1/submitted",
json={"text": "Новый пост", "post_id": 42, "rag_score": 0.8},
)
assert response.status_code == 200
data = response.json()
assert data["success"] is True
assert "добавлен" in data["message"].lower()
assert data["submitted_count"] == 5
mock_rag_service.add_submitted_post.assert_called_once_with(
text="Новый пост",
post_id=42,
rag_score=0.8,
)
def test_submitted_endpoint_duplicate(client, mock_rag_service):
"""POST /api/v1/submitted при дубликате возвращает success=False."""
mock_rag_service.add_submitted_post = AsyncMock(return_value=False)
mock_rag_service.vector_store.submitted_count = 10
response = client.post(
"/api/v1/submitted",
json={"text": "Дубликат поста"},
)
assert response.status_code == 200
data = response.json()
assert data["success"] is False
assert data["submitted_count"] == 10
def test_submitted_endpoint_optional_fields(client, mock_rag_service):
"""POST /api/v1/submitted с только текстом (post_id, rag_score опциональны)."""
mock_rag_service.add_submitted_post = AsyncMock(return_value=True)
mock_rag_service.vector_store.submitted_count = 1
response = client.post(
"/api/v1/submitted",
json={"text": "Только текст"},
)
assert response.status_code == 200
mock_rag_service.add_submitted_post.assert_called_once_with(
text="Только текст",
post_id=None,
rag_score=None,
)
def test_similar_validation_empty_text(client):
"""POST /api/v1/similar с пустым текстом возвращает 422."""
response = client.post(
"/api/v1/similar",
json={"text": "", "threshold": 0.9, "hours": 24},
)
assert response.status_code == 422
def test_submitted_validation_empty_text(client):
"""POST /api/v1/submitted с пустым текстом возвращает 422."""
response = client.post(
"/api/v1/submitted",
json={"text": ""},
)
assert response.status_code == 422