From 72918a6dd6b7b38d0ec681d8835e5c788598b905 Mon Sep 17 00:00:00 2001 From: bohd4nx Date: Mon, 11 May 2026 11:57:52 +0300 Subject: [PATCH] feat: add payment method support for premium and stars purchases, including validation and tests --- pyfragment/__init__.py | 38 ++++++---- pyfragment/client.py | 31 ++++++-- pyfragment/methods/giveaway_premium.py | 18 ++++- pyfragment/methods/giveaway_stars.py | 18 ++++- pyfragment/methods/purchase_premium.py | 24 +++++- pyfragment/methods/purchase_stars.py | 26 ++++++- pyfragment/types/__init__.py | 3 + pyfragment/types/constants.py | 11 ++- pyfragment/types/exceptions.py | 1 + tests/004_test_stars.py | 101 ++++++++++++++++++++++--- tests/005_test_premium.py | 81 ++++++++++++++++++++ 11 files changed, 308 insertions(+), 44 deletions(-) diff --git a/pyfragment/__init__.py b/pyfragment/__init__.py index daee3af..78c492b 100644 --- a/pyfragment/__init__.py +++ b/pyfragment/__init__.py @@ -15,6 +15,7 @@ from pyfragment.types import ( CookieError, CookieResult, FragmentAPIError, + # exceptions FragmentError, FragmentPageError, GiftsResult, @@ -22,9 +23,12 @@ from pyfragment.types import ( NumbersResult, OperationError, ParseError, + # literal types + PaymentMethod, PremiumGiveawayResult, PremiumResult, StarsGiveawayResult, + # results StarsResult, TerminateSessionsResult, TransactionError, @@ -41,31 +45,35 @@ __version__: str = version("pyfragment") __all__ = [ "__version__", "FragmentClient", - "AdsRechargeResult", + # results + "StarsResult", + "StarsGiveawayResult", + "PremiumResult", + "PremiumGiveawayResult", + "WalletInfo", "AdsTopupResult", + "AdsRechargeResult", + "CookieResult", "GiftsResult", "LoginCodeResult", "NumbersResult", - "PremiumGiveawayResult", - "PremiumResult", - "StarsGiveawayResult", - "StarsResult", "TerminateSessionsResult", "UsernamesResult", - "WalletInfo", - "ClientError", - "ConfigurationError", - "CookieError", - "CookieResult", - "FragmentAPIError", + # exceptions "FragmentError", + "FragmentAPIError", "FragmentPageError", + "ConfigurationError", + "UserNotFoundError", + "WalletError", + "VerificationError", + "TransactionError", "AnonymousNumberError", + "ClientError", + "CookieError", "OperationError", "ParseError", - "TransactionError", "UnexpectedError", - "UserNotFoundError", - "VerificationError", - "WalletError", + # literal types + "PaymentMethod", ] diff --git a/pyfragment/client.py b/pyfragment/client.py index c542d4c..43e9adc 100644 --- a/pyfragment/client.py +++ b/pyfragment/client.py @@ -36,6 +36,7 @@ from pyfragment.types.constants import ( FRAGMENT_BASE_URL, REQUIRED_COOKIE_KEYS, SUPPORTED_WALLET_VERSIONS, + PaymentMethod, WalletVersion, ) from pyfragment.utils.http import fragment_request, get_fragment_hash, make_headers @@ -125,31 +126,45 @@ class FragmentClient: def __repr__(self) -> str: return f"FragmentClient(wallet_version='{self.wallet_version}', cookies={len(self.cookies)} keys)" - async def purchase_premium(self, username: str, months: int, show_sender: bool = True) -> PremiumResult: + async def purchase_premium( + self, + username: str, + months: int, + show_sender: bool = True, + payment_method: PaymentMethod = "ton", + ) -> PremiumResult: """Gift Telegram Premium to a user. Args: username: Recipient's Telegram username (with or without ``@``). months: Duration — ``3``, ``6``, or ``12``. show_sender: Show your name as the sender. Defaults to ``True``. + payment_method: Payment currency — ``"ton"`` (default) or ``"usdt_ton"``. Returns: :class:`PremiumResult` with ``transaction_id``, ``username``, and ``amount``. """ - return await purchase_premium(self, username, months, show_sender) + return await purchase_premium(self, username, months, show_sender, payment_method) - async def purchase_stars(self, username: str, amount: int, show_sender: bool = True) -> StarsResult: + async def purchase_stars( + self, + username: str, + amount: int, + show_sender: bool = True, + payment_method: PaymentMethod = "ton", + ) -> StarsResult: """Send Telegram Stars to a user. Args: username: Recipient's Telegram username (with or without ``@``). amount: Number of stars — integer from ``50`` to ``1 000 000``. show_sender: Show your name as the gift sender. Defaults to ``True``. + payment_method: Payment currency — ``"ton"`` (default) or ``"usdt_ton"``. Returns: :class:`StarsResult` with ``transaction_id``, ``username``, and ``amount``. """ - return await purchase_stars(self, username, amount, show_sender) + return await purchase_stars(self, username, amount, show_sender, payment_method) async def topup_ton(self, username: str, amount: int, show_sender: bool = True) -> AdsTopupResult: """Top up TON to a recipient's Telegram balance. @@ -191,6 +206,7 @@ class FragmentClient: channel: str, winners: int, amount: int, + payment_method: PaymentMethod = "ton", ) -> StarsGiveawayResult: """Run a Telegram Stars giveaway for a channel. @@ -198,18 +214,20 @@ class FragmentClient: channel: Channel username (with or without ``@``). winners: Number of winners — integer from ``1`` to ``5``. amount: Stars each winner receives — integer from ``500`` to ``1 000 000``. + payment_method: Payment currency — ``"ton"`` (default) or ``"usdt_ton"``. Returns: :class:`StarsGiveawayResult` with ``transaction_id``, ``channel``, ``winners``, and ``amount``. """ - return await giveaway_stars(self, channel, winners, amount) + return await giveaway_stars(self, channel, winners, amount, payment_method) async def giveaway_premium( self, channel: str, winners: int, months: int = 3, + payment_method: PaymentMethod = "ton", ) -> PremiumGiveawayResult: """Run a Telegram Premium giveaway for a channel. @@ -217,12 +235,13 @@ class FragmentClient: channel: Channel username (with or without ``@``). winners: Number of winners — positive integer. months: Premium duration per winner — ``3``, ``6``, or ``12``. Defaults to ``3``. + payment_method: Payment currency — ``"ton"`` (default) or ``"usdt_ton"``. Returns: :class:`PremiumGiveawayResult` with ``transaction_id``, ``channel``, ``winners``, and ``amount``. """ - return await giveaway_premium(self, channel, winners, months) + return await giveaway_premium(self, channel, winners, months, payment_method) async def get_login_code(self, number: str) -> LoginCodeResult: """Fetch the current pending login code for an anonymous number. diff --git a/pyfragment/methods/giveaway_premium.py b/pyfragment/methods/giveaway_premium.py index 2c6675d..79534c4 100644 --- a/pyfragment/methods/giveaway_premium.py +++ b/pyfragment/methods/giveaway_premium.py @@ -12,7 +12,7 @@ from pyfragment.types import ( UserNotFoundError, VerificationError, ) -from pyfragment.types.constants import DEVICE, PREMIUM_GIVEAWAY_PAGE +from pyfragment.types.constants import DEVICE, PREMIUM_GIVEAWAY_PAGE, SUPPORTED_PAYMENT_METHODS, PaymentMethod from pyfragment.utils import get_account_info, process_transaction if TYPE_CHECKING: @@ -24,6 +24,7 @@ async def giveaway_premium( channel: str, winners: int, months: int = 3, + payment_method: PaymentMethod = "ton", ) -> PremiumGiveawayResult: """Run a Telegram Premium giveaway for a channel. @@ -32,6 +33,7 @@ async def giveaway_premium( channel: Channel username (with or without ``@``). winners: Number of winners — integer from ``1`` to ``24 000``. months: Premium duration per winner — ``3``, ``6``, or ``12``. Defaults to ``3``. + payment_method: Payment currency — ``"ton"`` (default) or ``"usdt_ton"``. Returns: :class:`PremiumGiveawayResult` with ``transaction_id``, ``channel``, @@ -47,6 +49,13 @@ async def giveaway_premium( raise ConfigurationError(ConfigurationError.INVALID_WINNERS_PREMIUM) if months not in (3, 6, 12): raise ConfigurationError(ConfigurationError.INVALID_MONTHS) + if payment_method not in SUPPORTED_PAYMENT_METHODS: + raise ConfigurationError( + ConfigurationError.INVALID_PAYMENT_METHOD.format( + method=payment_method, + supported=", ".join(sorted(SUPPORTED_PAYMENT_METHODS)), + ) + ) try: result = await client.call( @@ -60,7 +69,12 @@ async def giveaway_premium( result = await client.call( "initGiveawayPremiumRequest", - {"recipient": recipient, "quantity": str(winners), "months": str(months)}, + { + "recipient": recipient, + "quantity": str(winners), + "months": str(months), + "payment_method": payment_method, + }, page_url=PREMIUM_GIVEAWAY_PAGE, ) req_id = result.get("req_id") diff --git a/pyfragment/methods/giveaway_stars.py b/pyfragment/methods/giveaway_stars.py index 06646fb..77ed64d 100644 --- a/pyfragment/methods/giveaway_stars.py +++ b/pyfragment/methods/giveaway_stars.py @@ -12,7 +12,7 @@ from pyfragment.types import ( UserNotFoundError, VerificationError, ) -from pyfragment.types.constants import DEVICE, STARS_GIVEAWAY_PAGE +from pyfragment.types.constants import DEVICE, STARS_GIVEAWAY_PAGE, SUPPORTED_PAYMENT_METHODS, PaymentMethod from pyfragment.utils import get_account_info, process_transaction if TYPE_CHECKING: @@ -24,6 +24,7 @@ async def giveaway_stars( channel: str, winners: int, amount: int, + payment_method: PaymentMethod = "ton", ) -> StarsGiveawayResult: """Run a Telegram Stars giveaway for a channel. @@ -32,6 +33,7 @@ async def giveaway_stars( channel: Channel username (with or without ``@``). winners: Number of winners — integer from ``1`` to ``5``. amount: Stars each winner receives — integer from ``500`` to ``1 000 000``. + payment_method: Payment currency — ``"ton"`` (default) or ``"usdt_ton"``. Returns: :class:`StarsGiveawayResult` with ``transaction_id``, ``channel``, @@ -47,6 +49,13 @@ async def giveaway_stars( raise ConfigurationError(ConfigurationError.INVALID_WINNERS_STARS) if not isinstance(amount, int) or not (500 <= amount <= 1_000_000): raise ConfigurationError(ConfigurationError.INVALID_STARS_PER_WINNER) + if payment_method not in SUPPORTED_PAYMENT_METHODS: + raise ConfigurationError( + ConfigurationError.INVALID_PAYMENT_METHOD.format( + method=payment_method, + supported=", ".join(sorted(SUPPORTED_PAYMENT_METHODS)), + ) + ) try: result = await client.call("searchStarsGiveawayRecipient", {"query": channel}, page_url=STARS_GIVEAWAY_PAGE) @@ -56,7 +65,12 @@ async def giveaway_stars( result = await client.call( "initGiveawayStarsRequest", - {"recipient": recipient, "quantity": str(winners), "stars": str(amount)}, + { + "recipient": recipient, + "quantity": str(winners), + "stars": str(amount), + "payment_method": payment_method, + }, page_url=STARS_GIVEAWAY_PAGE, ) req_id = result.get("req_id") diff --git a/pyfragment/methods/purchase_premium.py b/pyfragment/methods/purchase_premium.py index 771cf8e..79edd42 100644 --- a/pyfragment/methods/purchase_premium.py +++ b/pyfragment/methods/purchase_premium.py @@ -13,14 +13,20 @@ from pyfragment.types import ( UserNotFoundError, VerificationError, ) -from pyfragment.types.constants import DEVICE, PREMIUM_PAGE +from pyfragment.types.constants import DEVICE, PREMIUM_PAGE, SUPPORTED_PAYMENT_METHODS, PaymentMethod from pyfragment.utils import get_account_info, process_transaction if TYPE_CHECKING: from pyfragment.client import FragmentClient -async def purchase_premium(client: FragmentClient, username: str, months: int, show_sender: bool = True) -> PremiumResult: +async def purchase_premium( + client: FragmentClient, + username: str, + months: int, + show_sender: bool = True, + payment_method: PaymentMethod = "ton", +) -> PremiumResult: """Gift Telegram Premium to a user. Args: @@ -28,6 +34,7 @@ async def purchase_premium(client: FragmentClient, username: str, months: int, s username: Recipient's Telegram username (with or without ``@``). months: Premium duration — ``3``, ``6``, or ``12``. show_sender: Show your name as the gift sender. Defaults to ``True``. + payment_method: Payment currency — ``"ton"`` (default) or ``"usdt_ton"``. Returns: :class:`PremiumResult` with ``transaction_id``, ``username``, and ``amount``. @@ -40,6 +47,13 @@ async def purchase_premium(client: FragmentClient, username: str, months: int, s """ if months not in (3, 6, 12): raise ConfigurationError(ConfigurationError.INVALID_MONTHS) + if payment_method not in SUPPORTED_PAYMENT_METHODS: + raise ConfigurationError( + ConfigurationError.INVALID_PAYMENT_METHOD.format( + method=payment_method, + supported=", ".join(sorted(SUPPORTED_PAYMENT_METHODS)), + ) + ) try: result = await client.call("searchPremiumGiftRecipient", {"query": username, "months": months}, page_url=PREMIUM_PAGE) @@ -52,7 +66,11 @@ async def purchase_premium(client: FragmentClient, username: str, months: int, s {"mode": "new", "lv": "false", "dh": str(int(time.time()))}, page_url=PREMIUM_PAGE, ) - result = await client.call("initGiftPremiumRequest", {"recipient": recipient, "months": months}, page_url=PREMIUM_PAGE) + result = await client.call( + "initGiftPremiumRequest", + {"recipient": recipient, "months": months, "payment_method": payment_method}, + page_url=PREMIUM_PAGE, + ) req_id = result.get("req_id") if not req_id: raise FragmentAPIError(FragmentAPIError.NO_REQUEST_ID.format(context="Premium purchase")) diff --git a/pyfragment/methods/purchase_stars.py b/pyfragment/methods/purchase_stars.py index 923682a..c6af17f 100644 --- a/pyfragment/methods/purchase_stars.py +++ b/pyfragment/methods/purchase_stars.py @@ -1,6 +1,7 @@ from __future__ import annotations import json +import time from typing import TYPE_CHECKING from pyfragment.types import ( @@ -12,14 +13,16 @@ from pyfragment.types import ( UserNotFoundError, VerificationError, ) -from pyfragment.types.constants import DEVICE, STARS_PAGE +from pyfragment.types.constants import DEVICE, STARS_PAGE, SUPPORTED_PAYMENT_METHODS, PaymentMethod from pyfragment.utils import get_account_info, process_transaction if TYPE_CHECKING: from pyfragment.client import FragmentClient -async def purchase_stars(client: FragmentClient, username: str, amount: int, show_sender: bool = True) -> StarsResult: +async def purchase_stars( + client: FragmentClient, username: str, amount: int, show_sender: bool = True, payment_method: PaymentMethod = "ton" +) -> StarsResult: """Send Telegram Stars to a user. Args: @@ -27,6 +30,7 @@ async def purchase_stars(client: FragmentClient, username: str, amount: int, sho username: Recipient's Telegram username (with or without ``@``). amount: Number of Stars to send — integer from ``50`` to ``1 000 000``. show_sender: Show your name as the gift sender. Defaults to ``True``. + payment_method: Payment currency — ``"ton"`` (default) or ``"usdt_ton"``. Returns: :class:`StarsResult` with ``transaction_id``, ``username``, and ``amount``. @@ -39,6 +43,13 @@ async def purchase_stars(client: FragmentClient, username: str, amount: int, sho """ if not isinstance(amount, int) or not (50 <= amount <= 1_000_000): raise ConfigurationError(ConfigurationError.INVALID_STARS_AMOUNT) + if payment_method not in SUPPORTED_PAYMENT_METHODS: + raise ConfigurationError( + ConfigurationError.INVALID_PAYMENT_METHOD.format( + method=payment_method, + supported=", ".join(sorted(SUPPORTED_PAYMENT_METHODS)), + ) + ) try: result = await client.call("searchStarsRecipient", {"query": username, "quantity": ""}, page_url=STARS_PAGE) @@ -46,7 +57,16 @@ async def purchase_stars(client: FragmentClient, username: str, amount: int, sho if not recipient: raise UserNotFoundError(UserNotFoundError.NOT_FOUND.format(username=username)) - result = await client.call("initBuyStarsRequest", {"recipient": recipient, "quantity": amount}, page_url=STARS_PAGE) + await client.call( + "updateStarsBuyState", + {"mode": "new", "lv": "false", "dh": str(int(time.time()))}, + page_url=STARS_PAGE, + ) + result = await client.call( + "initBuyStarsRequest", + {"recipient": recipient, "quantity": amount, "payment_method": payment_method}, + page_url=STARS_PAGE, + ) req_id = result.get("req_id") if not req_id: raise FragmentAPIError(FragmentAPIError.NO_REQUEST_ID.format(context="Stars purchase")) diff --git a/pyfragment/types/__init__.py b/pyfragment/types/__init__.py index 52526a6..e8c7ffe 100644 --- a/pyfragment/types/__init__.py +++ b/pyfragment/types/__init__.py @@ -1,3 +1,4 @@ +from pyfragment.types.constants import PaymentMethod from pyfragment.types.exceptions import ( AnonymousNumberError, ClientError, @@ -61,4 +62,6 @@ __all__ = [ "TerminateSessionsResult", "UsernamesResult", "WalletInfo", + # literal types + "PaymentMethod", ] diff --git a/pyfragment/types/constants.py b/pyfragment/types/constants.py index d552234..cd662d2 100644 --- a/pyfragment/types/constants.py +++ b/pyfragment/types/constants.py @@ -5,6 +5,10 @@ from typing import Any, Literal, get_args from tonutils.contracts.wallet import WalletV4R2, WalletV5R1 +# Payment methods +PaymentMethod = Literal["ton", "usdt_ton"] +SUPPORTED_PAYMENT_METHODS: frozenset[str] = frozenset(get_args(PaymentMethod)) + # Single source of truth for supported wallet versions WalletVersion = Literal["V4R2", "V5R1"] SUPPORTED_WALLET_VERSIONS: frozenset[str] = frozenset(get_args(WalletVersion)) @@ -74,12 +78,15 @@ BASE_HEADERS: dict[str, str] = { "content-type": "application/x-www-form-urlencoded; charset=UTF-8", "origin": FRAGMENT_BASE_URL, "priority": "u=1, i", + "sec-ch-ua": '"Google Chrome";v="147", "Not.A/Brand";v="8", "Chromium";v="147"', + "sec-ch-ua-mobile": "?1", + "sec-ch-ua-platform": '"Android"', "sec-fetch-dest": "empty", "sec-fetch-mode": "cors", "sec-fetch-site": "same-origin", "user-agent": ( - "Mozilla/5.0 (iPhone; CPU iPhone OS 18_5 like Mac OS X) " - "AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.5 Mobile/15E148 Safari/604.1" + "Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) " + "AppleWebKit/537.36 (KHTML, like Gecko) Chrome/147.0.0.0 Mobile Safari/537.36" ), "x-requested-with": "XMLHttpRequest", } diff --git a/pyfragment/types/exceptions.py b/pyfragment/types/exceptions.py index 5f34a29..dba3c1e 100644 --- a/pyfragment/types/exceptions.py +++ b/pyfragment/types/exceptions.py @@ -28,6 +28,7 @@ class ConfigurationError(ClientError): INVALID_WINNERS_STARS = "Invalid winners count: must be an integer between 1 and 5." INVALID_WINNERS_PREMIUM = "Invalid winners count: must be an integer between 1 and 24 000." INVALID_STARS_PER_WINNER = "Invalid Stars per winner: must be an integer between 500 and 1 000 000." + INVALID_PAYMENT_METHOD = "Invalid payment method '{method}'. Supported values: {supported}." class CookieError(ClientError): diff --git a/tests/004_test_stars.py b/tests/004_test_stars.py index e550c3c..ef99500 100644 --- a/tests/004_test_stars.py +++ b/tests/004_test_stars.py @@ -32,23 +32,27 @@ async def test_purchase_stars_float_amount(client: FragmentClient) -> None: await client.purchase_stars("@user", amount=100.5) # type: ignore[arg-type] +@pytest.mark.asyncio +async def test_purchase_stars_invalid_payment_method(client: FragmentClient) -> None: + with pytest.raises(ConfigurationError, match="Invalid payment method"): + await client.purchase_stars("@user", amount=500, payment_method="btc") # type: ignore[arg-type] + + # Stars purchase mocked tests @pytest.mark.asyncio async def test_purchase_stars_success(client: FragmentClient) -> None: + call_mock = AsyncMock( + side_effect=[ + {"found": {"recipient": FAKE_RECIPIENT}}, + {}, # updateStarsBuyState + {"req_id": FAKE_REQ_ID}, + FAKE_TRANSACTION, + ] + ) with ( - patch.object( - client, - "call", - AsyncMock( - side_effect=[ - {"found": {"recipient": FAKE_RECIPIENT}}, - {"req_id": FAKE_REQ_ID}, - FAKE_TRANSACTION, - ] - ), - ), + patch.object(client, "call", call_mock), patch.object(_purchase_stars_mod, "get_account_info", AsyncMock(return_value=FAKE_ACCOUNT)), patch.object(_purchase_stars_mod, "process_transaction", AsyncMock(return_value=FAKE_TX_HASH)), ): @@ -60,6 +64,41 @@ async def test_purchase_stars_success(client: FragmentClient) -> None: assert result.amount == 500 +@pytest.mark.asyncio +async def test_purchase_stars_passes_payment_method(client: FragmentClient) -> None: + call_mock = AsyncMock( + side_effect=[ + {"found": {"recipient": FAKE_RECIPIENT}}, + {}, # updateStarsBuyState + {"req_id": FAKE_REQ_ID}, + FAKE_TRANSACTION, + ] + ) + with ( + patch.object(client, "call", call_mock), + patch.object(_purchase_stars_mod, "get_account_info", AsyncMock(return_value=FAKE_ACCOUNT)), + patch.object(_purchase_stars_mod, "process_transaction", AsyncMock(return_value=FAKE_TX_HASH)), + ): + await client.purchase_stars("@user", amount=500, payment_method="usdt_ton") + + init_call = call_mock.await_args_list[2] + assert init_call.args[0] == "initBuyStarsRequest" + assert init_call.args[1]["payment_method"] == "usdt_ton" + + +@pytest.mark.asyncio +@pytest.mark.parametrize("query", ["@user", "monk", "https://t.me/monk"]) +async def test_purchase_stars_accepts_query_formats(client: FragmentClient, query: str) -> None: + call_mock = AsyncMock(return_value={"found": {}}) + with patch.object(client, "call", call_mock): + with pytest.raises(UserNotFoundError): + await client.purchase_stars(query, amount=500) + + search_call = call_mock.await_args_list[0] + assert search_call.args[0] == "searchStarsRecipient" + assert search_call.args[1]["query"] == query + + @pytest.mark.asyncio async def test_purchase_stars_user_not_found(client: FragmentClient) -> None: with patch.object(client, "call", AsyncMock(return_value={"found": {}})): @@ -106,6 +145,12 @@ async def test_giveaway_stars_float_amount(client: FragmentClient) -> None: await client.giveaway_stars("@channel", winners=1, amount=500.5) # type: ignore[arg-type] +@pytest.mark.asyncio +async def test_giveaway_stars_invalid_payment_method(client: FragmentClient) -> None: + with pytest.raises(ConfigurationError, match="Invalid payment method"): + await client.giveaway_stars("@channel", winners=1, amount=500, payment_method="btc") # type: ignore[arg-type] + + # Stars giveaway mocked tests @@ -135,6 +180,40 @@ async def test_giveaway_stars_success(client: FragmentClient) -> None: assert result.amount == 1000 +@pytest.mark.asyncio +async def test_giveaway_stars_passes_payment_method(client: FragmentClient) -> None: + call_mock = AsyncMock( + side_effect=[ + {"found": {"recipient": FAKE_RECIPIENT}}, + {"req_id": FAKE_REQ_ID}, + FAKE_TRANSACTION, + ] + ) + with ( + patch.object(client, "call", call_mock), + patch.object(_giveaway_stars_mod, "get_account_info", AsyncMock(return_value=FAKE_ACCOUNT)), + patch.object(_giveaway_stars_mod, "process_transaction", AsyncMock(return_value=FAKE_TX_HASH)), + ): + await client.giveaway_stars("@channel", winners=3, amount=1000, payment_method="usdt_ton") + + init_call = call_mock.await_args_list[1] + assert init_call.args[0] == "initGiveawayStarsRequest" + assert init_call.args[1]["payment_method"] == "usdt_ton" + + +@pytest.mark.asyncio +@pytest.mark.parametrize("query", ["@channel", "monk", "https://t.me/id2757542991"]) +async def test_giveaway_stars_accepts_query_formats(client: FragmentClient, query: str) -> None: + call_mock = AsyncMock(return_value={"found": {}}) + with patch.object(client, "call", call_mock): + with pytest.raises(UserNotFoundError): + await client.giveaway_stars(query, winners=1, amount=500) + + search_call = call_mock.await_args_list[0] + assert search_call.args[0] == "searchStarsGiveawayRecipient" + assert search_call.args[1]["query"] == query + + @pytest.mark.asyncio async def test_giveaway_stars_channel_not_found(client: FragmentClient) -> None: with patch.object(client, "call", AsyncMock(return_value={"found": {}})): diff --git a/tests/005_test_premium.py b/tests/005_test_premium.py index ba58d55..43df39e 100644 --- a/tests/005_test_premium.py +++ b/tests/005_test_premium.py @@ -26,6 +26,12 @@ async def test_purchase_premium_months_zero(client: FragmentClient) -> None: await client.purchase_premium("@user", months=0) +@pytest.mark.asyncio +async def test_purchase_premium_invalid_payment_method(client: FragmentClient) -> None: + with pytest.raises(ConfigurationError, match="Invalid payment method"): + await client.purchase_premium("@user", months=3, payment_method="btc") # type: ignore[arg-type] + + # Premium purchase mocked tests @@ -55,6 +61,41 @@ async def test_purchase_premium_success(client: FragmentClient) -> None: assert result.amount == 3 +@pytest.mark.asyncio +async def test_purchase_premium_passes_payment_method(client: FragmentClient) -> None: + call_mock = AsyncMock( + side_effect=[ + {"found": {"recipient": FAKE_RECIPIENT}}, + {}, # updatePremiumState + {"req_id": FAKE_REQ_ID}, + FAKE_TRANSACTION, + ] + ) + with ( + patch.object(client, "call", call_mock), + patch.object(_purchase_premium_mod, "get_account_info", AsyncMock(return_value=FAKE_ACCOUNT)), + patch.object(_purchase_premium_mod, "process_transaction", AsyncMock(return_value=FAKE_TX_HASH)), + ): + await client.purchase_premium("@user", months=6, payment_method="usdt_ton") + + init_call = call_mock.await_args_list[2] + assert init_call.args[0] == "initGiftPremiumRequest" + assert init_call.args[1]["payment_method"] == "usdt_ton" + + +@pytest.mark.asyncio +@pytest.mark.parametrize("query", ["@user", "monk", "https://t.me/monk"]) +async def test_purchase_premium_accepts_query_formats(client: FragmentClient, query: str) -> None: + call_mock = AsyncMock(return_value={"found": {}}) + with patch.object(client, "call", call_mock): + with pytest.raises(UserNotFoundError): + await client.purchase_premium(query, months=6) + + search_call = call_mock.await_args_list[0] + assert search_call.args[0] == "searchPremiumGiftRecipient" + assert search_call.args[1]["query"] == query + + @pytest.mark.asyncio async def test_purchase_premium_user_not_found(client: FragmentClient) -> None: with patch.object(client, "call", AsyncMock(return_value={"found": {}})): @@ -89,6 +130,12 @@ async def test_giveaway_premium_invalid_months(client: FragmentClient) -> None: await client.giveaway_premium("@channel", winners=10, months=5) +@pytest.mark.asyncio +async def test_giveaway_premium_invalid_payment_method(client: FragmentClient) -> None: + with pytest.raises(ConfigurationError, match="Invalid payment method"): + await client.giveaway_premium("@channel", winners=10, months=3, payment_method="btc") # type: ignore[arg-type] + + # Premium giveaway mocked tests @@ -118,6 +165,40 @@ async def test_giveaway_premium_success(client: FragmentClient) -> None: assert result.amount == 3 +@pytest.mark.asyncio +async def test_giveaway_premium_passes_payment_method(client: FragmentClient) -> None: + call_mock = AsyncMock( + side_effect=[ + {"found": {"recipient": FAKE_RECIPIENT}}, + {"req_id": FAKE_REQ_ID}, + FAKE_TRANSACTION, + ] + ) + with ( + patch.object(client, "call", call_mock), + patch.object(_giveaway_premium_mod, "get_account_info", AsyncMock(return_value=FAKE_ACCOUNT)), + patch.object(_giveaway_premium_mod, "process_transaction", AsyncMock(return_value=FAKE_TX_HASH)), + ): + await client.giveaway_premium("@channel", winners=10, months=6, payment_method="usdt_ton") + + init_call = call_mock.await_args_list[1] + assert init_call.args[0] == "initGiveawayPremiumRequest" + assert init_call.args[1]["payment_method"] == "usdt_ton" + + +@pytest.mark.asyncio +@pytest.mark.parametrize("query", ["@channel", "monk", "https://t.me/id2757542991"]) +async def test_giveaway_premium_accepts_query_formats(client: FragmentClient, query: str) -> None: + call_mock = AsyncMock(return_value={"found": {}}) + with patch.object(client, "call", call_mock): + with pytest.raises(UserNotFoundError): + await client.giveaway_premium(query, winners=10, months=3) + + search_call = call_mock.await_args_list[0] + assert search_call.args[0] == "searchPremiumGiveawayRecipient" + assert search_call.args[1]["query"] == query + + @pytest.mark.asyncio async def test_giveaway_premium_channel_not_found(client: FragmentClient) -> None: with patch.object(client, "call", AsyncMock(return_value={"found": {}})):