Initial commit: Research Bridge API with Podman support
This commit is contained in:
260
tests/unit/test_router.py
Normal file
260
tests/unit/test_router.py
Normal file
@@ -0,0 +1,260 @@
|
||||
"""Unit tests for API router."""
|
||||
from unittest.mock import AsyncMock, Mock, patch
|
||||
|
||||
import pytest
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from src.api.app import create_app
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def client():
|
||||
app = create_app()
|
||||
return TestClient(app)
|
||||
|
||||
|
||||
class TestHealthEndpoint:
|
||||
"""Test cases for health endpoint."""
|
||||
|
||||
def test_health_searxng_healthy(self, client):
|
||||
"""Test health check when SearXNG is up."""
|
||||
with patch("src.api.router.SearXNGClient") as mock_class:
|
||||
mock_instance = AsyncMock()
|
||||
mock_instance.health_check = AsyncMock(return_value=True)
|
||||
mock_instance.__aenter__ = AsyncMock(return_value=mock_instance)
|
||||
mock_instance.__aexit__ = AsyncMock(return_value=None)
|
||||
mock_class.return_value = mock_instance
|
||||
|
||||
response = client.get("/health")
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["status"] == "degraded" # No Kimi key configured in test
|
||||
assert data["searxng_connected"] is True
|
||||
assert data["kimi_coding_available"] is False
|
||||
|
||||
def test_health_searxng_down(self, client):
|
||||
"""Test health check when SearXNG is down."""
|
||||
with patch("src.api.router.SearXNGClient") as mock_class:
|
||||
mock_instance = AsyncMock()
|
||||
mock_instance.health_check = AsyncMock(return_value=False)
|
||||
mock_instance.__aenter__ = AsyncMock(return_value=mock_instance)
|
||||
mock_instance.__aexit__ = AsyncMock(return_value=None)
|
||||
mock_class.return_value = mock_instance
|
||||
|
||||
response = client.get("/health")
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["status"] == "degraded"
|
||||
assert data["searxng_connected"] is False
|
||||
|
||||
|
||||
class TestSearchEndpoint:
|
||||
"""Test cases for search endpoint."""
|
||||
|
||||
def test_search_get_success(self, client):
|
||||
"""Test GET search with successful response."""
|
||||
mock_response = Mock()
|
||||
mock_response.query = "python"
|
||||
mock_response.results = []
|
||||
mock_response.total = 0
|
||||
mock_response.page = 1
|
||||
mock_response.metadata = {}
|
||||
|
||||
with patch("src.api.router.SearXNGClient") as mock_class:
|
||||
mock_instance = AsyncMock()
|
||||
mock_instance.search = AsyncMock(return_value=mock_response)
|
||||
mock_instance.__aenter__ = AsyncMock(return_value=mock_instance)
|
||||
mock_instance.__aexit__ = AsyncMock(return_value=None)
|
||||
mock_class.return_value = mock_instance
|
||||
|
||||
response = client.get("/search?q=python")
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["query"] == "python"
|
||||
assert "results" in data
|
||||
|
||||
def test_search_post_success(self, client):
|
||||
"""Test POST search with successful response."""
|
||||
mock_response = Mock()
|
||||
mock_response.query = "asyncio"
|
||||
mock_response.results = []
|
||||
mock_response.total = 0
|
||||
mock_response.page = 1
|
||||
mock_response.metadata = {}
|
||||
|
||||
with patch("src.api.router.SearXNGClient") as mock_class:
|
||||
mock_instance = AsyncMock()
|
||||
mock_instance.search = AsyncMock(return_value=mock_response)
|
||||
mock_instance.__aenter__ = AsyncMock(return_value=mock_instance)
|
||||
mock_instance.__aexit__ = AsyncMock(return_value=None)
|
||||
mock_class.return_value = mock_instance
|
||||
|
||||
response = client.post("/search", json={
|
||||
"q": "asyncio",
|
||||
"engines": ["google"],
|
||||
"page": 1
|
||||
})
|
||||
|
||||
assert response.status_code == 200
|
||||
|
||||
def test_search_validation_error(self, client):
|
||||
"""Test search with invalid parameters."""
|
||||
response = client.get("/search?q=a")
|
||||
# Just test that it accepts the request
|
||||
assert response.status_code in [200, 502] # 502 if no SearXNG
|
||||
|
||||
|
||||
class TestResearchEndpoint:
|
||||
"""Test cases for research endpoint (Phase 2 - with synthesis)."""
|
||||
|
||||
def test_research_phase2_with_synthesis(self, client):
|
||||
"""Test research endpoint returns synthesis (Phase 2)."""
|
||||
from src.models.schemas import SearchResult
|
||||
import src.api.router as router_module
|
||||
|
||||
mock_search_response = Mock()
|
||||
mock_search_response.results = [
|
||||
SearchResult(title="Test", url="https://example.com", source="google")
|
||||
]
|
||||
mock_search_response.total = 1
|
||||
mock_search_response.page = 1
|
||||
|
||||
mock_synthesis = Mock()
|
||||
mock_synthesis.content = "This is a synthesized answer."
|
||||
mock_synthesis.sources = [{"index": 1, "title": "Test", "url": "https://example.com"}]
|
||||
mock_synthesis.tokens_used = 100
|
||||
|
||||
# Temporarily set API key in router module
|
||||
original_key = router_module.KIMI_API_KEY
|
||||
router_module.KIMI_API_KEY = "sk-test"
|
||||
|
||||
try:
|
||||
with patch("src.api.router.SearXNGClient") as mock_search_class, \
|
||||
patch("src.api.router.Synthesizer") as mock_synth_class:
|
||||
|
||||
# Mock SearXNG
|
||||
mock_search_instance = AsyncMock()
|
||||
mock_search_instance.search = AsyncMock(return_value=mock_search_response)
|
||||
mock_search_instance.__aenter__ = AsyncMock(return_value=mock_search_instance)
|
||||
mock_search_instance.__aexit__ = AsyncMock(return_value=None)
|
||||
mock_search_class.return_value = mock_search_instance
|
||||
|
||||
# Mock Synthesizer
|
||||
mock_synth_instance = AsyncMock()
|
||||
mock_synth_instance.synthesize = AsyncMock(return_value=mock_synthesis)
|
||||
mock_synth_instance.__aenter__ = AsyncMock(return_value=mock_synth_instance)
|
||||
mock_synth_instance.__aexit__ = AsyncMock(return_value=None)
|
||||
mock_synth_class.return_value = mock_synth_instance
|
||||
|
||||
response = client.post("/research", json={
|
||||
"query": "python asyncio",
|
||||
"depth": "shallow",
|
||||
"sources": ["web"]
|
||||
})
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["query"] == "python asyncio"
|
||||
assert data["depth"] == "shallow"
|
||||
assert data["synthesis"] == "This is a synthesized answer."
|
||||
assert data["metadata"]["phase"] == "2"
|
||||
assert len(data["sources"]) == 1
|
||||
finally:
|
||||
router_module.KIMI_API_KEY = original_key
|
||||
|
||||
def test_research_no_api_key_returns_message(self, client):
|
||||
"""Test research endpoint without API key returns appropriate message."""
|
||||
from src.models.schemas import SearchResult
|
||||
|
||||
mock_search_response = Mock()
|
||||
mock_search_response.results = [
|
||||
SearchResult(title="Test", url="https://example.com", source="google")
|
||||
]
|
||||
|
||||
with patch("src.api.router.SearXNGClient") as mock_class:
|
||||
mock_instance = AsyncMock()
|
||||
mock_instance.search = AsyncMock(return_value=mock_search_response)
|
||||
mock_instance.__aenter__ = AsyncMock(return_value=mock_instance)
|
||||
mock_instance.__aexit__ = AsyncMock(return_value=None)
|
||||
mock_class.return_value = mock_instance
|
||||
|
||||
# Ensure no API key
|
||||
with patch.dict("os.environ", {}, clear=True):
|
||||
with patch("src.api.router.KIMI_API_KEY", None):
|
||||
response = client.post("/research", json={
|
||||
"query": "test",
|
||||
"sources": ["web"]
|
||||
})
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert "not configured" in data["synthesis"].lower() or "API key" in data["synthesis"]
|
||||
|
||||
def test_research_no_results(self, client):
|
||||
"""Test research endpoint with no search results."""
|
||||
mock_search_response = Mock()
|
||||
mock_search_response.results = []
|
||||
|
||||
with patch("src.api.router.SearXNGClient") as mock_class:
|
||||
mock_instance = AsyncMock()
|
||||
mock_instance.search = AsyncMock(return_value=mock_search_response)
|
||||
mock_instance.__aenter__ = AsyncMock(return_value=mock_instance)
|
||||
mock_instance.__aexit__ = AsyncMock(return_value=None)
|
||||
mock_class.return_value = mock_instance
|
||||
|
||||
response = client.post("/research", json={
|
||||
"query": "xyzabc123nonexistent",
|
||||
"sources": ["web"]
|
||||
})
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert "no results" in data["synthesis"].lower()
|
||||
|
||||
def test_research_with_omit_raw(self, client):
|
||||
"""Test research endpoint with omit_raw=true."""
|
||||
from src.models.schemas import SearchResult
|
||||
import src.api.router as router_module
|
||||
|
||||
mock_search_response = Mock()
|
||||
mock_search_response.results = [
|
||||
SearchResult(title="Test", url="https://example.com", source="google")
|
||||
]
|
||||
|
||||
mock_synthesis = Mock()
|
||||
mock_synthesis.content = "Answer"
|
||||
mock_synthesis.sources = []
|
||||
mock_synthesis.tokens_used = 50
|
||||
|
||||
original_key = router_module.KIMI_API_KEY
|
||||
router_module.KIMI_API_KEY = "sk-test"
|
||||
|
||||
try:
|
||||
with patch("src.api.router.SearXNGClient") as mock_search_class, \
|
||||
patch("src.api.router.Synthesizer") as mock_synth_class:
|
||||
|
||||
mock_search_instance = AsyncMock()
|
||||
mock_search_instance.search = AsyncMock(return_value=mock_search_response)
|
||||
mock_search_instance.__aenter__ = AsyncMock(return_value=mock_search_instance)
|
||||
mock_search_instance.__aexit__ = AsyncMock(return_value=None)
|
||||
mock_search_class.return_value = mock_search_instance
|
||||
|
||||
mock_synth_instance = AsyncMock()
|
||||
mock_synth_instance.synthesize = AsyncMock(return_value=mock_synthesis)
|
||||
mock_synth_instance.__aenter__ = AsyncMock(return_value=mock_synth_instance)
|
||||
mock_synth_instance.__aexit__ = AsyncMock(return_value=None)
|
||||
mock_synth_class.return_value = mock_synth_instance
|
||||
|
||||
response = client.post("/research", json={
|
||||
"query": "test",
|
||||
"omit_raw": True
|
||||
})
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["raw_results"] is None
|
||||
finally:
|
||||
router_module.KIMI_API_KEY = original_key
|
||||
Reference in New Issue
Block a user