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,391 @@
"""Tests for API endpoints."""
from unittest.mock import AsyncMock, patch
import pytest
from fastapi.testclient import TestClient
from web.api.app import create_app
from web.api.dependencies import init_concurrency_limiter
from web.config import SettingsCache, WebSettings
from web.exceptions import (
ProviderError,
RateLimitError,
WebConnectionError,
WebTimeoutError,
)
from web.fetch.client import FetchClient
from web.metasearch.client import SearXNGClient
from web.orchestrator import Orchestrator
from web.schemas.search import SearchResponse, SearchResult
from web.search.paid import PaidSearchClient
@pytest.fixture
def mock_response() -> SearchResponse:
"""Create a mock search response."""
return SearchResponse(
request_id="test-id",
results=[
SearchResult(
query="test",
url="https://example.com",
title="Test",
snippet="Test snippet",
rank=1,
site="example.com",
published_at=None,
)
],
total_results=1,
execution_time_ms=100.0,
queries_processed=1,
)
@pytest.fixture
def test_client(test_settings: WebSettings) -> TestClient:
"""Create test client with mocked settings."""
SettingsCache.set(test_settings)
init_concurrency_limiter(test_settings.max_concurrent_requests)
app = create_app()
app.state.settings = test_settings
app.state.search_client = SearXNGClient(settings=test_settings)
app.state.search_client_free = SearXNGClient(settings=test_settings)
app.state.search_client_premium = PaidSearchClient(settings=test_settings)
app.state.fetch_client = FetchClient(settings=test_settings)
app.state.orchestrator_free = Orchestrator(settings=test_settings)
app.state.orchestrator_premium = Orchestrator(
settings=test_settings, llm_provider="openrouter"
)
return TestClient(app)
class TestHealthEndpoints:
"""Tests for health check endpoints."""
def test_ready_endpoint(self, test_client: TestClient) -> None:
"""Test /ready endpoint."""
response = test_client.get("/ready")
assert response.status_code == 200
assert response.json() == {"ready": True}
def test_health_endpoint(self, test_client: TestClient) -> None:
"""Test /health endpoint."""
with patch.object(
SearXNGClient, "health_check", new_callable=AsyncMock
) as mock_health:
mock_health.return_value = True
response = test_client.get("/health")
assert response.status_code == 200
data = response.json()
assert data["status"] == "healthy"
assert len(data["providers"]) == 1
assert data["providers"][0]["name"] == "searxng"
class TestSearchEndpoint:
"""Tests for /v1/search endpoint."""
def test_search_success(
self, test_client: TestClient, mock_response: SearchResponse
) -> None:
"""Test successful search."""
with patch.object(
SearXNGClient, "search", new_callable=AsyncMock
) as mock_search:
mock_search.return_value = mock_response
response = test_client.post(
"/v1/search",
json={"queries": ["test query"], "max_results": 5},
)
assert response.status_code == 200
data = response.json()
assert data["total_results"] == 1
assert len(data["results"]) == 1
assert data["results"][0]["url"] == "https://example.com"
def test_search_validation_empty_query(self, test_client: TestClient) -> None:
"""Test that empty query is rejected."""
response = test_client.post(
"/v1/search",
json={"queries": [""], "max_results": 5},
)
assert response.status_code == 422
def test_search_validation_too_many_queries(self, test_client: TestClient) -> None:
"""Test that too many queries are rejected."""
response = test_client.post(
"/v1/search",
json={"queries": [f"query{i}" for i in range(15)], "max_results": 5},
)
assert response.status_code == 422
def test_search_with_site_allowlist(
self, test_client: TestClient, mock_response: SearchResponse
) -> None:
"""Test search with site allowlist."""
with patch.object(
SearXNGClient, "search", new_callable=AsyncMock
) as mock_search:
mock_search.return_value = mock_response
response = test_client.post(
"/v1/search",
json={
"queries": ["test"],
"site_allowlist": ["example.com", "test.com"],
},
)
assert response.status_code == 200
def test_request_id_header(
self, test_client: TestClient, mock_response: SearchResponse
) -> None:
"""Test that X-Request-ID header is returned."""
with patch.object(
SearXNGClient, "search", new_callable=AsyncMock
) as mock_search:
mock_search.return_value = mock_response
response = test_client.post(
"/v1/search",
json={"queries": ["test"]},
headers={"X-Request-ID": "custom-request-id"},
)
assert response.status_code == 200
assert "X-Request-ID" in response.headers
assert response.headers["X-Request-ID"] == "custom-request-id"
class TestFetchEndpoint:
"""Tests for /v1/fetch endpoint."""
def test_fetch_rejects_private_urls(self, test_client: TestClient) -> None:
"""Test that private/localhost URLs are rejected (SSRF protection)."""
response = test_client.post(
"/v1/fetch",
json={"urls": ["http://localhost/secret"]},
)
assert response.status_code == 422
def test_fetch_rejects_non_http_schemes(self, test_client: TestClient) -> None:
"""Test that non-HTTP(S) schemes are rejected."""
response = test_client.post(
"/v1/fetch",
json={"urls": ["ftp://example.com/file"]},
)
assert response.status_code == 422
def test_fetch_validation_empty_urls(self, test_client: TestClient) -> None:
"""Test that empty URL list is rejected."""
response = test_client.post(
"/v1/fetch",
json={"urls": []},
)
assert response.status_code == 422
class TestInfoEndpoint:
"""Tests for /v1/info endpoint."""
def test_info_returns_resource(self, test_client: TestClient) -> None:
"""Test that /v1/info returns resource information."""
response = test_client.get("/v1/info")
assert response.status_code == 200
data = response.json()
assert "resource" in data
assert data["resource"]["slug"] == "web-factcheck"
def test_info_returns_functions(self, test_client: TestClient) -> None:
"""Test that /v1/info returns function definitions."""
response = test_client.get("/v1/info")
assert response.status_code == 200
data = response.json()
assert "functions" in data
assert len(data["functions"]) >= 2
slugs = [f["slug"] for f in data["functions"]]
assert "web-gather-evidence" in slugs
assert "web-search" in slugs
assert "web-fetch" in slugs
class TestSearchErrorResponses:
"""Tests for error response codes on /v1/search."""
def test_search_returns_429_on_rate_limit(self, test_client: TestClient) -> None:
"""Test that RateLimitError returns 429."""
with patch.object(
SearXNGClient, "search", new_callable=AsyncMock
) as mock_search:
mock_search.side_effect = RateLimitError(
"Rate limit exceeded", retry_after=5.0
)
response = test_client.post(
"/v1/search",
json={"queries": ["test"]},
)
assert response.status_code == 429
assert "Retry-After" in response.headers
def test_search_returns_502_on_connection_error(
self, test_client: TestClient
) -> None:
"""Test that WebConnectionError returns 502."""
with patch.object(
SearXNGClient, "search", new_callable=AsyncMock
) as mock_search:
mock_search.side_effect = WebConnectionError("searxng", "refused")
response = test_client.post(
"/v1/search",
json={"queries": ["test"]},
)
assert response.status_code == 502
data = response.json()
assert data["detail"]["error"] == "connection_error"
def test_search_returns_502_on_provider_error(
self, test_client: TestClient
) -> None:
"""Test that ProviderError returns 502."""
with patch.object(
SearXNGClient, "search", new_callable=AsyncMock
) as mock_search:
mock_search.side_effect = ProviderError("searxng", "Server error")
response = test_client.post(
"/v1/search",
json={"queries": ["test"]},
)
assert response.status_code == 502
data = response.json()
assert data["detail"]["error"] == "provider_error"
def test_search_returns_504_on_timeout(self, test_client: TestClient) -> None:
"""Test that WebTimeoutError returns 504."""
with patch.object(
SearXNGClient, "search", new_callable=AsyncMock
) as mock_search:
mock_search.side_effect = WebTimeoutError("Timeout", timeout=30.0)
response = test_client.post(
"/v1/search",
json={"queries": ["test"]},
)
assert response.status_code == 504
data = response.json()
assert data["detail"]["error"] == "timeout"
class TestAuthFlow:
"""Tests for Bearer token authentication."""
@pytest.fixture
def auth_settings(self) -> WebSettings:
"""Create settings with authentication enabled."""
return WebSettings(
searxng_base_url="http://localhost:55100",
llm_base_url="http://localhost:14011",
external_url="http://localhost:51100",
api_tokens="valid-token-123,valid-token-456",
)
@pytest.fixture
def auth_client(self, auth_settings: WebSettings) -> TestClient:
"""Create test client with auth enabled."""
SettingsCache.set(auth_settings)
init_concurrency_limiter(auth_settings.max_concurrent_requests)
app = create_app()
app.state.settings = auth_settings
app.state.search_client = SearXNGClient(settings=auth_settings)
app.state.search_client_free = SearXNGClient(settings=auth_settings)
app.state.search_client_premium = PaidSearchClient(settings=auth_settings)
app.state.fetch_client = FetchClient(settings=auth_settings)
app.state.orchestrator_free = Orchestrator(settings=auth_settings)
app.state.orchestrator_premium = Orchestrator(
settings=auth_settings, llm_provider="openrouter"
)
return TestClient(app)
def test_auth_required_returns_401_without_token(
self, auth_client: TestClient
) -> None:
"""Test that missing auth header returns 401."""
response = auth_client.post(
"/v1/search",
json={"queries": ["test"]},
)
assert response.status_code == 401
def test_auth_required_returns_401_with_invalid_token(
self, auth_client: TestClient
) -> None:
"""Test that invalid token returns 401."""
response = auth_client.post(
"/v1/search",
json={"queries": ["test"]},
headers={"Authorization": "Bearer wrong-token"},
)
assert response.status_code == 401
def test_auth_succeeds_with_valid_token(self, auth_client: TestClient) -> None:
"""Test that valid token allows access."""
with patch.object(
SearXNGClient, "search", new_callable=AsyncMock
) as mock_search:
mock_search.return_value = SearchResponse(
request_id="test",
results=[],
total_results=0,
execution_time_ms=10.0,
queries_processed=1,
)
response = auth_client.post(
"/v1/search",
json={"queries": ["test"]},
headers={"Authorization": "Bearer valid-token-123"},
)
assert response.status_code == 200
def test_auth_invalid_format_returns_401(self, auth_client: TestClient) -> None:
"""Test that non-Bearer auth format returns 401."""
response = auth_client.post(
"/v1/search",
json={"queries": ["test"]},
headers={"Authorization": "Basic dXNlcjpwYXNz"},
)
assert response.status_code == 401
def test_health_endpoints_skip_auth(self, auth_client: TestClient) -> None:
"""Test that health endpoints don't require auth."""
response = auth_client.get("/ready")
assert response.status_code == 200