Skip to content

Commit 489aae8

Browse files
committed
Add tests for _wait_for_transaction retry on transient network errors
Cover the new ConnectTimeout/ReadTimeout/ConnectError retry paths added in main (PR #9). Tests verify: - Retries on each error type then succeeds - Times out with TxnConfirmError after retries exhausted
1 parent a6acb6e commit 489aae8

1 file changed

Lines changed: 133 additions & 0 deletions

File tree

tests/test_pure_logic.py

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -489,3 +489,136 @@ def test_without_api_key(self) -> None:
489489

490490
def test_empty_string_api_key(self) -> None:
491491
assert _build_auth_headers("") == {}
492+
493+
494+
# ---------------------------------------------------------------------------
495+
# _base.py — _wait_for_transaction retry on transient network errors
496+
# ---------------------------------------------------------------------------
497+
498+
499+
class TestWaitForTransactionRetry:
500+
"""Verify _wait_for_transaction retries on ConnectTimeout/ReadTimeout/ConnectError."""
501+
502+
@pytest.fixture
503+
def base_sdk(self):
504+
"""Create a minimal BaseSDK instance for testing."""
505+
from unittest.mock import AsyncMock
506+
507+
from aptos_sdk.account import Account
508+
509+
from decibel._base import BaseSDK, BaseSDKOptions
510+
from decibel._constants import TESTNET_CONFIG
511+
512+
account = Account.generate()
513+
sdk = BaseSDK(
514+
TESTNET_CONFIG,
515+
account,
516+
opts=BaseSDKOptions(no_fee_payer=True),
517+
)
518+
# Patch sleep to avoid real delays
519+
sdk._async_sleep = AsyncMock() # type: ignore[assignment]
520+
return sdk
521+
522+
async def test_retries_on_connect_timeout_then_succeeds(self, base_sdk) -> None:
523+
"""SHALL retry on ConnectTimeout and succeed when tx confirms."""
524+
from unittest.mock import AsyncMock, patch
525+
526+
call_count = 0
527+
528+
async def mock_get(url, headers=None):
529+
nonlocal call_count
530+
call_count += 1
531+
if call_count <= 2:
532+
raise httpx.ConnectTimeout("connection timed out")
533+
# Third call succeeds
534+
resp = httpx.Response(
535+
200,
536+
json={"success": True, "hash": "0xabc", "type": "user_transaction"},
537+
)
538+
return resp
539+
540+
mock_client = AsyncMock()
541+
mock_client.get = mock_get
542+
mock_client.__aenter__ = AsyncMock(return_value=mock_client)
543+
mock_client.__aexit__ = AsyncMock(return_value=False)
544+
545+
with patch("httpx.AsyncClient", return_value=mock_client):
546+
result = await base_sdk._wait_for_transaction("0xabc")
547+
548+
assert result["success"] is True
549+
assert call_count == 3
550+
551+
async def test_retries_on_read_timeout(self, base_sdk) -> None:
552+
"""SHALL retry on ReadTimeout."""
553+
from unittest.mock import AsyncMock, patch
554+
555+
call_count = 0
556+
557+
async def mock_get(url, headers=None):
558+
nonlocal call_count
559+
call_count += 1
560+
if call_count == 1:
561+
raise httpx.ReadTimeout("read timed out")
562+
resp = httpx.Response(
563+
200,
564+
json={"success": True, "hash": "0xabc", "type": "user_transaction"},
565+
)
566+
return resp
567+
568+
mock_client = AsyncMock()
569+
mock_client.get = mock_get
570+
mock_client.__aenter__ = AsyncMock(return_value=mock_client)
571+
mock_client.__aexit__ = AsyncMock(return_value=False)
572+
573+
with patch("httpx.AsyncClient", return_value=mock_client):
574+
result = await base_sdk._wait_for_transaction("0xabc")
575+
576+
assert result["success"] is True
577+
assert call_count == 2
578+
579+
async def test_retries_on_connect_error(self, base_sdk) -> None:
580+
"""SHALL retry on ConnectError."""
581+
from unittest.mock import AsyncMock, patch
582+
583+
call_count = 0
584+
585+
async def mock_get(url, headers=None):
586+
nonlocal call_count
587+
call_count += 1
588+
if call_count == 1:
589+
raise httpx.ConnectError("connection refused")
590+
resp = httpx.Response(
591+
200,
592+
json={"success": True, "hash": "0xabc", "type": "user_transaction"},
593+
)
594+
return resp
595+
596+
mock_client = AsyncMock()
597+
mock_client.get = mock_get
598+
mock_client.__aenter__ = AsyncMock(return_value=mock_client)
599+
mock_client.__aexit__ = AsyncMock(return_value=False)
600+
601+
with patch("httpx.AsyncClient", return_value=mock_client):
602+
result = await base_sdk._wait_for_transaction("0xabc")
603+
604+
assert result["success"] is True
605+
606+
async def test_timeout_after_retries_exhausted(self, base_sdk) -> None:
607+
"""SHALL raise TxnConfirmError if timeout exceeded during retries."""
608+
from unittest.mock import AsyncMock, patch
609+
610+
from decibel._exceptions import TxnConfirmError
611+
612+
async def mock_get(url, headers=None):
613+
raise httpx.ConnectTimeout("always fails")
614+
615+
mock_client = AsyncMock()
616+
mock_client.get = mock_get
617+
mock_client.__aenter__ = AsyncMock(return_value=mock_client)
618+
mock_client.__aexit__ = AsyncMock(return_value=False)
619+
620+
with (
621+
patch("httpx.AsyncClient", return_value=mock_client),
622+
pytest.raises(TxnConfirmError, match="did not confirm"),
623+
):
624+
await base_sdk._wait_for_transaction("0xabc", txn_confirm_timeout=0.01)

0 commit comments

Comments
 (0)