mirror of
https://github.com/Routstr/routstr-core.git
synced 2026-08-09 19:04:47 +00:00
122 lines
3.8 KiB
Python
122 lines
3.8 KiB
Python
import asyncio
|
|
from unittest.mock import AsyncMock, Mock, patch
|
|
|
|
import httpx
|
|
import pytest
|
|
from cashu.core.base import Unit
|
|
|
|
from routstr.mint import (
|
|
MintCooldownError,
|
|
MintRateGuard,
|
|
MintRateLimitedError,
|
|
fail_fast_mint_operations,
|
|
)
|
|
from routstr.wallet import Wallet
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_cooldown_fails_fast_while_wallet_mutation_scope_is_held() -> None:
|
|
guard = MintRateGuard("http://mint:3338", max_concurrency=1)
|
|
guard.apply_cooldown(3600, reason="rate_limited")
|
|
operation = AsyncMock(return_value="should not run")
|
|
|
|
with (
|
|
patch("routstr.mint.asyncio.sleep", AsyncMock()) as sleep,
|
|
pytest.raises(MintCooldownError) as caught,
|
|
):
|
|
async with fail_fast_mint_operations():
|
|
await guard.run(operation)
|
|
|
|
assert caught.value.retry_after_seconds > 0
|
|
operation.assert_not_awaited()
|
|
sleep.assert_not_awaited()
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_expired_cooldown_allows_probe_in_wallet_mutation_scope() -> None:
|
|
guard = MintRateGuard("http://mint:3338", max_concurrency=1)
|
|
guard.apply_cooldown(0, reason="rate_limited")
|
|
operation = AsyncMock(return_value="recovered")
|
|
|
|
async with fail_fast_mint_operations():
|
|
result = await guard.run(operation)
|
|
|
|
assert result == "recovered"
|
|
operation.assert_awaited_once()
|
|
assert guard._needs_probe is False
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_fail_fast_does_not_wait_behind_existing_probe() -> None:
|
|
guard = MintRateGuard("http://mint:3338", max_concurrency=1)
|
|
guard.apply_cooldown(0, reason="rate_limited")
|
|
probe_started = asyncio.Event()
|
|
release_probe = asyncio.Event()
|
|
|
|
async def probe() -> str:
|
|
probe_started.set()
|
|
await release_probe.wait()
|
|
return "recovered"
|
|
|
|
first = asyncio.create_task(guard.run(probe))
|
|
await probe_started.wait()
|
|
try:
|
|
async with fail_fast_mint_operations():
|
|
with pytest.raises(MintCooldownError):
|
|
await asyncio.wait_for(guard.run(AsyncMock()), timeout=0.05)
|
|
finally:
|
|
release_probe.set()
|
|
assert await first == "recovered"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_cashu_429_dispatches_through_wallet_override() -> None:
|
|
async def handler(request: httpx.Request) -> httpx.Response:
|
|
return httpx.Response(
|
|
429,
|
|
request=request,
|
|
json={"detail": "too many requests", "code": 42900},
|
|
)
|
|
|
|
wallet = object.__new__(Wallet)
|
|
wallet.url = "http://mint:3338"
|
|
wallet.db = Mock()
|
|
wallet.keysets = {"loaded": Mock()}
|
|
wallet.mint_info = Mock()
|
|
wallet.mint_info.requires_blind_auth_path.return_value = False
|
|
wallet.mint_info.requires_clear_auth_path.return_value = False
|
|
wallet.auth_db = None
|
|
wallet.auth_keyset_id = None
|
|
|
|
real_client = httpx.AsyncClient
|
|
|
|
def client_factory(*args: object, **kwargs: object) -> httpx.AsyncClient:
|
|
return real_client(
|
|
transport=httpx.MockTransport(handler),
|
|
base_url=str(kwargs["base_url"]),
|
|
)
|
|
|
|
with (
|
|
patch("cashu.wallet.v1_api.httpx.AsyncClient", side_effect=client_factory),
|
|
pytest.raises(MintRateLimitedError),
|
|
):
|
|
await wallet.mint_quote(1, Unit.sat)
|
|
|
|
|
|
async def test_guard_concurrency_change_preserves_cooldown_state() -> None:
|
|
from routstr.core.settings import settings
|
|
|
|
mint_url = "https://mint.test-concurrency-carryover"
|
|
with patch.object(settings, "mint_max_concurrency", 2):
|
|
guard = MintRateGuard.get(mint_url)
|
|
guard.apply_cooldown(120.0, reason="rate_limited")
|
|
guard._consecutive_rate_limits = 3
|
|
|
|
with patch.object(settings, "mint_max_concurrency", 5):
|
|
rebuilt = MintRateGuard.get(mint_url)
|
|
|
|
assert rebuilt is not guard
|
|
assert rebuilt.cooldown_remaining() > 0
|
|
assert rebuilt._cooldown_reason == "rate_limited"
|
|
assert rebuilt._consecutive_rate_limits == 3
|