From 4539dc980393e4d7fc2eac778b4549a59ea1dddb Mon Sep 17 00:00:00 2001 From: bohd4nx Date: Tue, 16 Jun 2026 01:49:11 +0300 Subject: [PATCH] Refactor tests: Add error handling tests for API provider and recharge flows --- tests/002_test_client.py | 30 ++++++++++ tests/004_test_stars.py | 105 +++++++++++++++++++++++++++++++++ tests/005_test_premium.py | 105 +++++++++++++++++++++++++++++++++ tests/006_test_topup.py | 48 +++++++++++++++ tests/007_test_wallet.py | 94 +++++++++++++++++++++++++++++ tests/010_test_recharge_ads.py | 46 +++++++++++++++ 6 files changed, 428 insertions(+) diff --git a/tests/002_test_client.py b/tests/002_test_client.py index 3bb76d5..2e8b878 100644 --- a/tests/002_test_client.py +++ b/tests/002_test_client.py @@ -16,6 +16,30 @@ def test_valid_init() -> None: assert client.seed == VALID_SEED.strip() assert client.api_key == VALID_API_KEY assert client.wallet_version == "V5R1" + assert client.api_provider == "tonapi" + + +# API provider tests + + +def test_api_provider_default_is_tonapi() -> None: + client = FragmentClient(seed=VALID_SEED, api_key=VALID_API_KEY, cookies=VALID_COOKIES) + assert client.api_provider == "tonapi" + + +def test_api_provider_toncenter() -> None: + client = FragmentClient(seed=VALID_SEED, api_key=VALID_API_KEY, cookies=VALID_COOKIES, api_provider="toncenter") + assert client.api_provider == "toncenter" + + +def test_api_provider_is_case_insensitive() -> None: + client = FragmentClient(seed=VALID_SEED, api_key=VALID_API_KEY, cookies=VALID_COOKIES, api_provider="TONAPI") + assert client.api_provider == "tonapi" + + +def test_unsupported_api_provider_raises() -> None: + with pytest.raises(ConfigurationError): + FragmentClient(seed=VALID_SEED, api_key=VALID_API_KEY, cookies=VALID_COOKIES, api_provider="infura") # Wallet version tests @@ -44,6 +68,11 @@ def test_missing_seed_raises() -> None: FragmentClient(seed="", api_key=VALID_API_KEY, cookies=VALID_COOKIES) +def test_both_seed_and_api_key_missing_raises() -> None: + with pytest.raises(ConfigurationError): + FragmentClient(seed="", api_key="", cookies=VALID_COOKIES) + + def test_whitespace_only_seed_raises() -> None: with pytest.raises(ConfigurationError): FragmentClient(seed=" ", api_key=VALID_API_KEY, cookies=VALID_COOKIES) @@ -105,6 +134,7 @@ def test_repr() -> None: r = repr(client) assert "FragmentClient" in r assert "V5R1" in r + assert "tonapi" in r assert "4 keys" in r diff --git a/tests/004_test_stars.py b/tests/004_test_stars.py index 97f73b0..45c0eba 100644 --- a/tests/004_test_stars.py +++ b/tests/004_test_stars.py @@ -236,3 +236,108 @@ async def test_giveaway_stars_channel_not_found(client: FragmentClient) -> None: with patch.object(client, "call", AsyncMock(return_value={"found": {}})): with pytest.raises(UserNotFoundError): await client.giveaway_stars("@ghost", winners=1, amount=500) + + +# Stars purchase — error branches + + +@pytest.mark.asyncio +async def test_purchase_stars_not_a_user_raises(client: FragmentClient) -> None: + with patch.object(client, "call", AsyncMock(return_value={"error": "Please enter a username assigned to a user."})): + with pytest.raises(UserNotFoundError, match="does not belong"): + await client.purchase_stars("@channel", amount=500) + + +@pytest.mark.asyncio +async def test_purchase_stars_missing_req_id_raises(client: FragmentClient) -> None: + with ( + patch.object( + client, + "call", + AsyncMock( + side_effect=[ + {"found": {"recipient": FAKE_RECIPIENT}}, + {}, # updateStarsBuyState + {"amount": "0.1"}, # initBuyStarsRequest — no req_id + ] + ), + ), + patch.object(_purchase_stars_mod, "get_account_info", AsyncMock(return_value=FAKE_ACCOUNT)), + ): + from pyfragment.exceptions import FragmentAPIError + + with pytest.raises(FragmentAPIError): + await client.purchase_stars("@user", amount=500) + + +@pytest.mark.asyncio +async def test_purchase_stars_need_verify_raises(client: FragmentClient) -> None: + with ( + patch.object( + client, + "call", + AsyncMock( + side_effect=[ + {"found": {"recipient": FAKE_RECIPIENT}}, + {}, # updateStarsBuyState + {"req_id": FAKE_REQ_ID}, + {"need_verify": True}, # getBuyStarsLink + ] + ), + ), + patch.object(_purchase_stars_mod, "get_account_info", AsyncMock(return_value=FAKE_ACCOUNT)), + ): + from pyfragment.exceptions import VerificationError + + with pytest.raises(VerificationError): + await client.purchase_stars("@user", amount=500) + + +# Stars giveaway — error branches + + +@pytest.mark.asyncio +async def test_giveaway_stars_missing_req_id_raises(client: FragmentClient) -> None: + with ( + patch.object( + client, + "call", + AsyncMock( + side_effect=[ + {"found": {"recipient": FAKE_RECIPIENT}}, + {}, # updateStarsGiveawayState + {}, # updateStarsGiveawayPrices + {"amount": "0.1"}, # initGiveawayStarsRequest — no req_id + ] + ), + ), + patch.object(_giveaway_stars_mod, "get_account_info", AsyncMock(return_value=FAKE_ACCOUNT)), + ): + from pyfragment.exceptions import FragmentAPIError + + with pytest.raises(FragmentAPIError): + await client.giveaway_stars("@channel", winners=3, amount=1000) + + +@pytest.mark.asyncio +async def test_giveaway_stars_need_verify_raises(client: FragmentClient) -> None: + with ( + patch.object( + client, + "call", + AsyncMock( + side_effect=[ + {"found": {"recipient": FAKE_RECIPIENT}}, + {}, # updateStarsGiveawayState + {}, # updateStarsGiveawayPrices + {"req_id": FAKE_REQ_ID}, + {"need_verify": True}, # getGiveawayStarsLink + ] + ), + ), + patch.object(_giveaway_stars_mod, "get_account_info", AsyncMock(return_value=FAKE_ACCOUNT)), + ): + from pyfragment.exceptions import VerificationError + + with pytest.raises(VerificationError): + await client.giveaway_stars("@channel", winners=3, amount=1000) diff --git a/tests/005_test_premium.py b/tests/005_test_premium.py index 64af05b..1655823 100644 --- a/tests/005_test_premium.py +++ b/tests/005_test_premium.py @@ -234,3 +234,108 @@ async def test_giveaway_premium_channel_not_found(client: FragmentClient) -> Non with patch.object(client, "call", AsyncMock(return_value={"found": {}})): with pytest.raises(UserNotFoundError): await client.giveaway_premium("@ghost", winners=1, months=3) + + +# Premium purchase — error branches + + +@pytest.mark.asyncio +async def test_purchase_premium_not_a_user_raises(client: FragmentClient) -> None: + with patch.object(client, "call", AsyncMock(return_value={"error": "Please enter a username assigned to a user."})): + with pytest.raises(UserNotFoundError, match="does not belong"): + await client.purchase_premium("@channel", months=3) + + +@pytest.mark.asyncio +async def test_purchase_premium_missing_req_id_raises(client: FragmentClient) -> None: + from pyfragment.exceptions import FragmentAPIError + + with ( + patch.object( + client, + "call", + AsyncMock( + side_effect=[ + {"found": {"recipient": FAKE_RECIPIENT}}, + {}, # updatePremiumState + {"amount": "0.1"}, # initGiftPremiumRequest — no req_id + ] + ), + ), + patch.object(_purchase_premium_mod, "get_account_info", AsyncMock(return_value=FAKE_ACCOUNT)), + ): + with pytest.raises(FragmentAPIError): + await client.purchase_premium("@user", months=3) + + +@pytest.mark.asyncio +async def test_purchase_premium_need_verify_raises(client: FragmentClient) -> None: + from pyfragment.exceptions import VerificationError + + with ( + patch.object( + client, + "call", + AsyncMock( + side_effect=[ + {"found": {"recipient": FAKE_RECIPIENT}}, + {}, # updatePremiumState + {"req_id": FAKE_REQ_ID}, + {"need_verify": True}, # getGiftPremiumLink + ] + ), + ), + patch.object(_purchase_premium_mod, "get_account_info", AsyncMock(return_value=FAKE_ACCOUNT)), + ): + with pytest.raises(VerificationError): + await client.purchase_premium("@user", months=3) + + +# Premium giveaway — error branches + + +@pytest.mark.asyncio +async def test_giveaway_premium_missing_req_id_raises(client: FragmentClient) -> None: + from pyfragment.exceptions import FragmentAPIError + + with ( + patch.object( + client, + "call", + AsyncMock( + side_effect=[ + {"found": {"recipient": FAKE_RECIPIENT}}, + {}, # updatePremiumGiveawayState + {}, # updatePremiumGiveawayPrices + {"amount": "0.1"}, # initGiveawayPremiumRequest — no req_id + ] + ), + ), + patch.object(_giveaway_premium_mod, "get_account_info", AsyncMock(return_value=FAKE_ACCOUNT)), + ): + with pytest.raises(FragmentAPIError): + await client.giveaway_premium("@channel", winners=10, months=3) + + +@pytest.mark.asyncio +async def test_giveaway_premium_need_verify_raises(client: FragmentClient) -> None: + from pyfragment.exceptions import VerificationError + + with ( + patch.object( + client, + "call", + AsyncMock( + side_effect=[ + {"found": {"recipient": FAKE_RECIPIENT}}, + {}, # updatePremiumGiveawayState + {}, # updatePremiumGiveawayPrices + {"req_id": FAKE_REQ_ID}, + {"need_verify": True}, # getGiveawayPremiumLink + ] + ), + ), + patch.object(_giveaway_premium_mod, "get_account_info", AsyncMock(return_value=FAKE_ACCOUNT)), + ): + with pytest.raises(VerificationError): + await client.giveaway_premium("@channel", winners=10, months=3) diff --git a/tests/006_test_topup.py b/tests/006_test_topup.py index eea5bdc..6f88339 100644 --- a/tests/006_test_topup.py +++ b/tests/006_test_topup.py @@ -73,3 +73,51 @@ async def test_topup_gram_user_not_found(client: FragmentClient) -> None: ): with pytest.raises(UserNotFoundError): await client.topup_gram("@ghost", amount=10) + + +# topup_gram — error branches + + +@pytest.mark.asyncio +async def test_topup_gram_missing_req_id_raises(client: FragmentClient) -> None: + from pyfragment.exceptions import FragmentAPIError + + with ( + patch.object( + client, + "call", + AsyncMock( + side_effect=[ + {}, # updateAdsTopupState + {"found": {"recipient": FAKE_RECIPIENT}}, + {"amount": "0.1"}, # initAdsTopupRequest — no req_id + ] + ), + ), + patch.object(_topup_gram_mod, "get_account_info", AsyncMock(return_value=FAKE_ACCOUNT)), + ): + with pytest.raises(FragmentAPIError): + await client.topup_gram("@user", amount=10) + + +@pytest.mark.asyncio +async def test_topup_gram_need_verify_raises(client: FragmentClient) -> None: + from pyfragment.exceptions import VerificationError + + with ( + patch.object( + client, + "call", + AsyncMock( + side_effect=[ + {}, # updateAdsTopupState + {"found": {"recipient": FAKE_RECIPIENT}}, + {"req_id": FAKE_REQ_ID}, + {"need_verify": True}, # getAdsTopupLink + ] + ), + ), + patch.object(_topup_gram_mod, "get_account_info", AsyncMock(return_value=FAKE_ACCOUNT)), + ): + with pytest.raises(VerificationError): + await client.topup_gram("@user", amount=10) diff --git a/tests/007_test_wallet.py b/tests/007_test_wallet.py index ea07fdc..6d54b3d 100644 --- a/tests/007_test_wallet.py +++ b/tests/007_test_wallet.py @@ -58,3 +58,97 @@ async def test_get_wallet_balance_is_zero(client: FragmentClient) -> None: assert result.gram_balance == 0.0 assert result.usdt_balance == 0.0 assert result.state == "uninit" + + +# _make_ton_client — provider selection + + +def test_make_ton_client_uses_toncenter_for_toncenter_provider(client: FragmentClient) -> None: + from tonutils.clients import ToncenterClient + + from pyfragment.enums import ApiProvider + from pyfragment.services.tonapi.account import _make_ton_client + + client.api_provider = ApiProvider.TONCENTER + result = _make_ton_client(client) + assert isinstance(result, ToncenterClient) + + +def test_make_ton_client_uses_tonapi_for_tonapi_provider(client: FragmentClient) -> None: + from tonutils.clients import TonapiClient + + from pyfragment.enums import ApiProvider + from pyfragment.services.tonapi.account import _make_ton_client + + client.api_provider = ApiProvider.TONAPI + result = _make_ton_client(client) + assert isinstance(result, TonapiClient) + + +# get_usdt_balance — error paths + + +@pytest.mark.asyncio +async def test_get_usdt_balance_non_404_provider_error_raises() -> None: + from tonutils.exceptions import ProviderResponseError + + from pyfragment import WalletError + from pyfragment.services.tonapi.account import get_usdt_balance + + ton = MagicMock() + err = ProviderResponseError(code=500, message="server error", endpoint="api.tonapi.io") + + with patch("pyfragment.services.tonapi.account.get_wallet_address_get_method", AsyncMock(side_effect=err)): + with pytest.raises(WalletError): + await get_usdt_balance(ton, "0:abc") + + +@pytest.mark.asyncio +async def test_get_usdt_balance_generic_exception_raises() -> None: + from pyfragment import WalletError + from pyfragment.services.tonapi.account import get_usdt_balance + + ton = MagicMock() + + with patch( + "pyfragment.services.tonapi.account.get_wallet_address_get_method", AsyncMock(side_effect=RuntimeError("timeout")) + ): + with pytest.raises(WalletError): + await get_usdt_balance(ton, "0:abc") + + +# get_account_info / get_wallet_info — exception paths + + +@pytest.mark.asyncio +async def test_get_account_info_exception_raises_wallet_error(client: FragmentClient) -> None: + from pyfragment import WalletError + from pyfragment.services.tonapi.account import get_account_info + + with ( + patch("pyfragment.services.tonapi.account._make_ton_client") as mock_tonapi, + patch("pyfragment.services.tonapi.account.WALLET_CLASSES") as mock_classes, + ): + mock_tonapi.return_value.__aenter__ = AsyncMock(return_value=MagicMock()) + mock_tonapi.return_value.__aexit__ = AsyncMock(return_value=False) + mock_classes["V5R1"].from_mnemonic.side_effect = RuntimeError("wallet init failed") + + with pytest.raises(WalletError, match="account info"): + await get_account_info(client) + + +@pytest.mark.asyncio +async def test_get_wallet_info_exception_raises_wallet_error(client: FragmentClient) -> None: + from pyfragment import WalletError + from pyfragment.services.tonapi.account import get_wallet_info + + with ( + patch("pyfragment.services.tonapi.account._make_ton_client") as mock_tonapi, + patch("pyfragment.services.tonapi.account.WALLET_CLASSES") as mock_classes, + ): + mock_tonapi.return_value.__aenter__ = AsyncMock(return_value=MagicMock()) + mock_tonapi.return_value.__aexit__ = AsyncMock(return_value=False) + mock_classes["V5R1"].from_mnemonic.side_effect = RuntimeError("key derivation failed") + + with pytest.raises(WalletError, match="wallet info"): + await get_wallet_info(client) diff --git a/tests/010_test_recharge_ads.py b/tests/010_test_recharge_ads.py index bb31094..664344d 100644 --- a/tests/010_test_recharge_ads.py +++ b/tests/010_test_recharge_ads.py @@ -55,3 +55,49 @@ async def test_recharge_ads_success(client: FragmentClient) -> None: assert isinstance(result, AdsRechargeResult) assert result.transaction_id == FAKE_TX_HASH assert result.amount == 10 + + +# recharge_ads — error branches + + +@pytest.mark.asyncio +async def test_recharge_ads_missing_req_id_raises(client: FragmentClient) -> None: + from pyfragment.exceptions import FragmentAPIError + + with ( + patch.object( + client, + "call", + AsyncMock( + side_effect=[ + {}, # updateAdsState + {"amount": "0.1"}, # initAdsRechargeRequest — no req_id + ] + ), + ), + patch.object(_recharge_ads_mod, "get_account_info", AsyncMock(return_value=FAKE_ACCOUNT)), + ): + with pytest.raises(FragmentAPIError): + await client.recharge_ads(FAKE_ADS_ACCOUNT, amount=10) + + +@pytest.mark.asyncio +async def test_recharge_ads_need_verify_raises(client: FragmentClient) -> None: + from pyfragment.exceptions import VerificationError + + with ( + patch.object( + client, + "call", + AsyncMock( + side_effect=[ + {}, # updateAdsState + {"req_id": FAKE_REQ_ID}, + {"need_verify": True}, # getAdsRechargeLink + ] + ), + ), + patch.object(_recharge_ads_mod, "get_account_info", AsyncMock(return_value=FAKE_ACCOUNT)), + ): + with pytest.raises(VerificationError): + await client.recharge_ads(FAKE_ADS_ACCOUNT, amount=10)