Livrare LOT 1 - Didi
This commit is contained in:
commit
5380c3fc63
990 changed files with 133308 additions and 0 deletions
292
ai_platform/modules/web/tests/test_search_client.py
Normal file
292
ai_platform/modules/web/tests/test_search_client.py
Normal file
|
|
@ -0,0 +1,292 @@
|
|||
"""Unit tests for SearXNGClient with mocked httpx."""
|
||||
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
import httpx
|
||||
import pytest
|
||||
|
||||
from web.config import WebSettings
|
||||
from web.exceptions import ProviderError, WebTimeoutError
|
||||
from web.metasearch.client import SearXNGClient
|
||||
from web.schemas.search import SearchRequest
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def settings() -> WebSettings:
|
||||
"""Create test settings."""
|
||||
return WebSettings(
|
||||
searxng_base_url="http://localhost:55100",
|
||||
llm_base_url="http://localhost:14011",
|
||||
external_url="http://localhost:51100",
|
||||
max_retries=2,
|
||||
retry_min_wait=0.1,
|
||||
retry_max_wait=1.0,
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def client(settings: WebSettings) -> SearXNGClient:
|
||||
"""Create a SearXNGClient."""
|
||||
return SearXNGClient(settings)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def search_request() -> SearchRequest:
|
||||
"""Create a basic search request."""
|
||||
return SearchRequest(queries=["test query"], max_results=5)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def searxng_response() -> dict:
|
||||
"""Create a mock SearXNG API response."""
|
||||
return {
|
||||
"results": [
|
||||
{
|
||||
"url": "https://example.com/article",
|
||||
"title": "Test Article",
|
||||
"content": "A test snippet.",
|
||||
"publishedDate": "2024-01-15",
|
||||
},
|
||||
{
|
||||
"url": "https://news.example.com/story",
|
||||
"title": "Another Result",
|
||||
"content": "Another snippet.",
|
||||
"publishedDate": None,
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
class TestSearchClientSearch:
|
||||
"""Tests for SearXNGClient.search."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_search_returns_results(
|
||||
self,
|
||||
client: SearXNGClient,
|
||||
search_request: SearchRequest,
|
||||
searxng_response: dict,
|
||||
) -> None:
|
||||
"""Test successful search returns parsed results."""
|
||||
mock_response = httpx.Response(200, json=searxng_response)
|
||||
with patch.object(httpx.AsyncClient, "get", new_callable=AsyncMock) as mock_get:
|
||||
mock_get.return_value = mock_response
|
||||
response = await client.search(search_request)
|
||||
|
||||
assert response.total_results == 2
|
||||
assert response.results[0].url == "https://example.com/article"
|
||||
assert response.results[0].title == "Test Article"
|
||||
assert response.results[0].rank == 1
|
||||
assert response.results[1].rank == 2
|
||||
assert response.queries_processed == 1
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_search_multiple_queries(
|
||||
self, client: SearXNGClient, searxng_response: dict
|
||||
) -> None:
|
||||
"""Test that multiple queries are run in parallel."""
|
||||
request = SearchRequest(queries=["query1", "query2"], max_results=3)
|
||||
mock_response = httpx.Response(200, json=searxng_response)
|
||||
|
||||
with patch.object(httpx.AsyncClient, "get", new_callable=AsyncMock) as mock_get:
|
||||
mock_get.return_value = mock_response
|
||||
response = await client.search(request)
|
||||
|
||||
assert response.queries_processed == 2
|
||||
# 2 results per query * 2 queries
|
||||
assert response.total_results == 4
|
||||
assert mock_get.call_count == 2
|
||||
|
||||
|
||||
class TestSearchClientRetries:
|
||||
"""Tests for retry logic in SearXNGClient."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_retries_on_timeout(
|
||||
self,
|
||||
client: SearXNGClient,
|
||||
search_request: SearchRequest,
|
||||
searxng_response: dict,
|
||||
) -> None:
|
||||
"""Test that transient timeouts are retried."""
|
||||
mock_response = httpx.Response(200, json=searxng_response)
|
||||
|
||||
with patch.object(httpx.AsyncClient, "get", new_callable=AsyncMock) as mock_get:
|
||||
mock_get.side_effect = [
|
||||
httpx.TimeoutException("timeout"),
|
||||
mock_response,
|
||||
]
|
||||
response = await client.search(search_request)
|
||||
|
||||
assert response.total_results == 2
|
||||
assert mock_get.call_count == 2
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_retries_on_connection_error(
|
||||
self,
|
||||
client: SearXNGClient,
|
||||
search_request: SearchRequest,
|
||||
searxng_response: dict,
|
||||
) -> None:
|
||||
"""Test that connection errors are retried."""
|
||||
mock_response = httpx.Response(200, json=searxng_response)
|
||||
|
||||
with patch.object(httpx.AsyncClient, "get", new_callable=AsyncMock) as mock_get:
|
||||
mock_get.side_effect = [
|
||||
httpx.ConnectError("refused"),
|
||||
mock_response,
|
||||
]
|
||||
response = await client.search(search_request)
|
||||
|
||||
assert response.total_results == 2
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_raises_after_max_retries(
|
||||
self, client: SearXNGClient, search_request: SearchRequest
|
||||
) -> None:
|
||||
"""Test that error is raised after all retries exhausted."""
|
||||
with patch.object(httpx.AsyncClient, "get", new_callable=AsyncMock) as mock_get:
|
||||
mock_get.side_effect = httpx.TimeoutException("timeout")
|
||||
|
||||
with pytest.raises(WebTimeoutError):
|
||||
await client.search(search_request)
|
||||
|
||||
# Initial attempt + 2 retries = 3 calls
|
||||
assert mock_get.call_count == 3
|
||||
|
||||
|
||||
class TestSearchClientErrors:
|
||||
"""Tests for error handling in SearXNGClient."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_raises_provider_error_on_429(
|
||||
self, client: SearXNGClient, search_request: SearchRequest
|
||||
) -> None:
|
||||
"""Test that 429 raises ProviderError without retry."""
|
||||
mock_response = httpx.Response(429, json={})
|
||||
|
||||
with patch.object(httpx.AsyncClient, "get", new_callable=AsyncMock) as mock_get:
|
||||
mock_get.return_value = mock_response
|
||||
|
||||
with pytest.raises(ProviderError):
|
||||
await client.search(search_request)
|
||||
|
||||
# ProviderError should not be retried
|
||||
assert mock_get.call_count == 1
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_raises_provider_error_on_500(
|
||||
self, client: SearXNGClient, search_request: SearchRequest
|
||||
) -> None:
|
||||
"""Test that 500 raises ProviderError after retries are exhausted."""
|
||||
mock_response = httpx.Response(500, json={})
|
||||
|
||||
with patch.object(httpx.AsyncClient, "get", new_callable=AsyncMock) as mock_get:
|
||||
mock_get.return_value = mock_response
|
||||
|
||||
with pytest.raises(ProviderError):
|
||||
await client.search(search_request)
|
||||
|
||||
# 500 server errors should be retried (1 initial + 2 retries = 3)
|
||||
assert mock_get.call_count == 3
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_retries_500_then_succeeds(
|
||||
self, client: SearXNGClient, search_request: SearchRequest
|
||||
) -> None:
|
||||
"""Test that 500 is retried and succeeds on subsequent attempt."""
|
||||
fail_response = httpx.Response(500, json={})
|
||||
success_response = httpx.Response(
|
||||
200,
|
||||
json={
|
||||
"results": [
|
||||
{
|
||||
"url": "https://example.com/a",
|
||||
"title": "Result",
|
||||
"content": "Content",
|
||||
}
|
||||
]
|
||||
},
|
||||
)
|
||||
|
||||
with patch.object(httpx.AsyncClient, "get", new_callable=AsyncMock) as mock_get:
|
||||
mock_get.side_effect = [fail_response, success_response]
|
||||
response = await client.search(search_request)
|
||||
|
||||
assert response.total_results == 1
|
||||
assert mock_get.call_count == 2
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_raises_provider_error_on_403(
|
||||
self, client: SearXNGClient, search_request: SearchRequest
|
||||
) -> None:
|
||||
"""Test that 403 raises ProviderError (not authorized)."""
|
||||
mock_response = httpx.Response(403, json={})
|
||||
|
||||
with patch.object(httpx.AsyncClient, "get", new_callable=AsyncMock) as mock_get:
|
||||
mock_get.return_value = mock_response
|
||||
|
||||
with pytest.raises(ProviderError):
|
||||
await client.search(search_request)
|
||||
|
||||
|
||||
class TestSearchClientSiteFilters:
|
||||
"""Tests for site filtering in SearXNGClient."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_blocklist_filters_results(self, client: SearXNGClient) -> None:
|
||||
"""Test that blocklist sites are removed from results."""
|
||||
request = SearchRequest(
|
||||
queries=["test"],
|
||||
max_results=5,
|
||||
site_blocklist=["blocked.com"],
|
||||
)
|
||||
searxng_response = {
|
||||
"results": [
|
||||
{
|
||||
"url": "https://example.com/a",
|
||||
"title": "Good",
|
||||
"content": "Good result",
|
||||
},
|
||||
{
|
||||
"url": "https://blocked.com/b",
|
||||
"title": "Blocked",
|
||||
"content": "Blocked result",
|
||||
},
|
||||
]
|
||||
}
|
||||
mock_response = httpx.Response(200, json=searxng_response)
|
||||
|
||||
with patch.object(httpx.AsyncClient, "get", new_callable=AsyncMock) as mock_get:
|
||||
mock_get.return_value = mock_response
|
||||
response = await client.search(request)
|
||||
|
||||
assert response.total_results == 1
|
||||
assert response.results[0].url == "https://example.com/a"
|
||||
|
||||
|
||||
class TestSearchClientHealthCheck:
|
||||
"""Tests for SearXNGClient health check."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_health_check_returns_true_on_200(
|
||||
self, client: SearXNGClient
|
||||
) -> None:
|
||||
"""Test that health check returns True on 200."""
|
||||
mock_response = httpx.Response(200, json={})
|
||||
with patch.object(httpx.AsyncClient, "get", new_callable=AsyncMock) as mock_get:
|
||||
mock_get.return_value = mock_response
|
||||
result = await client.health_check()
|
||||
|
||||
assert result is True
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_health_check_returns_false_on_error(
|
||||
self, client: SearXNGClient
|
||||
) -> None:
|
||||
"""Test that health check returns False on exception."""
|
||||
with patch.object(httpx.AsyncClient, "get", new_callable=AsyncMock) as mock_get:
|
||||
mock_get.side_effect = httpx.ConnectError("refused")
|
||||
result = await client.health_check()
|
||||
|
||||
assert result is False
|
||||
Loading…
Add table
Add a link
Reference in a new issue