mirror of
https://github.com/Routstr/routstr-core.git
synced 2026-08-09 02:54:37 +00:00
770 lines
26 KiB
Python
770 lines
26 KiB
Python
"""
|
|
Integration tests for provider management functionality.
|
|
Tests GET /v1/providers/ endpoint for listing and managing providers.
|
|
"""
|
|
|
|
import time
|
|
from types import TracebackType
|
|
from typing import Any, Generator
|
|
from unittest.mock import patch
|
|
|
|
import pytest
|
|
from httpx import AsyncClient
|
|
|
|
from routstr.core.admin import admin_sessions
|
|
from routstr.core.db import UpstreamProviderRow
|
|
from routstr.nostr.discovery import _PROVIDERS_CACHE
|
|
|
|
from .utils import ResponseValidator
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def _clear_providers_cache() -> None:
|
|
_PROVIDERS_CACHE.clear()
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def _enable_provider_discovery() -> Generator[None, Any, Any]:
|
|
"""Enable provider discovery for all tests in this module"""
|
|
with patch(
|
|
"routstr.core.settings.settings.providers_refresh_interval_seconds", 300
|
|
):
|
|
yield
|
|
|
|
|
|
@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",
|
|
"pubkey": "test_pubkey1",
|
|
"kind": 38421, # NIP-91 event kind
|
|
"created_at": 1234567890,
|
|
"content": '{"name": "Provider 1", "about": "Test provider 1"}',
|
|
"tags": [
|
|
["d", "provider1"],
|
|
["u", "http://provider1.onion"],
|
|
],
|
|
},
|
|
{
|
|
"id": "event2",
|
|
"pubkey": "test_pubkey2",
|
|
"kind": 38421, # NIP-91 event kind
|
|
"created_at": 1234567891,
|
|
"content": '{"name": "Provider 2", "about": "Test provider 2"}',
|
|
"tags": [
|
|
["d", "provider2"],
|
|
["u", "http://provider2.onion"],
|
|
],
|
|
},
|
|
]
|
|
|
|
# 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(
|
|
"routstr.nostr.discovery.query_nostr_relay_for_providers",
|
|
return_value=mock_events,
|
|
):
|
|
with patch("routstr.nostr.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 objects
|
|
for provider in data["providers"]:
|
|
assert isinstance(provider, dict)
|
|
assert "endpoint_url" in provider
|
|
assert provider["endpoint_url"].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",
|
|
"pubkey": "test_pubkey",
|
|
"kind": 38421, # NIP-91 event kind
|
|
"created_at": 1234567890,
|
|
"content": '{"name": "Test Provider", "about": "A test provider"}',
|
|
"tags": [
|
|
["d", "test-provider"],
|
|
["u", "http://test-provider.onion"],
|
|
],
|
|
}
|
|
]
|
|
|
|
# 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(
|
|
"routstr.nostr.discovery.query_nostr_relay_for_providers",
|
|
return_value=mock_events,
|
|
):
|
|
with patch("routstr.nostr.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 with provider and health info
|
|
for provider_data in data["providers"]:
|
|
assert isinstance(provider_data, dict)
|
|
# Each provider should have 'provider' and 'health' keys
|
|
assert "provider" in provider_data
|
|
assert "health" in provider_data
|
|
|
|
provider_info = provider_data["provider"]
|
|
assert "endpoint_url" in provider_info
|
|
assert provider_info["endpoint_url"].endswith(".onion")
|
|
|
|
health_info = provider_data["health"]
|
|
assert isinstance(health_info, dict)
|
|
assert "status_code" in health_info
|
|
|
|
# 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 NIP-91 provider announcement event
|
|
mock_events: list[dict[str, Any]] = [
|
|
{
|
|
"id": "event1",
|
|
"pubkey": "test_pubkey",
|
|
"created_at": 1234567890,
|
|
"kind": 38421, # NIP-91 event kind
|
|
"content": '{"name": "Comprehensive Provider", "about": "A comprehensive AI provider"}',
|
|
"tags": [
|
|
["d", "provider-123"],
|
|
["u", "https://api.provider.example/v1"],
|
|
["models", "gpt-3.5-turbo", "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(
|
|
"routstr.nostr.discovery.query_nostr_relay_for_providers",
|
|
return_value=mock_events,
|
|
):
|
|
with patch("routstr.nostr.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"]
|
|
# health_info = provider_data["health"]
|
|
|
|
# Expected fields from NIP-91 parser (supported_models removed)
|
|
expected_fields = ["id", "name", "endpoint_url"]
|
|
for field in expected_fields:
|
|
assert field in provider_info
|
|
|
|
|
|
@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"""
|
|
|
|
# Force empty discovery by returning events that are filtered out
|
|
mock_events: list[dict[str, Any]] = [
|
|
{
|
|
"id": "localhost-event",
|
|
"pubkey": "ignored_pubkey",
|
|
"kind": 38421,
|
|
"created_at": 1234567899,
|
|
"content": '{"name": "Local"}',
|
|
"tags": [["d", "local"], ["u", "http://localhost:8000"]],
|
|
}
|
|
]
|
|
|
|
with patch(
|
|
"routstr.nostr.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",
|
|
"kind": 38421, # NIP-91 event kind
|
|
"created_at": 1234567890,
|
|
"content": '{"name": "Healthy Provider", "about": "Healthy provider announcement"}',
|
|
"tags": [
|
|
["d", "healthy-provider"],
|
|
["u", "http://healthy-provider.onion"],
|
|
],
|
|
},
|
|
{
|
|
"id": "event2",
|
|
"pubkey": "offline_provider_pubkey",
|
|
"kind": 38421, # NIP-91 event kind
|
|
"created_at": 1234567891,
|
|
"content": '{"name": "Offline Provider", "about": "Offline provider announcement"}',
|
|
"tags": [
|
|
["d", "offline-provider"],
|
|
["u", "http://offline-provider.onion"],
|
|
],
|
|
},
|
|
]
|
|
|
|
# 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(
|
|
"routstr.nostr.discovery.query_nostr_relay_for_providers",
|
|
return_value=mock_events,
|
|
):
|
|
with patch(
|
|
"routstr.nostr.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",
|
|
"kind": 38421, # NIP-91 event kind
|
|
"created_at": 1234567890,
|
|
"content": '{"name": "Provider", "about": "Provider announcement"}',
|
|
"tags": [
|
|
["d", "provider-1"],
|
|
["u", "http://provider.onion"],
|
|
],
|
|
},
|
|
{
|
|
"id": "event2",
|
|
"pubkey": "other_provider_pubkey",
|
|
"kind": 38421, # NIP-91 event kind
|
|
"created_at": 1234567892,
|
|
"content": '{"name": "Other Provider", "about": "Different provider announcement"}',
|
|
"tags": [
|
|
["d", "other-provider"],
|
|
["u", "http://other-provider.onion"],
|
|
],
|
|
},
|
|
]
|
|
|
|
with patch(
|
|
"routstr.nostr.discovery.query_nostr_relay_for_providers",
|
|
return_value=mock_events,
|
|
):
|
|
with patch("routstr.nostr.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(
|
|
"routstr.nostr.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(
|
|
"routstr.nostr.discovery.query_nostr_relay_for_providers",
|
|
return_value=mock_events,
|
|
):
|
|
with patch("routstr.nostr.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()
|
|
|
|
# With NIP-91-only parsing, events without required tags are ignored
|
|
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_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(
|
|
"routstr.nostr.discovery.query_nostr_relay_for_providers",
|
|
return_value=mock_events,
|
|
):
|
|
with patch("routstr.nostr.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_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(
|
|
"routstr.nostr.discovery.query_nostr_relay_for_providers",
|
|
return_value=mock_events,
|
|
):
|
|
with patch("routstr.nostr.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",
|
|
"pubkey": "param_pubkey",
|
|
"kind": 38421,
|
|
"created_at": 1234567890,
|
|
"content": '{"name": "Param Test Provider"}',
|
|
"tags": [
|
|
["d", "param-test-provider"],
|
|
["u", "http://param-test-provider.onion"],
|
|
],
|
|
}
|
|
]
|
|
|
|
with patch(
|
|
"routstr.nostr.discovery.query_nostr_relay_for_providers",
|
|
return_value=mock_events,
|
|
):
|
|
with patch("routstr.nostr.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 {provider, health} dictionaries
|
|
for item in providers:
|
|
assert isinstance(item, dict)
|
|
assert "provider" in item and "health" in item
|
|
else:
|
|
# Should be list of provider objects
|
|
for provider in providers:
|
|
assert isinstance(provider, dict)
|
|
|
|
|
|
@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(
|
|
"routstr.nostr.discovery.query_nostr_relay_for_providers",
|
|
return_value=mock_events,
|
|
):
|
|
with patch("routstr.nostr.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"] == []
|
|
|
|
|
|
@pytest.mark.integration
|
|
@pytest.mark.asyncio
|
|
async def test_admin_routstr_topup_retries_transient_upstream_failure(
|
|
integration_client: AsyncClient,
|
|
integration_session: Any,
|
|
) -> None:
|
|
admin_token = "test-admin-token"
|
|
admin_sessions[admin_token] = int(time.time()) + 3600
|
|
integration_client.headers["Authorization"] = f"Bearer {admin_token}"
|
|
|
|
provider = UpstreamProviderRow(
|
|
provider_type="routstr",
|
|
base_url="https://node.example",
|
|
api_key="sk-upstream-test",
|
|
enabled=True,
|
|
provider_fee=1.01,
|
|
)
|
|
integration_session.add(provider)
|
|
await integration_session.commit()
|
|
await integration_session.refresh(provider)
|
|
|
|
class MockResponse:
|
|
def __init__(self, status_code: int, data: dict[str, Any] | None = None):
|
|
self.status_code = status_code
|
|
self._data = data or {}
|
|
self.text = str(self._data)
|
|
|
|
def json(self) -> dict[str, Any]:
|
|
return self._data
|
|
|
|
class MockAsyncClient:
|
|
def __init__(self) -> None:
|
|
self.calls = 0
|
|
|
|
async def __aenter__(self) -> "MockAsyncClient":
|
|
return self
|
|
|
|
async def __aexit__(
|
|
self,
|
|
exc_type: type[BaseException] | None,
|
|
exc: BaseException | None,
|
|
tb: TracebackType | None,
|
|
) -> None:
|
|
return None
|
|
|
|
async def post(
|
|
self, url: str, json: dict[str, Any], headers: dict[str, str]
|
|
) -> MockResponse:
|
|
self.calls += 1
|
|
assert url == "https://node.example/v1/balance/lightning/invoice"
|
|
assert json["amount_sats"] == 10
|
|
assert json["purpose"] == "topup"
|
|
assert json["api_key"] == "sk-upstream-test"
|
|
assert headers["Authorization"] == "Bearer sk-upstream-test"
|
|
|
|
if self.calls == 1:
|
|
return MockResponse(500, {"detail": "warmup failure"})
|
|
|
|
return MockResponse(
|
|
200,
|
|
{
|
|
"bolt11": "lnbc1testinvoice",
|
|
"invoice_id": "invoice-123",
|
|
},
|
|
)
|
|
|
|
mock_client = MockAsyncClient()
|
|
|
|
try:
|
|
with patch("httpx.AsyncClient", return_value=mock_client):
|
|
response = await integration_client.post(
|
|
f"/admin/api/upstream-providers/{provider.id}/topup",
|
|
json={"amount": 10},
|
|
)
|
|
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert data["ok"] is True
|
|
assert data["topup_data"]["payment_request"] == "lnbc1testinvoice"
|
|
assert data["topup_data"]["invoice_id"] == "invoice-123"
|
|
assert mock_client.calls == 2
|
|
finally:
|
|
admin_sessions.pop(admin_token, None)
|