Merge pull request #117 from Routstr/chdir-router-routstr

change dir 'router' -> 'routstr'
This commit is contained in:
shroominic
2025-08-10 23:38:14 -03:00
committed by GitHub
43 changed files with 193 additions and 194 deletions
+2 -1
View File
@@ -12,6 +12,7 @@ RUN apk add --no-cache \
RUN apk add git
COPY uv.lock pyproject.toml ./
COPY routstr ./routstr
RUN uv add git+https://github.com/saschanaz/secp256k1-py.git#branch=upgrade060
# RUN uv sync
@@ -25,4 +26,4 @@ ENV PYTHONUNBUFFERED=1
EXPOSE 8000
CMD ["/.venv/bin/fastapi", "run", "router", "--host", "0.0.0.0"]
CMD ["/.venv/bin/fastapi", "run", "routstr", "--host", "0.0.0.0"]
+4 -4
View File
@@ -92,7 +92,7 @@ docker-down:
lint:
@echo "🔍 Running linting checks..."
$(RUFF) check .
$(MYPY) router/ --ignore-missing-imports
$(MYPY) routstr/ --ignore-missing-imports
format:
@echo "✨ Formatting code..."
@@ -101,7 +101,7 @@ format:
type-check:
@echo "🔎 Running type checks..."
$(MYPY) router/ --ignore-missing-imports
$(MYPY) routstr/ --ignore-missing-imports
# Development setup
dev-setup:
@@ -209,7 +209,7 @@ db-clean:
# Advanced testing options
test-coverage:
@echo "📊 Running tests with coverage..."
$(PYTEST) --cov=router --cov-report=html --cov-report=term
$(PYTEST) --cov=routstr --cov-report=html --cov-report=term
@echo "Coverage report generated in htmlcov/"
test-watch:
@@ -228,7 +228,7 @@ ci-test:
ci-lint:
@echo "🤖 Running CI linting..."
$(RUFF) check . --exit-non-zero-on-fix
$(MYPY) router/ --ignore-missing-imports --no-error-summary
$(MYPY) routstr/ --ignore-missing-imports --no-error-summary
# Debug helpers
test-debug:
+1 -1
View File
@@ -69,7 +69,7 @@ cp .env.example .env
### Running Locally
```bash
fastapi run router --host 0.0.0.0 --port 8000
fastapi run routstr --host 0.0.0.0 --port 8000
```
The service forwards requests to `UPSTREAM_BASE_URL`. Supply the upstream API key via the `UPSTREAM_API_KEY` environment variable if required.
+2 -7
View File
@@ -1,9 +1,9 @@
version: '3.8'
services:
router:
routstr:
build: .
command: ["/.venv/bin/fastapi", "dev", "router", "--host", "0.0.0.0", "--port", "8000"]
command: ["/.venv/bin/fastapi", "dev", "routstr", "--host", "0.0.0.0", "--port", "8000"]
ports:
- "8000:8000"
environment:
@@ -27,9 +27,6 @@ services:
- "REFUND_PROCESSING_INTERVAL=3600"
- "MINIMUM_PAYOUT=1000"
- "PAYOUT_INTERVAL=86400"
volumes:
- ./:/app
- ./logs:/app/logs
depends_on:
- mock-mint
- mock-openai
@@ -40,8 +37,6 @@ services:
restart: unless-stopped
ports:
- "8088:8080" # host:container
volumes:
- ./relay-data:/usr/src/app/db
environment:
- LISTEN_ADDR=0.0.0.0
- LISTEN_PORT=8080
+3 -3
View File
@@ -1,7 +1,7 @@
version: '3.8'
services:
router:
routstr:
build: .
volumes:
- .:/app
@@ -21,9 +21,9 @@ services:
- tor-data:/var/lib/tor
environment:
# Format: HS_<NAME>=<TARGET_HOST>:<TARGET_PORT>:<VIRTUAL_PORT>
- HS_ROUTER=router:8000:80
- HS_ROUTER=routstr:8000:80
depends_on:
- router
- routstr
volumes:
tor-data:
+2 -2
View File
@@ -9,10 +9,10 @@ from sqlalchemy.engine import Connection
from sqlalchemy.ext.asyncio import create_async_engine
from sqlmodel import SQLModel
# Add the parent directory to the Python path so we can import router modules
# Add the parent directory to the Python path so we can import routstr modules
sys.path.insert(0, str(pathlib.Path(__file__).resolve().parents[1]))
from router.core.db import DATABASE_URL
from routstr.core.db import DATABASE_URL
config = context.config
if config.config_file_name is None:
+8 -4
View File
@@ -58,6 +58,13 @@ markers = [
"performance: marks tests that measure performance metrics",
]
[build-system]
requires = ["setuptools", "wheel"]
build-backend = "setuptools.build_meta"
[tool.setuptools]
packages = ["routstr"]
[tool.ruff.lint]
select = ["E", "F", "I"]
ignore = ["E501"]
@@ -72,8 +79,5 @@ disallow_incomplete_defs = true
disallow_untyped_decorators = true
[tool.uv.sources]
secp256k1 = { git = "https://github.com/saschanaz/secp256k1-py", branch = "upgrade060" }
routstr = { workspace = true }
[tool.uv.workspace]
members = ["."]
secp256k1 = { git = "https://github.com/saschanaz/secp256k1-py", branch = "upgrade060" }
View File
@@ -89,7 +89,7 @@ def get_package_version() -> str:
return version
current_path = current_path.parent
# Fallback: try the simple path resolution (3 levels up for router/logging/logging_config.py)
# Fallback: try the simple path resolution (3 levels up for routstr/logging/logging_config.py)
pyproject_path = Path(__file__).parent.parent.parent / "pyproject.toml"
if pyproject_path.exists():
with open(pyproject_path, "rb") as f:
@@ -229,27 +229,22 @@ def setup_logging() -> None:
},
},
"loggers": {
"router": {
"routstr": {
"level": log_level,
"handlers": handlers,
"propagate": True,
},
"routstr.payment": {
"level": log_level,
"handlers": handlers,
"propagate": False,
},
"router.payment": {
"routstr.proxy": {
"level": log_level,
"handlers": handlers,
"propagate": False,
},
"router.cashu": {
"level": log_level,
"handlers": handlers,
"propagate": False,
},
"router.proxy": {
"level": log_level,
"handlers": handlers,
"propagate": False,
},
"router.auth": {
"routstr.auth": {
"level": log_level,
"handlers": handlers,
"propagate": False,
-19
View File
@@ -1,19 +0,0 @@
from setuptools import find_packages, setup
setup(
name="routstr",
version="0.1.0",
packages=find_packages(),
install_requires=[
"fastapi[standard]>=0.115",
"aiosqlite>=0.20",
"sqlmodel>=0.0.24",
"httpx[socks]>=0.25.2",
"greenlet>=3.2.1",
"python-json-logger>=2.0.0",
"cashu",
"secp256k1",
"marshmallow>=3.13,<4.0",
],
python_requires=">=3.11",
)
+21 -21
View File
@@ -11,7 +11,7 @@ from httpx import AsyncClient
from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine
from sqlmodel import select
from router.core.logging import get_logger
from routstr.core.logging import get_logger
logger = get_logger(__name__)
@@ -25,7 +25,7 @@ if use_local_services:
"DATABASE_URL": "sqlite+aiosqlite:///:memory:",
"UPSTREAM_BASE_URL": "http://localhost:3000", # Mock OpenAI service
"UPSTREAM_API_KEY": "test-upstream-key",
"CASHU_MINTS": "http://mint:3338", # Docker service name for router validation
"CASHU_MINTS": "http://mint:3338", # Docker service name for routstr validation
"MINT": "http://mint:3338",
"MINT_URL": "http://mint:3338",
"NOSTR_RELAY_URL": "ws://localhost:8088",
@@ -63,8 +63,8 @@ else:
# Set test environment variables before importing the app
os.environ.update(test_env)
from router.core.db import ApiKey, get_session # noqa: E402
from router.core.main import app, lifespan # noqa: E402
from routstr.core.db import ApiKey, get_session # noqa: E402
from routstr.core.main import app, lifespan # noqa: E402
@pytest.fixture(scope="session")
@@ -157,7 +157,7 @@ class TestmintWallet:
mint_response = await wallet.mint(amount=amount, hash=quote)
token = mint_response.token
# Replace connection URL with Docker service name for router validation
# Replace connection URL with Docker service name for routstr validation
if self.connection_url != self.mint_url:
token = token.replace(self.connection_url, self.mint_url)
@@ -251,7 +251,7 @@ class TestmintWallet:
async def send_token(
self, amount: int, unit: str, mint_url: Optional[str] = None
) -> str:
"""Send token with compatible signature for mocking router.wallet.send_token"""
"""Send token with compatible signature for mocking routstr.wallet.send_token"""
return await self.send(amount)
async def send_to_lnurl(self, lnurl: str, amount: int) -> int:
@@ -501,25 +501,25 @@ async def integration_app(
if use_real_mint:
# Use real mint - no wallet patches needed
with patch("router.core.db.engine", integration_engine):
with patch("routstr.core.db.engine", integration_engine):
yield test_app
else:
# Use testmint with wallet patches for all integration tests
mint_url = os.environ.get("CASHU_MINTS", "http://localhost:3338")
with (
patch("router.core.db.engine", integration_engine),
patch("router.wallet.TRUSTED_MINTS", [mint_url]),
patch("router.wallet.PRIMARY_MINT_URL", mint_url),
patch("router.auth.credit_balance", testmint_wallet.credit_balance),
patch("router.wallet.credit_balance", testmint_wallet.credit_balance),
patch("router.balance.credit_balance", testmint_wallet.credit_balance),
patch("router.wallet.send_token", testmint_wallet.send_token),
patch("router.balance.send_token", testmint_wallet.send_token),
patch("router.wallet.recieve_token", testmint_wallet.redeem_token),
patch("router.wallet.get_balance", testmint_wallet.get_balance),
patch("routstr.core.db.engine", integration_engine),
patch("routstr.wallet.TRUSTED_MINTS", [mint_url]),
patch("routstr.wallet.PRIMARY_MINT_URL", mint_url),
patch("routstr.auth.credit_balance", testmint_wallet.credit_balance),
patch("routstr.wallet.credit_balance", testmint_wallet.credit_balance),
patch("routstr.balance.credit_balance", testmint_wallet.credit_balance),
patch("routstr.wallet.send_token", testmint_wallet.send_token),
patch("routstr.balance.send_token", testmint_wallet.send_token),
patch("routstr.wallet.recieve_token", testmint_wallet.redeem_token),
patch("routstr.wallet.get_balance", testmint_wallet.get_balance),
patch("websockets.connect") as mock_websockets,
patch("router.payment.price.btc_usd_ask_price", return_value=50000.0),
patch("router.payment.price.sats_usd_ask_price", return_value=0.0005),
patch("routstr.payment.price.btc_usd_ask_price", return_value=50000.0),
patch("routstr.payment.price.sats_usd_ask_price", return_value=0.0005),
):
# Configure the WebSocket mock for discovery service - fast failure for performance tests
async def mock_websocket_connect(*args: Any, **kwargs: Any) -> None:
@@ -689,8 +689,8 @@ async def background_tasks_controller() -> AsyncGenerator[Any, None]:
original_periodic_payout: Optional[Callable] = None
try:
from router.payment.models import update_sats_pricing
from router.wallet import periodic_payout
from routstr.payment.models import update_sats_pricing
from routstr.wallet import periodic_payout
async def controlled_update_pricing() -> None:
while not controller.cancelled:
+1 -1
View File
@@ -139,7 +139,7 @@ async def main() -> None:
print("WARNING: Proxy server may not be running properly")
except Exception:
print("ERROR: Proxy server is not running!")
print("Please start the server with: uvicorn router.main:app")
print("Please start the server with: uvicorn routstr.main:app")
sys.exit(1)
# Run performance tests
+33 -29
View File
@@ -9,9 +9,9 @@ from unittest.mock import AsyncMock, patch
import pytest
from router.core.db import ApiKey
from router.payment.models import MODELS, Model, Pricing, update_sats_pricing
from router.wallet import periodic_payout
from routstr.core.db import ApiKey
from routstr.payment.models import MODELS, Model, Pricing, update_sats_pricing
from routstr.wallet import periodic_payout
@pytest.mark.asyncio
@@ -24,7 +24,7 @@ class TestPricingUpdateTask:
mock_sats_usd = 0.00002 # 1 sat = $0.00002 (BTC at $50,000)
with patch(
"router.payment.price.sats_usd_ask_price",
"routstr.payment.price.sats_usd_ask_price",
AsyncMock(return_value=mock_sats_usd),
):
# Create a test model
@@ -114,7 +114,7 @@ class TestPricingUpdateTask:
raise Exception("Price API error")
return 0.00002
with patch("router.payment.price.sats_usd_ask_price", mock_price_func):
with patch("routstr.payment.price.sats_usd_ask_price", mock_price_func):
# Test the retry behavior directly
# First call should fail
try:
@@ -122,7 +122,7 @@ class TestPricingUpdateTask:
assert False, "Expected exception on first call"
except Exception:
pass
# Second call should succeed
result = await mock_price_func()
assert result == 0.00002
@@ -165,7 +165,7 @@ class TestPricingUpdateTask:
try:
with patch(
"router.payment.price.sats_usd_ask_price",
"routstr.payment.price.sats_usd_ask_price",
AsyncMock(return_value=0.00002),
):
# Initialize pricing once to ensure consistent state
@@ -214,9 +214,9 @@ class TestRefundCheckTask:
# Mock the wallet send_to_lnurl method and get_session
with (
patch(
"router.wallet.send_to_lnurl", AsyncMock(return_value=5)
"routstr.wallet.send_to_lnurl", AsyncMock(return_value=5)
) as mock_send_to_lnurl,
patch("router.core.db.get_session") as mock_get_session,
patch("routstr.core.db.get_session") as mock_get_session,
):
# Make get_session return our integration session
async def get_test_session() -> Any:
@@ -279,9 +279,9 @@ class TestRefundCheckTask:
with (
patch(
"router.wallet.send_to_lnurl", mock_send_to_lnurl
"routstr.wallet.send_to_lnurl", mock_send_to_lnurl
) as mock_send_to_lnurl_patch,
patch("router.core.db.get_session") as mock_get_session,
patch("routstr.core.db.get_session") as mock_get_session,
):
# Make get_session return our integration session
async def get_test_session() -> Any:
@@ -366,9 +366,9 @@ class TestRefundCheckTask:
with (
patch(
"router.wallet.send_to_lnurl", AsyncMock(return_value=1)
"routstr.wallet.send_to_lnurl", AsyncMock(return_value=1)
) as mock_send_to_lnurl,
patch("router.core.db.get_session") as mock_get_session,
patch("routstr.core.db.get_session") as mock_get_session,
):
# Make get_session return our integration session
async def get_test_session() -> Any:
@@ -424,7 +424,7 @@ class TestRefundCheckTask:
# async def test_refund_check_disabled(self) -> None:
# """Test that refund check can be disabled by setting interval to 0"""
# # Patch the constant directly to disable refunds
# with patch.object(router.wallet, "REFUND_PROCESSING_INTERVAL", 0):
# with patch.object(routstr.wallet, "REFUND_PROCESSING_INTERVAL", 0):
# # Task should exit immediately
# task = asyncio.create_task(check_for_refunds())
# await task # Should complete without hanging
@@ -466,9 +466,9 @@ class TestPeriodicPayoutTask:
wallet_balance = 200000 # 200 sats total
with (
patch("router.wallet.get_balance", AsyncMock(return_value=wallet_balance)),
patch("routstr.wallet.get_balance", AsyncMock(return_value=wallet_balance)),
patch(
"router.wallet.send_to_lnurl", AsyncMock(return_value=None)
"routstr.wallet.send_to_lnurl", AsyncMock(return_value=None)
) as mock_send_to_lnurl,
):
# Mock environment variables
@@ -481,7 +481,7 @@ class TestPeriodicPayoutTask:
},
):
# Call periodic_payout directly (pay_out was renamed/refactored)
from router.wallet import periodic_payout
from routstr.wallet import periodic_payout
await periodic_payout()
@@ -506,7 +506,7 @@ class TestPeriodicPayoutTask:
# integration_session.add(key)
# await integration_session.commit()
# with patch("router.cashu.wallet") as mock_wallet:
# with patch("routstr.cashu.wallet") as mock_wallet:
# mock_wallet_instance = AsyncMock()
# mock_wallet_instance.balance = AsyncMock(
# return_value=100000
@@ -522,7 +522,7 @@ class TestPeriodicPayoutTask:
# "DEV_LN_ADDRESS": "dev@test.com",
# },
# ):
# from router.cashu import pay_out
# from routstr.cashu import pay_out
# await pay_out()
@@ -543,7 +543,7 @@ class TestPeriodicPayoutTask:
# integration_session.add(key)
# await integration_session.commit()
# with patch("router.cashu.wallet") as mock_wallet:
# with patch("routstr.cashu.wallet") as mock_wallet:
# mock_wallet_instance = AsyncMock()
# mock_wallet_instance.balance = AsyncMock(
# return_value=96000
@@ -552,7 +552,7 @@ class TestPeriodicPayoutTask:
# mock_wallet.return_value = mock_wallet_instance
# with patch.dict(os.environ, {"MINIMUM_PAYOUT": "10"}): # 10 sats minimum
# from router.cashu import pay_out
# from routstr.cashu import pay_out
# await pay_out()
@@ -571,9 +571,9 @@ class TestTaskInteractions:
# """Test that all tasks can run concurrently without issues"""
# # Mock all external dependencies
# with (
# patch("router.payment.price.sats_usd_ask_price", AsyncMock(return_value=0.00002)),
# patch("router.cashu.wallet") as mock_wallet,
# patch("router.cashu.pay_out", AsyncMock()),
# patch("routstr.payment.price.sats_usd_ask_price", AsyncMock(return_value=0.00002)),
# patch("routstr.cashu.wallet") as mock_wallet,
# patch("routstr.cashu.pay_out", AsyncMock()),
# ):
# mock_wallet_instance = AsyncMock()
# mock_wallet_instance.send_to_lnurl = AsyncMock(return_value=1)
@@ -587,7 +587,7 @@ class TestTaskInteractions:
# tasks.append(pricing_task)
# # Refund task (disabled to avoid interference)
# with patch.object(router.wallet, "REFUND_PROCESSING_INTERVAL", 0):
# with patch.object(routstr.wallet, "REFUND_PROCESSING_INTERVAL", 0):
# refund_task = asyncio.create_task(check_for_refunds())
# tasks.append(refund_task)
@@ -621,7 +621,7 @@ class TestTaskInteractions:
processing.set()
await asyncio.sleep(2) # Simulate long operation
with patch("router.payment.price.sats_usd_ask_price", slow_task):
with patch("routstr.payment.price.sats_usd_ask_price", slow_task):
# Start the pricing task
task = asyncio.create_task(update_sats_pricing())
@@ -708,11 +708,15 @@ class TestTaskInteractions:
# Patch the actual task functions
with (
patch(
"router.payment.models.update_sats_pricing",
"routstr.payment.models.update_sats_pricing",
lambda: task_with_cleanup("pricing"),
),
patch("router.wallet.periodic_payout", lambda: task_with_cleanup("refund")),
patch("router.wallet.periodic_payout", lambda: task_with_cleanup("payout")),
patch(
"routstr.wallet.periodic_payout", lambda: task_with_cleanup("refund")
),
patch(
"routstr.wallet.periodic_payout", lambda: task_with_cleanup("payout")
),
):
# Start all tasks
tasks = [
@@ -11,7 +11,7 @@ from sqlalchemy.exc import IntegrityError
from sqlalchemy.ext.asyncio import AsyncSession
from sqlmodel import select
from router.core.db import ApiKey
from routstr.core.db import ApiKey
class TestTransactionAtomicity:
@@ -114,7 +114,7 @@ class TestTransactionAtomicity:
initial_balance = api_key.balance
# Mock wallet to fail after token validation
with patch("router.wallet.send_token") as mock_wallet_func:
with patch("routstr.wallet.send_token") as mock_wallet_func:
mock_proof = MagicMock()
mock_proof.amount = 1000
mock_wallet = AsyncMock()
@@ -256,7 +256,7 @@ class TestConcurrentOperations:
await integration_session.commit()
# Mock wallet for topup
with patch("router.wallet.send_token") as mock_wallet_func:
with patch("routstr.wallet.send_token") as mock_wallet_func:
mock_proof = MagicMock()
mock_proof.amount = 2000
mock_wallet = AsyncMock()
@@ -521,7 +521,7 @@ class TestPerformance:
operation_times["select"].append((end - start) * 1000) # Convert to ms
# Test UPDATE performance (via topup)
with patch("router.wallet.send_token") as mock_wallet_func:
with patch("routstr.wallet.send_token") as mock_wallet_func:
mock_proof = MagicMock()
mock_proof.amount = 100
mock_wallet = AsyncMock()
@@ -10,7 +10,7 @@ from httpx import AsyncClient, ConnectError
from sqlalchemy.ext.asyncio import AsyncSession
from sqlmodel import select
from router.core.db import ApiKey
from routstr.core.db import ApiKey
class TestNetworkFailureScenarios:
@@ -26,11 +26,11 @@ class TestNetworkFailureScenarios:
# Patch the wallet send function to simulate failure across all modules
with (
patch(
"router.wallet.send_token",
"routstr.wallet.send_token",
AsyncMock(side_effect=ConnectError("Mint service unavailable")),
),
patch(
"router.balance.send_token",
"routstr.balance.send_token",
AsyncMock(side_effect=ConnectError("Mint service unavailable")),
),
):
@@ -46,8 +46,8 @@ class TestNetworkFailureScenarios:
integration_session: AsyncSession,
) -> None:
"""Test proxy behavior when upstream LLM service is down"""
# Mock at the router level to simulate upstream being down
with patch("router.proxy.httpx.AsyncClient") as mock_client_class:
# Mock at the routstr level to simulate upstream being down
with patch("routstr.proxy.httpx.AsyncClient") as mock_client_class:
# Create a mock client instance
mock_client = AsyncMock()
mock_client_class.return_value = mock_client
+1 -1
View File
@@ -149,7 +149,7 @@ class TestPerformanceBaseline:
"""Test database operation performance"""
from sqlmodel import select
from router.core.db import ApiKey
from routstr.core.db import ApiKey
# Create test data
for i in range(100):
+55 -40
View File
@@ -43,9 +43,9 @@ async def test_providers_endpoint_default_response(
}
with patch(
"router.discovery.query_nostr_relay_for_providers", return_value=mock_events
"routstr.discovery.query_nostr_relay_for_providers", return_value=mock_events
):
with patch("router.discovery.fetch_provider_health") as mock_fetch:
with patch("routstr.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"}}
@@ -100,9 +100,9 @@ async def test_providers_endpoint_with_include_json(
}
with patch(
"router.discovery.query_nostr_relay_for_providers", return_value=mock_events
"routstr.discovery.query_nostr_relay_for_providers", return_value=mock_events
):
with patch("router.discovery.fetch_provider_health") as mock_fetch:
with patch("routstr.discovery.fetch_provider_health") as mock_fetch:
mock_fetch.return_value = {
"status_code": 200,
"json": mock_provider_response,
@@ -155,7 +155,7 @@ async def test_providers_data_structure_validation(
["description", "A comprehensive AI provider"],
["model", "gpt-3.5-turbo"],
["model", "gpt-4"],
]
],
}
]
@@ -165,15 +165,15 @@ async def test_providers_data_structure_validation(
"json": {
"data": [
{"id": "gpt-3.5-turbo", "object": "model"},
{"id": "gpt-4", "object": "model"}
{"id": "gpt-4", "object": "model"},
]
}
},
}
with patch(
"router.discovery.query_nostr_relay_for_providers", return_value=mock_events
"routstr.discovery.query_nostr_relay_for_providers", return_value=mock_events
):
with patch("router.discovery.fetch_provider_health") as mock_fetch:
with patch("routstr.discovery.fetch_provider_health") as mock_fetch:
mock_fetch.return_value = mock_health_response
response = await integration_client.get("/v1/providers/?include_json=true")
@@ -188,7 +188,7 @@ async def test_providers_data_structure_validation(
# 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"]
@@ -215,7 +215,7 @@ async def test_providers_endpoint_no_providers_found(
mock_events: list[dict[str, Any]] = []
with patch(
"router.discovery.query_nostr_relay_for_providers", return_value=mock_events
"routstr.discovery.query_nostr_relay_for_providers", return_value=mock_events
):
response = await integration_client.get("/v1/providers/")
@@ -245,33 +245,41 @@ async def test_providers_endpoint_offline_providers(
["d", "healthy-provider"],
["endpoint", "http://healthy-provider.onion"],
["name", "Healthy Provider"],
]
],
},
{
"id": "event2",
"pubkey": "offline_provider_pubkey",
"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"}}
return {
"status_code": 200,
"endpoint": "root",
"json": {"status": "online"},
}
else:
return {"status_code": 500, "endpoint": "error", "json": {"error": "Service unavailable"}}
return {
"status_code": 500,
"endpoint": "error",
"json": {"error": "Service unavailable"},
}
with patch(
"router.discovery.query_nostr_relay_for_providers", return_value=mock_events
"routstr.discovery.query_nostr_relay_for_providers", return_value=mock_events
):
with patch(
"router.discovery.fetch_provider_health",
"routstr.discovery.fetch_provider_health",
side_effect=mock_fetch_provider_health,
):
response = await integration_client.get("/v1/providers/?include_json=true")
@@ -286,10 +294,10 @@ async def test_providers_endpoint_offline_providers(
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
@@ -297,7 +305,10 @@ async def test_providers_endpoint_offline_providers(
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"]
assert (
"status" in health_info["json"]
or "error" not in health_info["json"]
)
@pytest.mark.integration
@@ -318,7 +329,7 @@ async def test_providers_endpoint_duplicate_urls(
["d", "provider-1"],
["endpoint", "http://provider.onion"],
["name", "Provider"],
]
],
},
{
"id": "event2",
@@ -329,15 +340,19 @@ async def test_providers_endpoint_duplicate_urls(
["d", "other-provider"],
["endpoint", "http://other-provider.onion"],
["name", "Other Provider"],
]
],
},
]
with patch(
"router.discovery.query_nostr_relay_for_providers", return_value=mock_events
"routstr.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"}}
with patch("routstr.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/")
@@ -352,7 +367,7 @@ async def test_providers_endpoint_duplicate_urls(
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)
@@ -369,7 +384,7 @@ async def test_providers_endpoint_nostr_relay_failures(
raise Exception("Connection to relay failed")
with patch(
"router.discovery.query_nostr_relay_for_providers", side_effect=failing_query
"routstr.discovery.query_nostr_relay_for_providers", side_effect=failing_query
):
response = await integration_client.get("/v1/providers/")
@@ -407,9 +422,9 @@ async def test_providers_endpoint_malformed_urls(
]
with patch(
"router.discovery.query_nostr_relay_for_providers", return_value=mock_events
"routstr.discovery.query_nostr_relay_for_providers", return_value=mock_events
):
with patch("router.discovery.fetch_provider_health") as mock_fetch:
with patch("routstr.discovery.fetch_provider_health") as mock_fetch:
mock_fetch.return_value = {"status_code": 200, "json": {"status": "online"}}
response = await integration_client.get("/v1/providers/")
@@ -440,9 +455,9 @@ async def test_providers_endpoint_response_format(
]
with patch(
"router.discovery.query_nostr_relay_for_providers", return_value=mock_events
"routstr.discovery.query_nostr_relay_for_providers", return_value=mock_events
):
with patch("router.discovery.fetch_provider_health") as mock_fetch:
with patch("routstr.discovery.fetch_provider_health") as mock_fetch:
mock_fetch.return_value = {"status_code": 200, "json": {"status": "online"}}
# Test default format
@@ -490,9 +505,9 @@ async def test_providers_endpoint_performance(integration_client: AsyncClient) -
validator = PerformanceValidator()
with patch(
"router.discovery.query_nostr_relay_for_providers", return_value=mock_events
"routstr.discovery.query_nostr_relay_for_providers", return_value=mock_events
):
with patch("router.discovery.fetch_provider_health") as mock_fetch:
with patch("routstr.discovery.fetch_provider_health") as mock_fetch:
mock_fetch.return_value = {"status_code": 200, "json": {"status": "online"}}
# Test multiple requests
@@ -530,9 +545,9 @@ async def test_providers_endpoint_concurrent_requests(
]
with patch(
"router.discovery.query_nostr_relay_for_providers", return_value=mock_events
"routstr.discovery.query_nostr_relay_for_providers", return_value=mock_events
):
with patch("router.discovery.fetch_provider_health") as mock_fetch:
with patch("routstr.discovery.fetch_provider_health") as mock_fetch:
mock_fetch.return_value = {"status_code": 200, "json": {"status": "online"}}
# Create concurrent requests
@@ -566,9 +581,9 @@ async def test_providers_endpoint_parameter_validation(
]
with patch(
"router.discovery.query_nostr_relay_for_providers", return_value=mock_events
"routstr.discovery.query_nostr_relay_for_providers", return_value=mock_events
):
with patch("router.discovery.fetch_provider_health") as mock_fetch:
with patch("routstr.discovery.fetch_provider_health") as mock_fetch:
mock_fetch.return_value = {"status_code": 200, "json": {"status": "online"}}
# Test various parameter values
@@ -617,9 +632,9 @@ async def test_no_database_changes_during_provider_operations(
]
with patch(
"router.discovery.query_nostr_relay_for_providers", return_value=mock_events
"routstr.discovery.query_nostr_relay_for_providers", return_value=mock_events
):
with patch("router.discovery.fetch_provider_health") as mock_fetch:
with patch("routstr.discovery.fetch_provider_health") as mock_fetch:
mock_fetch.return_value = {"status_code": 200, "json": {"status": "online"}}
# Make multiple requests with different parameters
@@ -14,7 +14,7 @@ import pytest
from httpx import AsyncClient
from sqlmodel import select
from router.core.db import ApiKey
from routstr.core.db import ApiKey
from .utils import (
ConcurrencyTester,
@@ -11,7 +11,7 @@ import pytest
from httpx import AsyncClient
from sqlmodel import select
from router.core.db import ApiKey
from routstr.core.db import ApiKey
from .utils import (
CashuTokenGenerator,
+1 -1
View File
@@ -11,7 +11,7 @@ import pytest
from httpx import AsyncClient
from sqlmodel import select, update
from router.core.db import ApiKey
from routstr.core.db import ApiKey
from .utils import ConcurrencyTester, ResponseValidator
+6 -6
View File
@@ -13,8 +13,8 @@ import pytest
from httpx import AsyncClient
from sqlmodel import select
from router.core.db import ApiKey
from router.wallet import CurrencyUnit
from routstr.core.db import ApiKey
from routstr.wallet import CurrencyUnit
@pytest.mark.integration
@@ -207,12 +207,12 @@ async def test_refund_with_lightning_address(
await db_snapshot.capture()
# Mock send_to_lnurl function directly
with patch("router.balance.send_to_lnurl") as mock_send_to_lnurl:
with patch("routstr.balance.send_to_lnurl") as mock_send_to_lnurl:
mock_send_to_lnurl.return_value = {
"amount_sent": balance,
"unit": "msat",
"lnurl": refund_address,
"status": "completed"
"status": "completed",
}
# Request refund
@@ -411,7 +411,7 @@ async def test_mint_unavailability_handling(
# Make the send_token method raise an exception
with patch(
"router.balance.send_token",
"routstr.balance.send_token",
side_effect=Exception("Mint unavailable: Connection refused"),
):
# The exception should propagate as a 503 error (Service Unavailable)
@@ -534,7 +534,7 @@ async def test_refund_with_expired_key(
integration_client.headers["Authorization"] = f"Bearer {api_key}"
# Mock the refund to LN address
with patch("router.wallet.send_token") as mock_wallet_func:
with patch("routstr.wallet.send_token") as mock_wallet_func:
mock_wallet = AsyncMock()
mock_wallet.send_to_lnurl = AsyncMock(return_value=500) # type: ignore[method-assign]
mock_wallet_func.return_value = mock_wallet
+7 -3
View File
@@ -11,7 +11,7 @@ import pytest
from httpx import AsyncClient
from sqlmodel import select
from router.core.db import ApiKey
from routstr.core.db import ApiKey
from .utils import (
CashuTokenGenerator,
@@ -425,7 +425,7 @@ async def test_network_failure_during_token_verification( # type: ignore[no-unt
token = await testmint_wallet.mint_tokens(300)
# Mock credit_balance to simulate network failure during token verification
with patch("router.balance.credit_balance") as mock_credit_balance:
with patch("routstr.balance.credit_balance") as mock_credit_balance:
mock_credit_balance.side_effect = Exception("Network error: Connection timeout")
response = await authenticated_client.post(
@@ -470,7 +470,11 @@ async def test_topup_with_zero_amount_token( # type: ignore[no-untyped-def]
# Create a token with 0 amount (edge case)
# The testmint wallet should handle this
with patch.object(testmint_wallet, "redeem_token", return_value=(0, "sat", testmint_wallet.mint_url)):
with patch.object(
testmint_wallet,
"redeem_token",
return_value=(0, "sat", testmint_wallet.mint_url),
):
token = await testmint_wallet.mint_tokens(0)
response = await authenticated_client.post(
+1 -1
View File
@@ -9,7 +9,7 @@ import httpx
from sqlalchemy.ext.asyncio import AsyncSession
from sqlmodel import select
from router.core.db import ApiKey
from routstr.core.db import ApiKey
class CashuTokenGenerator:
+2 -2
View File
@@ -43,8 +43,8 @@ def check_imports() -> bool:
print("Conftest fixtures imported successfully")
# Check router modules - imports are for verification only
from router.core.db import ApiKey
# Check routstr modules - imports are for verification only
from routstr.core.db import ApiKey
del ApiKey
+1 -1
View File
@@ -21,7 +21,7 @@ pytest
To run tests with coverage:
```bash
pytest --cov=router --cov-report=html
pytest --cov=routstr --cov-report=html
```
To run specific test files:
+7 -7
View File
@@ -5,7 +5,7 @@ from unittest.mock import Mock, patch
os.environ["UPSTREAM_BASE_URL"] = "http://test"
os.environ["UPSTREAM_API_KEY"] = "test"
from router.payment.helpers import get_max_cost_for_model # noqa: E402
from routstr.payment.helpers import get_max_cost_for_model # noqa: E402
def test_get_max_cost_for_model_known() -> None:
@@ -14,21 +14,21 @@ def test_get_max_cost_for_model_known() -> None:
mock_model.sats_pricing = Mock()
mock_model.sats_pricing.max_cost = 500
with patch("router.payment.helpers.MODELS", [mock_model]):
with patch("router.payment.helpers.MODEL_BASED_PRICING", True):
with patch("routstr.payment.helpers.MODELS", [mock_model]):
with patch("routstr.payment.helpers.MODEL_BASED_PRICING", True):
cost = get_max_cost_for_model("gpt-4")
assert cost == 500000 # 500 sats * 1000 = msats
def test_get_max_cost_for_model_unknown() -> None:
with patch("router.payment.helpers.MODELS", []):
with patch("router.payment.helpers.COST_PER_REQUEST", 100):
with patch("routstr.payment.helpers.MODELS", []):
with patch("routstr.payment.helpers.COST_PER_REQUEST", 100):
cost = get_max_cost_for_model("unknown-model")
assert cost == 100
def test_get_max_cost_for_model_disabled() -> None:
with patch("router.payment.helpers.MODEL_BASED_PRICING", False):
with patch("router.payment.helpers.COST_PER_REQUEST", 200):
with patch("routstr.payment.helpers.MODEL_BASED_PRICING", False):
with patch("routstr.payment.helpers.COST_PER_REQUEST", 200):
cost = get_max_cost_for_model("any-model")
assert cost == 200
+13 -13
View File
@@ -4,7 +4,7 @@ from unittest.mock import AsyncMock, Mock, patch
import pytest
from router.wallet import credit_balance, get_balance, recieve_token, send_token
from routstr.wallet import credit_balance, get_balance, recieve_token, send_token
@pytest.mark.asyncio
@@ -13,7 +13,7 @@ async def test_get_balance() -> None:
mock_wallet.available_balance = Mock(amount=50000)
mock_wallet.load_proofs = AsyncMock()
with patch("router.wallet.Wallet.with_db", return_value=mock_wallet):
with patch("routstr.wallet.Wallet.with_db", return_value=mock_wallet):
balance = await get_balance("sat")
assert balance == 50000
@@ -38,8 +38,8 @@ async def test_recieve_token_valid() -> None:
mock_wallet = Mock()
mock_wallet.redeem = AsyncMock()
with patch("router.wallet.TRUSTED_MINTS", ["http://mint:3338"]):
with patch("router.wallet.deserialize_token_from_string") as mock_deserialize:
with patch("routstr.wallet.TRUSTED_MINTS", ["http://mint:3338"]):
with patch("routstr.wallet.deserialize_token_from_string") as mock_deserialize:
mock_token = Mock()
mock_token.keysets = ["keyset1"]
mock_token.mint = "http://mint:3338"
@@ -48,7 +48,7 @@ async def test_recieve_token_valid() -> None:
mock_token.proofs = [{"amount": 1000}]
mock_deserialize.return_value = mock_token
with patch("router.wallet.Wallet.with_db", return_value=mock_wallet):
with patch("routstr.wallet.Wallet.with_db", return_value=mock_wallet):
mock_wallet.load_mint = AsyncMock()
amount, unit, mint = await recieve_token(token_str)
@@ -61,8 +61,8 @@ async def test_recieve_token_valid() -> None:
async def test_send_token() -> None:
mock_wallet = Mock()
with patch("router.wallet.Wallet.with_db", return_value=mock_wallet):
with patch("router.wallet.send", return_value=(1000, "test_token")):
with patch("routstr.wallet.Wallet.with_db", return_value=mock_wallet):
with patch("routstr.wallet.send", return_value=(1000, "test_token")):
token = await send_token(1000, "sat", "http://mint:3338")
assert token == "test_token"
@@ -81,9 +81,9 @@ async def test_credit_balance() -> None:
mock_key.balance = 5000000
mock_session = AsyncMock()
with patch("router.wallet.PRIMARY_MINT_URL", "http://mint:3338"):
with patch("routstr.wallet.PRIMARY_MINT_URL", "http://mint:3338"):
with patch(
"router.wallet.recieve_token",
"routstr.wallet.recieve_token",
return_value=(1000, "sat", "http://mint:3338"),
):
amount = await credit_balance(token_str, mock_key, mock_session)
@@ -99,7 +99,7 @@ async def test_credit_balance_invalid_mint() -> None:
mock_session = AsyncMock()
with patch(
"router.wallet.recieve_token", return_value=(1000, "sat", "http://other:3338")
"routstr.wallet.recieve_token", return_value=(1000, "sat", "http://other:3338")
):
with pytest.raises(ValueError, match="Mint URL is not supported"):
await credit_balance("test_token", mock_key, mock_session)
@@ -109,7 +109,7 @@ async def test_credit_balance_invalid_mint() -> None:
async def test_recieve_token_untrusted_mint() -> None:
mock_wallet = Mock()
with patch("router.wallet.deserialize_token_from_string") as mock_deserialize:
with patch("routstr.wallet.deserialize_token_from_string") as mock_deserialize:
mock_token = Mock()
mock_token.keysets = ["keyset1"]
mock_token.mint = "http://untrusted:3338"
@@ -117,10 +117,10 @@ async def test_recieve_token_untrusted_mint() -> None:
mock_token.amount = 1000
mock_deserialize.return_value = mock_token
with patch("router.wallet.Wallet.with_db", return_value=mock_wallet):
with patch("routstr.wallet.Wallet.with_db", return_value=mock_wallet):
mock_wallet.load_mint = AsyncMock()
with patch(
"router.wallet.swap_to_primary_mint",
"routstr.wallet.swap_to_primary_mint",
return_value=(900, "sat", "http://mint:3338"),
):
amount, unit, mint = await recieve_token("test_token")
Generated
+2 -2
View File
@@ -1768,7 +1768,7 @@ wheels = [
[[package]]
name = "routstr"
version = "0.1.0"
source = { virtual = "." }
source = { editable = "." }
dependencies = [
{ name = "aiosqlite" },
{ name = "alembic" },
@@ -1822,7 +1822,7 @@ dev = [
{ name = "pytest-asyncio", specifier = ">=0.24.0" },
{ name = "pytest-benchmark", specifier = ">=4.0.0" },
{ name = "pytest-cov", specifier = ">=6.1.1" },
{ name = "routstr", virtual = "." },
{ name = "routstr", editable = "." },
{ name = "ruff", specifier = ">=0.11.6" },
]