mirror of
https://github.com/Routstr/routstr-core.git
synced 2026-07-30 23:36:15 +00:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
50eabafa57 |
@@ -59,7 +59,7 @@ class Settings(BaseSettings):
|
||||
cors_origins: list[str] = Field(default_factory=lambda: ["*"], env="CORS_ORIGINS")
|
||||
tor_proxy_url: str = Field(default="socks5://127.0.0.1:9050", env="TOR_PROXY_URL")
|
||||
providers_refresh_interval_seconds: int = Field(
|
||||
default=0, env="PROVIDERS_REFRESH_INTERVAL_SECONDS"
|
||||
default=300, env="PROVIDERS_REFRESH_INTERVAL_SECONDS"
|
||||
)
|
||||
pricing_refresh_interval_seconds: int = Field(
|
||||
default=120, env="PRICING_REFRESH_INTERVAL_SECONDS"
|
||||
|
||||
@@ -6,7 +6,7 @@ from typing import Any
|
||||
|
||||
import httpx
|
||||
import websockets
|
||||
from fastapi import APIRouter, HTTPException
|
||||
from fastapi import APIRouter
|
||||
|
||||
from .core.logging import get_logger
|
||||
from .core.settings import settings
|
||||
@@ -389,9 +389,6 @@ async def get_providers(
|
||||
Return cached providers. If include_json, return provider+health; otherwise provider only.
|
||||
Optional filter by pubkey.
|
||||
"""
|
||||
if settings.providers_refresh_interval_seconds == 0:
|
||||
raise HTTPException(status_code=404, detail="Provider discovery is disabled")
|
||||
|
||||
cache = await get_cache()
|
||||
if not cache:
|
||||
await refresh_providers_cache(pubkey=pubkey)
|
||||
|
||||
@@ -10,7 +10,6 @@ from httpx import AsyncClient
|
||||
|
||||
from .utils import (
|
||||
CashuTokenGenerator,
|
||||
PerformanceValidator,
|
||||
ResponseValidator,
|
||||
)
|
||||
|
||||
@@ -159,30 +158,7 @@ async def test_error_handling(
|
||||
assert response.status_code == 401
|
||||
|
||||
|
||||
@pytest.mark.integration
|
||||
@pytest.mark.asyncio
|
||||
async def test_performance_requirements(integration_client: AsyncClient) -> None:
|
||||
"""Test that endpoints meet performance requirements"""
|
||||
|
||||
validator = PerformanceValidator()
|
||||
|
||||
# Test info endpoint performance
|
||||
for i in range(50):
|
||||
start = validator.start_timing("info_endpoint")
|
||||
response = await integration_client.get("/")
|
||||
validator.end_timing("info_endpoint", start)
|
||||
assert response.status_code == 200
|
||||
|
||||
# Validate 95th percentile is under 500ms
|
||||
result = validator.validate_response_time(
|
||||
"info_endpoint", max_duration=0.5, percentile=0.95
|
||||
)
|
||||
|
||||
assert result["valid"], (
|
||||
f"Performance requirement failed: "
|
||||
f"95th percentile was {result['percentile_time']:.3f}s "
|
||||
f"(required < {result['max_allowed']}s)"
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.integration
|
||||
|
||||
@@ -9,7 +9,6 @@ import gc
|
||||
import statistics
|
||||
import time
|
||||
from typing import Any, Dict, List
|
||||
from unittest.mock import patch
|
||||
|
||||
import psutil
|
||||
import pytest
|
||||
@@ -106,46 +105,42 @@ class TestPerformanceBaseline:
|
||||
("GET", "/v1/wallet/info", authenticated_client, None),
|
||||
]
|
||||
|
||||
# Enable provider discovery for this test
|
||||
with patch(
|
||||
"routstr.core.settings.settings.providers_refresh_interval_seconds", 300
|
||||
):
|
||||
# Warm up
|
||||
for _ in range(10):
|
||||
await integration_client.get("/")
|
||||
# Warm up
|
||||
for _ in range(10):
|
||||
await integration_client.get("/")
|
||||
|
||||
# Test each endpoint
|
||||
for method, path, client, data in endpoints:
|
||||
response_times = []
|
||||
# Test each endpoint
|
||||
for method, path, client, data in endpoints:
|
||||
response_times = []
|
||||
|
||||
for i in range(100):
|
||||
start = time.time()
|
||||
for i in range(100):
|
||||
start = time.time()
|
||||
|
||||
if method == "GET":
|
||||
response = await client.get(path)
|
||||
else:
|
||||
response = await client.post(path, json=data)
|
||||
if method == "GET":
|
||||
response = await client.get(path)
|
||||
else:
|
||||
response = await client.post(path, json=data)
|
||||
|
||||
duration = time.time() - start
|
||||
response_times.append(duration * 1000) # Convert to ms
|
||||
duration = time.time() - start
|
||||
response_times.append(duration * 1000) # Convert to ms
|
||||
|
||||
assert response.status_code in [200, 201]
|
||||
assert response.status_code in [200, 201]
|
||||
|
||||
if i % 10 == 0:
|
||||
metrics.record_system_metrics()
|
||||
if i % 10 == 0:
|
||||
metrics.record_system_metrics()
|
||||
|
||||
# Verify 95th percentile < 500ms
|
||||
p95 = sorted(response_times)[int(len(response_times) * 0.95)]
|
||||
assert p95 < 500, (
|
||||
f"{method} {path} p95 response time {p95}ms exceeds 500ms limit"
|
||||
)
|
||||
# Verify 95th percentile < 500ms
|
||||
p95 = sorted(response_times)[int(len(response_times) * 0.95)]
|
||||
assert p95 < 500, (
|
||||
f"{method} {path} p95 response time {p95}ms exceeds 500ms limit"
|
||||
)
|
||||
|
||||
print(f"\n{method} {path}:")
|
||||
print(f" Mean: {statistics.mean(response_times):.2f}ms")
|
||||
print(f" P95: {p95:.2f}ms")
|
||||
print(
|
||||
f" P99: {sorted(response_times)[int(len(response_times) * 0.99)]:.2f}ms"
|
||||
)
|
||||
print(f"\n{method} {path}:")
|
||||
print(f" Mean: {statistics.mean(response_times):.2f}ms")
|
||||
print(f" P95: {p95:.2f}ms")
|
||||
print(
|
||||
f" P99: {sorted(response_times)[int(len(response_times) * 0.99)]:.2f}ms"
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.integration
|
||||
|
||||
@@ -3,7 +3,7 @@ Integration tests for provider management functionality.
|
||||
Tests GET /v1/providers/ endpoint for listing and managing providers.
|
||||
"""
|
||||
|
||||
from typing import Any, Generator
|
||||
from typing import Any
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
@@ -11,7 +11,7 @@ from httpx import AsyncClient
|
||||
|
||||
from routstr.discovery import _PROVIDERS_CACHE
|
||||
|
||||
from .utils import PerformanceValidator, ResponseValidator
|
||||
from .utils import ResponseValidator
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
@@ -19,15 +19,6 @@ 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(
|
||||
@@ -527,46 +518,6 @@ async def test_providers_endpoint_response_format(
|
||||
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(
|
||||
"routstr.discovery.query_nostr_relay_for_providers", return_value=mock_events
|
||||
):
|
||||
with patch("routstr.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(
|
||||
|
||||
@@ -18,7 +18,6 @@ from routstr.core.db import ApiKey
|
||||
|
||||
from .utils import (
|
||||
ConcurrencyTester,
|
||||
PerformanceValidator,
|
||||
)
|
||||
|
||||
|
||||
@@ -551,39 +550,7 @@ async def test_proxy_get_concurrent_requests(
|
||||
assert response.status_code == 200
|
||||
|
||||
|
||||
@pytest.mark.integration
|
||||
@pytest.mark.asyncio
|
||||
async def test_proxy_get_performance_requirements(
|
||||
integration_client: AsyncClient, authenticated_client: AsyncClient
|
||||
) -> None:
|
||||
"""Test that GET proxy requests meet performance requirements"""
|
||||
|
||||
validator = PerformanceValidator()
|
||||
|
||||
with patch("httpx.AsyncClient.request") as mock_request:
|
||||
mock_response = AsyncMock()
|
||||
mock_response.status_code = 200
|
||||
mock_response.headers = {"content-type": "application/json"}
|
||||
mock_response.json = MagicMock(return_value={"performance": "test"})
|
||||
mock_response.text = '{"performance": "test"}'
|
||||
mock_response.iter_bytes = AsyncMock(return_value=[b'{"performance": "test"}'])
|
||||
mock_request.return_value = mock_response
|
||||
|
||||
# Test multiple requests for performance measurement
|
||||
for i in range(20):
|
||||
start = validator.start_timing("proxy_get")
|
||||
response = await authenticated_client.get(f"/v1/perf-test-{i}")
|
||||
validator.end_timing("proxy_get", start)
|
||||
|
||||
assert response.status_code == 200
|
||||
|
||||
# Validate performance requirements
|
||||
perf_result = validator.validate_response_time(
|
||||
"proxy_get",
|
||||
max_duration=1.0, # Should complete within 1 second
|
||||
percentile=0.95,
|
||||
)
|
||||
assert perf_result["valid"], f"Performance requirement failed: {perf_result}"
|
||||
|
||||
|
||||
@pytest.mark.integration
|
||||
|
||||
@@ -15,7 +15,6 @@ from httpx import ASGITransport, AsyncClient
|
||||
|
||||
from .utils import (
|
||||
ConcurrencyTester,
|
||||
PerformanceValidator,
|
||||
)
|
||||
|
||||
|
||||
@@ -290,55 +289,7 @@ async def test_proxy_post_unauthorized_access(integration_client: AsyncClient) -
|
||||
assert response.status_code in [400, 401]
|
||||
|
||||
|
||||
@pytest.mark.integration
|
||||
@pytest.mark.asyncio
|
||||
async def test_proxy_post_performance(
|
||||
integration_client: AsyncClient, authenticated_client: AsyncClient
|
||||
) -> None:
|
||||
"""Test POST endpoint performance requirements"""
|
||||
|
||||
test_payload = {
|
||||
"model": "gpt-3.5-turbo",
|
||||
"messages": [{"role": "user", "content": "Performance test"}],
|
||||
}
|
||||
|
||||
validator = PerformanceValidator()
|
||||
|
||||
with patch("httpx.AsyncClient.send") as mock_send:
|
||||
# Mock fast responses
|
||||
async def mock_iter_bytes(*args: Any, **kwargs: Any) -> Any:
|
||||
yield b'{"choices": [{"message": {"content": "Fast"}}], "usage": {"total_tokens": 5}}'
|
||||
|
||||
mock_response = AsyncMock()
|
||||
mock_response.status_code = 200
|
||||
mock_response.headers = {"content-type": "application/json"}
|
||||
response_data = {
|
||||
"choices": [{"message": {"content": "Fast"}}],
|
||||
"usage": {"total_tokens": 5},
|
||||
}
|
||||
mock_response.text = json.dumps(response_data)
|
||||
mock_response.json = AsyncMock(return_value=response_data)
|
||||
mock_response.iter_bytes = mock_iter_bytes
|
||||
mock_response.aiter_bytes = mock_iter_bytes
|
||||
mock_send.return_value = mock_response
|
||||
|
||||
# Run multiple requests for performance measurement
|
||||
for i in range(20):
|
||||
start = validator.start_timing("proxy_post")
|
||||
response = await authenticated_client.post(
|
||||
"/v1/chat/completions", json=test_payload
|
||||
)
|
||||
validator.end_timing("proxy_post", start)
|
||||
|
||||
assert response.status_code == 200
|
||||
|
||||
# Validate performance
|
||||
perf_result = validator.validate_response_time(
|
||||
"proxy_post",
|
||||
max_duration=1.5, # Allow slightly more time for POST
|
||||
percentile=0.95,
|
||||
)
|
||||
assert perf_result["valid"], f"Performance requirement failed: {perf_result}"
|
||||
|
||||
|
||||
@pytest.mark.integration
|
||||
|
||||
@@ -3,7 +3,7 @@ Integration tests for wallet information retrieval endpoints.
|
||||
Tests GET /v1/wallet/ and GET /v1/wallet/info endpoints with various scenarios.
|
||||
"""
|
||||
|
||||
import time
|
||||
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Any
|
||||
|
||||
@@ -367,30 +367,4 @@ async def test_wallet_info_with_special_characters_in_headers(
|
||||
# Note: Current implementation doesn't return refund_address in response
|
||||
|
||||
|
||||
@pytest.mark.integration
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.slow
|
||||
async def test_wallet_endpoints_performance(authenticated_client: AsyncClient) -> None:
|
||||
"""Test wallet endpoints meet performance requirements"""
|
||||
|
||||
# Warm up
|
||||
await authenticated_client.get("/v1/wallet/")
|
||||
|
||||
# Measure response times
|
||||
response_times = []
|
||||
|
||||
for _ in range(50):
|
||||
start_time = time.time()
|
||||
response = await authenticated_client.get("/v1/wallet/")
|
||||
end_time = time.time()
|
||||
|
||||
assert response.status_code == 200
|
||||
response_times.append(end_time - start_time)
|
||||
|
||||
# Calculate statistics
|
||||
avg_time = sum(response_times) / len(response_times)
|
||||
max_time = max(response_times)
|
||||
|
||||
# Performance assertions
|
||||
assert avg_time < 0.1 # Average should be under 100ms
|
||||
assert max_time < 0.5 # No request should take more than 500ms
|
||||
|
||||
@@ -537,42 +537,4 @@ async def test_refund_with_expired_key(
|
||||
assert response.json()["recipient"] == "expired@ln.address"
|
||||
|
||||
|
||||
@pytest.mark.integration
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.slow
|
||||
async def test_refund_performance(
|
||||
integration_client: AsyncClient, testmint_wallet: Any
|
||||
) -> None:
|
||||
"""Test refund endpoint performance"""
|
||||
|
||||
import time
|
||||
|
||||
# Create multiple API keys
|
||||
api_keys = []
|
||||
for i in range(10):
|
||||
token = await testmint_wallet.mint_tokens(100 + i)
|
||||
# Use cashu token as Bearer auth to create API key
|
||||
integration_client.headers["Authorization"] = f"Bearer {token}"
|
||||
response = await integration_client.get("/v1/wallet/info")
|
||||
assert response.status_code == 200
|
||||
api_keys.append(response.json()["api_key"])
|
||||
|
||||
# Measure refund times
|
||||
refund_times = []
|
||||
|
||||
for api_key in api_keys:
|
||||
integration_client.headers["Authorization"] = f"Bearer {api_key}"
|
||||
|
||||
start_time = time.time()
|
||||
response = await integration_client.post("/v1/wallet/refund")
|
||||
end_time = time.time()
|
||||
|
||||
assert response.status_code == 200
|
||||
refund_times.append(end_time - start_time)
|
||||
|
||||
# Performance assertions
|
||||
avg_time = sum(refund_times) / len(refund_times)
|
||||
max_time = max(refund_times)
|
||||
|
||||
assert avg_time < 0.5 # Average under 500ms
|
||||
assert max_time < 1.0 # No refund takes more than 1 second
|
||||
|
||||
Reference in New Issue
Block a user