Files
routstr-core/tests/integration/test_provider_management.py
T
2025-08-08 19:00:59 -04:00

647 lines
22 KiB
Python

"""
Integration tests for provider management functionality.
Tests GET /v1/providers/ endpoint for listing and managing providers.
"""
from typing import Any
from unittest.mock import patch
import pytest
from httpx import AsyncClient
from .utils import PerformanceValidator, ResponseValidator
@pytest.mark.integration
@pytest.mark.asyncio
async def test_providers_endpoint_default_response(
integration_client: AsyncClient, db_snapshot: Any
) -> None:
"""Test GET /v1/providers/ endpoint returns list of providers in default format"""
# Capture initial database state
await db_snapshot.capture()
# Mock the Nostr relay queries and onion fetching to avoid external dependencies
mock_events: list[dict[str, Any]] = [
{
"id": "event1",
"content": "Check out this provider: http://provider1.onion",
"created_at": 1234567890,
},
{
"id": "event2",
"content": "Another provider at http://provider2.onion is good",
"created_at": 1234567891,
},
]
# Mock the healthy provider check
mock_fetch_responses = {
"http://provider1.onion": {"status_code": 200, "json": {"status": "healthy"}},
"http://provider2.onion": {"status_code": 200, "json": {"status": "healthy"}},
}
with patch(
"router.discovery.query_nostr_relay_for_providers", return_value=mock_events
):
with patch("router.discovery.fetch_provider_health") as mock_fetch:
# Configure mock to return appropriate responses
mock_fetch.side_effect = lambda url: mock_fetch_responses.get(
url, {"status_code": 500, "json": {"error": "Unknown provider"}}
)
response = await integration_client.get("/v1/providers/")
assert response.status_code == 200
data = response.json()
# Validate response structure
assert "providers" in data
assert isinstance(data["providers"], list)
# In default format, should return list of provider URLs (strings)
for provider in data["providers"]:
assert isinstance(provider, str)
assert provider.endswith(".onion")
# Verify no database state changes
diff = await db_snapshot.diff()
assert len(diff["api_keys"]["added"]) == 0
assert len(diff["api_keys"]["modified"]) == 0
assert len(diff["api_keys"]["removed"]) == 0
@pytest.mark.integration
@pytest.mark.asyncio
async def test_providers_endpoint_with_include_json(
integration_client: AsyncClient, db_snapshot: Any
) -> None:
"""Test GET /v1/providers/ with include_json=true returns full provider details"""
# Capture initial database state
await db_snapshot.capture()
# Mock events with provider URLs
mock_events: list[dict[str, Any]] = [
{
"id": "event1",
"content": "Provider info: http://test-provider.onion",
"created_at": 1234567890,
}
]
# Mock provider health check response
mock_provider_response = {
"status": "online",
"name": "Test Provider",
"models": ["gpt-3.5-turbo", "gpt-4"],
"pricing": {"gpt-3.5-turbo": "0.002", "gpt-4": "0.03"},
}
with patch(
"router.discovery.query_nostr_relay_for_providers", return_value=mock_events
):
with patch("router.discovery.fetch_provider_health") as mock_fetch:
mock_fetch.return_value = {
"status_code": 200,
"json": mock_provider_response,
}
response = await integration_client.get("/v1/providers/?include_json=true")
assert response.status_code == 200
data = response.json()
# Validate response structure
assert "providers" in data
assert isinstance(data["providers"], list)
# With include_json=true, should return list of dictionaries
for provider in data["providers"]:
assert isinstance(provider, dict)
# Each provider should be in format {url: json_data}
assert len(provider) == 1
url = list(provider.keys())[0]
json_data = provider[url]
assert url.endswith(".onion")
assert isinstance(json_data, dict)
# Verify no database state changes
diff = await db_snapshot.diff()
assert len(diff["api_keys"]["added"]) == 0
assert len(diff["api_keys"]["modified"]) == 0
assert len(diff["api_keys"]["removed"]) == 0
@pytest.mark.integration
@pytest.mark.asyncio
async def test_providers_data_structure_validation(
integration_client: AsyncClient,
) -> None:
"""Test provider data structure contains expected fields"""
# Mock RIP-02 provider announcement event
mock_events: list[dict[str, Any]] = [
{
"id": "event1",
"pubkey": "test_pubkey",
"created_at": 1234567890,
"content": "Comprehensive provider announcement",
"tags": [
["d", "provider-123"],
["endpoint", "https://api.provider.example/v1"],
["name", "Comprehensive Provider"],
["description", "A comprehensive AI provider"],
["model", "gpt-3.5-turbo"],
["model", "gpt-4"],
]
}
]
mock_health_response = {
"status_code": 200,
"endpoint": "models",
"json": {
"data": [
{"id": "gpt-3.5-turbo", "object": "model"},
{"id": "gpt-4", "object": "model"}
]
}
}
with patch(
"router.discovery.query_nostr_relay_for_providers", return_value=mock_events
):
with patch("router.discovery.fetch_provider_health") as mock_fetch:
mock_fetch.return_value = mock_health_response
response = await integration_client.get("/v1/providers/?include_json=true")
assert response.status_code == 200
data = response.json()
providers = data["providers"]
# Validate that provider data contains expected fields
assert len(providers) > 0
for provider_data in providers:
# Should have provider and health keys based on actual implementation
assert "provider" in provider_data
assert "health" in provider_data
provider_info = provider_data["provider"]
# Expected fields from RIP-02 parser
expected_fields = ["id", "name", "endpoint_url", "supported_models"]
for field in expected_fields:
assert field in provider_info
# Validate models structure if present
if "supported_models" in provider_info:
models = provider_info["supported_models"]
assert isinstance(models, list)
# Should have the models from the mocked event
assert "gpt-3.5-turbo" in models
assert "gpt-4" in models
@pytest.mark.integration
@pytest.mark.asyncio
async def test_providers_endpoint_no_providers_found(
integration_client: AsyncClient,
) -> None:
"""Test providers endpoint when no providers are found"""
# Mock empty events (no providers mentioned)
mock_events: list[dict[str, Any]] = []
with patch(
"router.discovery.query_nostr_relay_for_providers", return_value=mock_events
):
response = await integration_client.get("/v1/providers/")
assert response.status_code == 200
data = response.json()
# Should return empty list
assert "providers" in data
assert isinstance(data["providers"], list)
assert len(data["providers"]) == 0
@pytest.mark.integration
@pytest.mark.asyncio
async def test_providers_endpoint_offline_providers(
integration_client: AsyncClient,
) -> None:
"""Test providers endpoint handling of offline/unhealthy providers"""
mock_events: list[dict[str, Any]] = [
{
"id": "event1",
"pubkey": "healthy_provider_pubkey",
"created_at": 1234567890,
"content": "Healthy provider announcement",
"tags": [
["d", "healthy-provider"],
["endpoint", "http://healthy-provider.onion"],
["name", "Healthy Provider"],
]
},
{
"id": "event2",
"pubkey": "offline_provider_pubkey",
"created_at": 1234567891,
"content": "Offline provider announcement",
"tags": [
["d", "offline-provider"],
["endpoint", "http://offline-provider.onion"],
["name", "Offline Provider"],
]
},
]
# Mock one healthy and one offline provider
def mock_fetch_provider_health(url: str) -> dict[str, Any]:
if "healthy" in url:
return {"status_code": 200, "endpoint": "root", "json": {"status": "online"}}
else:
return {"status_code": 500, "endpoint": "error", "json": {"error": "Service unavailable"}}
with patch(
"router.discovery.query_nostr_relay_for_providers", return_value=mock_events
):
with patch(
"router.discovery.fetch_provider_health",
side_effect=mock_fetch_provider_health,
):
response = await integration_client.get("/v1/providers/?include_json=true")
assert response.status_code == 200
data = response.json()
# Should include both providers regardless of status
assert len(data["providers"]) == 2
# Verify that offline providers are still included but marked appropriately
for provider_data in data["providers"]:
assert "provider" in provider_data
assert "health" in provider_data
provider_info = provider_data["provider"]
health_info = provider_data["health"]
if "offline" in provider_info["endpoint_url"]:
# Offline provider should have error information in health
assert health_info["status_code"] == 500
assert "error" in health_info["json"]
else:
# Healthy provider should have successful health check
assert health_info["status_code"] == 200
assert "status" in health_info["json"] or "error" not in health_info["json"]
@pytest.mark.integration
@pytest.mark.asyncio
async def test_providers_endpoint_duplicate_urls(
integration_client: AsyncClient,
) -> None:
"""Test providers endpoint handles duplicate URLs correctly"""
# Mock events with duplicate provider events (same event ID) - should be deduplicated by relay query logic
mock_events: list[dict[str, Any]] = [
{
"id": "event1",
"pubkey": "provider_pubkey",
"created_at": 1234567890,
"content": "Provider announcement",
"tags": [
["d", "provider-1"],
["endpoint", "http://provider.onion"],
["name", "Provider"],
]
},
{
"id": "event2",
"pubkey": "other_provider_pubkey",
"created_at": 1234567892,
"content": "Different provider announcement",
"tags": [
["d", "other-provider"],
["endpoint", "http://other-provider.onion"],
["name", "Other Provider"],
]
},
]
with patch(
"router.discovery.query_nostr_relay_for_providers", return_value=mock_events
):
with patch("router.discovery.fetch_provider_health") as mock_fetch:
mock_fetch.return_value = {"status_code": 200, "endpoint": "root", "json": {"status": "online"}}
response = await integration_client.get("/v1/providers/")
assert response.status_code == 200
data = response.json()
# Should return 2 unique providers based on events
providers = data["providers"]
assert len(providers) == 2 # 2 unique events
# Verify all providers are unique by endpoint_url
endpoint_urls = []
for provider_data in providers:
endpoint_urls.append(provider_data["endpoint_url"])
unique_endpoints = set(endpoint_urls)
assert len(unique_endpoints) == len(endpoint_urls)
@pytest.mark.integration
@pytest.mark.asyncio
async def test_providers_endpoint_nostr_relay_failures(
integration_client: AsyncClient,
) -> None:
"""Test providers endpoint handles Nostr relay failures gracefully"""
# Mock relay failure
async def failing_query(*args: Any, **kwargs: Any) -> None:
raise Exception("Connection to relay failed")
with patch(
"router.discovery.query_nostr_relay_for_providers", side_effect=failing_query
):
response = await integration_client.get("/v1/providers/")
# Should still return 200 with empty providers list
assert response.status_code == 200
data = response.json()
assert "providers" in data
assert isinstance(data["providers"], list)
assert len(data["providers"]) == 0
@pytest.mark.integration
@pytest.mark.asyncio
async def test_providers_endpoint_malformed_urls(
integration_client: AsyncClient,
) -> None:
"""Test providers endpoint handles malformed URLs in Nostr events"""
mock_events: list[dict[str, Any]] = [
{
"id": "event1",
"content": "Valid provider: http://good-provider.onion",
"created_at": 1234567890,
},
{
"id": "event2",
"content": "Invalid URL: not-a-valid-url.onion",
"created_at": 1234567891,
},
{
"id": "event3",
"content": "No URLs here, just text",
"created_at": 1234567892,
},
]
with patch(
"router.discovery.query_nostr_relay_for_providers", return_value=mock_events
):
with patch("router.discovery.fetch_provider_health") as mock_fetch:
mock_fetch.return_value = {"status_code": 200, "json": {"status": "online"}}
response = await integration_client.get("/v1/providers/")
assert response.status_code == 200
data = response.json()
# Should only extract valid onion URLs
providers = data["providers"]
for provider in providers:
assert provider.startswith("http://") or provider.startswith("https://")
assert provider.endswith(".onion")
@pytest.mark.integration
@pytest.mark.asyncio
async def test_providers_endpoint_response_format(
integration_client: AsyncClient,
) -> None:
"""Test providers endpoint response format consistency"""
mock_events: list[dict[str, Any]] = [
{
"id": "event1",
"content": "Provider: http://test-provider.onion",
"created_at": 1234567890,
}
]
with patch(
"router.discovery.query_nostr_relay_for_providers", return_value=mock_events
):
with patch("router.discovery.fetch_provider_health") as mock_fetch:
mock_fetch.return_value = {"status_code": 200, "json": {"status": "online"}}
# Test default format
response = await integration_client.get("/v1/providers/")
assert response.status_code == 200
validator = ResponseValidator()
validation = validator.validate_success_response(
response, expected_status=200, required_fields=["providers"]
)
assert validation["valid"]
data = response.json()
assert isinstance(data, dict)
assert "providers" in data
assert isinstance(data["providers"], list)
# Test include_json format
response_json = await integration_client.get(
"/v1/providers/?include_json=true"
)
assert response_json.status_code == 200
data_json = response_json.json()
assert isinstance(data_json, dict)
assert "providers" in data_json
assert isinstance(data_json["providers"], list)
@pytest.mark.integration
@pytest.mark.asyncio
async def test_providers_endpoint_performance(integration_client: AsyncClient) -> None:
"""Test providers endpoint meets performance requirements"""
# Mock quick responses to avoid network delays
mock_events: list[dict[str, Any]] = [
{
"id": f"event{i}",
"content": f"Provider: http://provider{i}.onion",
"created_at": 1234567890 + i,
}
for i in range(5)
]
validator = PerformanceValidator()
with patch(
"router.discovery.query_nostr_relay_for_providers", return_value=mock_events
):
with patch("router.discovery.fetch_provider_health") as mock_fetch:
mock_fetch.return_value = {"status_code": 200, "json": {"status": "online"}}
# Test multiple requests
for i in range(10):
start = validator.start_timing("providers_endpoint")
response = await integration_client.get("/v1/providers/")
validator.end_timing("providers_endpoint", start)
assert response.status_code == 200
# Validate performance (should be fast with mocked dependencies)
perf_result = validator.validate_response_time(
"providers_endpoint",
max_duration=2.0, # Allow more time since it involves multiple operations
percentile=0.95,
)
assert perf_result["valid"], f"Performance requirement failed: {perf_result}"
@pytest.mark.integration
@pytest.mark.asyncio
async def test_providers_endpoint_concurrent_requests(
integration_client: AsyncClient,
) -> None:
"""Test providers endpoint handles concurrent requests correctly"""
from .utils import ConcurrencyTester
mock_events: list[dict[str, Any]] = [
{
"id": "event1",
"content": "Provider: http://concurrent-provider.onion",
"created_at": 1234567890,
}
]
with patch(
"router.discovery.query_nostr_relay_for_providers", return_value=mock_events
):
with patch("router.discovery.fetch_provider_health") as mock_fetch:
mock_fetch.return_value = {"status_code": 200, "json": {"status": "online"}}
# Create concurrent requests
requests = [{"method": "GET", "url": "/v1/providers/"} for _ in range(10)]
tester = ConcurrencyTester()
responses = await tester.run_concurrent_requests(
integration_client, requests, max_concurrent=5
)
# All should succeed
for response in responses:
assert response.status_code == 200
data = response.json()
assert "providers" in data
@pytest.mark.integration
@pytest.mark.asyncio
async def test_providers_endpoint_parameter_validation(
integration_client: AsyncClient,
) -> None:
"""Test providers endpoint parameter handling"""
mock_events: list[dict[str, Any]] = [
{
"id": "event1",
"content": "Provider: http://param-test-provider.onion",
"created_at": 1234567890,
}
]
with patch(
"router.discovery.query_nostr_relay_for_providers", return_value=mock_events
):
with patch("router.discovery.fetch_provider_health") as mock_fetch:
mock_fetch.return_value = {"status_code": 200, "json": {"status": "online"}}
# Test various parameter values
test_cases = [
("/v1/providers/", False), # Default
("/v1/providers/?include_json=false", False), # Explicit false
("/v1/providers/?include_json=true", True), # Explicit true
("/v1/providers/?include_json=1", True), # Truthy value
("/v1/providers/?include_json=0", False), # Falsy value
]
for url, expected_json_format in test_cases:
response = await integration_client.get(url)
assert response.status_code == 200
data = response.json()
providers = data["providers"]
if len(providers) > 0:
if expected_json_format:
# Should be list of dictionaries
for provider in providers:
assert isinstance(provider, dict)
else:
# Should be list of strings
for provider in providers:
assert isinstance(provider, str)
@pytest.mark.integration
@pytest.mark.asyncio
async def test_no_database_changes_during_provider_operations(
integration_client: AsyncClient, db_snapshot: Any
) -> None:
"""Comprehensive test that provider operations don't modify database state"""
# Capture initial state
await db_snapshot.capture()
mock_events: list[dict[str, Any]] = [
{
"id": "event1",
"content": "Provider: http://no-db-change-provider.onion",
"created_at": 1234567890,
}
]
with patch(
"router.discovery.query_nostr_relay_for_providers", return_value=mock_events
):
with patch("router.discovery.fetch_provider_health") as mock_fetch:
mock_fetch.return_value = {"status_code": 200, "json": {"status": "online"}}
# Make multiple requests with different parameters
endpoints = [
"/v1/providers/",
"/v1/providers/?include_json=true",
"/v1/providers/?include_json=false",
]
for endpoint in endpoints:
response = await integration_client.get(endpoint)
assert response.status_code == 200
# Check no database changes after each request
current_diff = await db_snapshot.diff()
assert len(current_diff["api_keys"]["added"]) == 0
assert len(current_diff["api_keys"]["modified"]) == 0
assert len(current_diff["api_keys"]["removed"]) == 0
# Final verification - database state should be identical
final_diff = await db_snapshot.diff()
assert final_diff["api_keys"]["added"] == []
assert final_diff["api_keys"]["modified"] == []
assert final_diff["api_keys"]["removed"] == []