Livrare LOT 1 - Didi

This commit is contained in:
Dezvoltari Evotech 2026-06-25 14:13:25 -07:00
commit 5380c3fc63
990 changed files with 133308 additions and 0 deletions

View 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