mirror of
https://github.com/Routstr/routstr-core.git
synced 2026-08-09 19:04:47 +00:00
149 lines
4.7 KiB
Python
149 lines
4.7 KiB
Python
import base64
|
|
import json
|
|
from types import SimpleNamespace
|
|
from unittest.mock import AsyncMock, Mock
|
|
|
|
import pytest
|
|
from fastapi import HTTPException
|
|
|
|
import routstr.wallet as wallet_module
|
|
from routstr.core import admin
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
@pytest.mark.parametrize("requested_mint", [None, "https://secondary.example"])
|
|
async def test_withdraw_uses_effective_mint_and_records_outgoing_transaction(
|
|
monkeypatch: pytest.MonkeyPatch, requested_mint: str | None
|
|
) -> None:
|
|
primary_mint = "https://primary.example"
|
|
effective_mint = requested_mint or primary_mint
|
|
token = "cashuBoutgoing"
|
|
send_token = AsyncMock(return_value=token)
|
|
store_transaction = AsyncMock(return_value=True)
|
|
|
|
monkeypatch.setattr(admin, "send_token", send_token)
|
|
monkeypatch.setattr(admin, "token_mint_url", Mock(return_value=effective_mint))
|
|
monkeypatch.setattr(admin, "store_cashu_transaction", store_transaction)
|
|
monkeypatch.setattr(admin.settings, "primary_mint", primary_mint)
|
|
|
|
result = await admin.withdraw(
|
|
Mock(),
|
|
admin.WithdrawRequest(amount=75, mint_url=requested_mint, unit="sat"),
|
|
)
|
|
|
|
assert result == {"token": token, "mint_url": effective_mint}
|
|
send_token.assert_awaited_once_with(75, "sat", effective_mint)
|
|
store_transaction.assert_awaited_once_with(
|
|
token=token,
|
|
amount=75,
|
|
unit="sat",
|
|
mint_url=effective_mint,
|
|
typ="out",
|
|
collected=False,
|
|
source="admin",
|
|
)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_withdraw_propagates_audit_storage_failure(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
mint = "https://primary.example"
|
|
token = "cashuBrecoverable"
|
|
|
|
monkeypatch.setattr(admin, "send_token", AsyncMock(return_value=token))
|
|
monkeypatch.setattr(admin, "token_mint_url", Mock(return_value=mint))
|
|
monkeypatch.setattr(
|
|
admin,
|
|
"store_cashu_transaction",
|
|
AsyncMock(side_effect=RuntimeError("database unavailable")),
|
|
)
|
|
monkeypatch.setattr(admin.settings, "primary_mint", mint)
|
|
|
|
with pytest.raises(RuntimeError, match="database unavailable"):
|
|
await admin.withdraw(Mock(), admin.WithdrawRequest(amount=75))
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_withdraw_falls_back_from_insufficient_preferred_mint(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
requested_mint = "https://primary.example"
|
|
actual_mint = "https://secondary.example"
|
|
proofs = [SimpleNamespace(amount=100, reserved=False, id="00")]
|
|
token_payload = {
|
|
"token": [
|
|
{
|
|
"mint": actual_mint,
|
|
"proofs": [
|
|
{
|
|
"id": "00",
|
|
"amount": 75,
|
|
"secret": "secret",
|
|
"C": "02" + "00" * 32,
|
|
}
|
|
],
|
|
}
|
|
],
|
|
"unit": "sat",
|
|
}
|
|
token = "cashuA" + base64.urlsafe_b64encode(
|
|
json.dumps(token_payload).encode()
|
|
).decode()
|
|
wallet = SimpleNamespace(
|
|
keysets={},
|
|
proofs=proofs,
|
|
select_to_send=AsyncMock(return_value=(proofs, 0)),
|
|
serialize_proofs=AsyncMock(return_value=token),
|
|
set_reserved_for_send=AsyncMock(),
|
|
)
|
|
find_funded = AsyncMock(return_value=actual_mint)
|
|
store_transaction = AsyncMock(return_value=True)
|
|
|
|
monkeypatch.setattr(wallet_module, "find_trusted_mint_with_funds", find_funded)
|
|
monkeypatch.setattr(wallet_module, "get_wallet", AsyncMock(return_value=wallet))
|
|
monkeypatch.setattr(
|
|
wallet_module, "get_proofs_per_mint_and_unit", Mock(return_value=proofs)
|
|
)
|
|
monkeypatch.setattr(admin, "store_cashu_transaction", store_transaction)
|
|
|
|
result = await admin.withdraw(
|
|
Mock(), admin.WithdrawRequest(amount=75, mint_url=requested_mint)
|
|
)
|
|
|
|
assert result == {"token": token, "mint_url": actual_mint}
|
|
find_funded.assert_awaited_once_with(
|
|
75, "sat", requested_mint, force_reload=True
|
|
)
|
|
wallet.select_to_send.assert_awaited_once()
|
|
store_transaction.assert_awaited_once_with(
|
|
token=token,
|
|
amount=75,
|
|
unit="sat",
|
|
mint_url=actual_mint,
|
|
typ="out",
|
|
collected=False,
|
|
source="admin",
|
|
)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_withdraw_maps_true_aggregate_insufficient_funds_to_400(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
monkeypatch.setattr(
|
|
admin,
|
|
"send_token",
|
|
AsyncMock(
|
|
side_effect=ValueError(
|
|
"No trusted mint has 75 sat available; balances={'mint': 0}"
|
|
)
|
|
),
|
|
)
|
|
|
|
with pytest.raises(HTTPException) as exc_info:
|
|
await admin.withdraw(Mock(), admin.WithdrawRequest(amount=75))
|
|
|
|
assert exc_info.value.status_code == 400
|
|
assert exc_info.value.detail == "Insufficient wallet balance"
|