diff --git a/tests/integration/test_wallet_refund.py b/tests/integration/test_wallet_refund.py index 55dd9a05..4bac88bd 100644 --- a/tests/integration/test_wallet_refund.py +++ b/tests/integration/test_wallet_refund.py @@ -356,6 +356,66 @@ async def test_concurrent_refund_requests( assert len(successful) + len(failed) == 5 +@pytest.mark.integration +@pytest.mark.asyncio +async def test_refund_rejects_concurrent_topup_on_same_key( + authenticated_client: AsyncClient, + testmint_wallet: Any, +) -> None: + """Test refund returns 409 when a concurrent topup changes the balance first.""" + from routstr import balance as balance_module + + wallet_response = await authenticated_client.get("/v1/wallet/") + assert wallet_response.status_code == 200 + initial_balance = wallet_response.json()["balance"] + + topup_amount_sat = 500 + topup_token = await testmint_wallet.mint_tokens(topup_amount_sat) + + validate_called = asyncio.Event() + allow_refund_to_continue = asyncio.Event() + original_validate_bearer_key = balance_module.validate_bearer_key + + async def delayed_validate_bearer_key(*args: Any, **kwargs: Any) -> ApiKey: + key = await original_validate_bearer_key(*args, **kwargs) + validate_called.set() + await allow_refund_to_continue.wait() + return key + + async def issue_refund() -> Any: + return await authenticated_client.post("/v1/wallet/refund") + + async def issue_topup() -> Any: + await validate_called.wait() + try: + return await authenticated_client.post( + "/v1/wallet/topup", params={"cashu_token": topup_token} + ) + finally: + allow_refund_to_continue.set() + + with patch( + "routstr.balance.validate_bearer_key", new=delayed_validate_bearer_key + ): + refund_response, topup_response = await asyncio.gather( + issue_refund(), issue_topup() + ) + + assert topup_response.status_code == 200 + assert topup_response.json()["msats"] == topup_amount_sat * 1000 + assert refund_response.status_code == 409 + assert ( + refund_response.json()["detail"] + == "Balance changed concurrently. Please retry the refund." + ) + + final_balance_response = await authenticated_client.get("/v1/wallet/") + assert final_balance_response.status_code == 200 + assert final_balance_response.json()["balance"] == ( + initial_balance + topup_amount_sat * 1000 + ) + + @pytest.mark.integration @pytest.mark.asyncio async def test_refund_during_active_usage(