feat: add payment method support for premium and stars purchases, including validation and tests

This commit is contained in:
bohd4nx
2026-05-11 11:57:52 +03:00
parent c1ae5f0f28
commit 72918a6dd6
11 changed files with 308 additions and 44 deletions
+23 -15
View File
@@ -15,6 +15,7 @@ from pyfragment.types import (
CookieError, CookieError,
CookieResult, CookieResult,
FragmentAPIError, FragmentAPIError,
# exceptions
FragmentError, FragmentError,
FragmentPageError, FragmentPageError,
GiftsResult, GiftsResult,
@@ -22,9 +23,12 @@ from pyfragment.types import (
NumbersResult, NumbersResult,
OperationError, OperationError,
ParseError, ParseError,
# literal types
PaymentMethod,
PremiumGiveawayResult, PremiumGiveawayResult,
PremiumResult, PremiumResult,
StarsGiveawayResult, StarsGiveawayResult,
# results
StarsResult, StarsResult,
TerminateSessionsResult, TerminateSessionsResult,
TransactionError, TransactionError,
@@ -41,31 +45,35 @@ __version__: str = version("pyfragment")
__all__ = [ __all__ = [
"__version__", "__version__",
"FragmentClient", "FragmentClient",
"AdsRechargeResult", # results
"StarsResult",
"StarsGiveawayResult",
"PremiumResult",
"PremiumGiveawayResult",
"WalletInfo",
"AdsTopupResult", "AdsTopupResult",
"AdsRechargeResult",
"CookieResult",
"GiftsResult", "GiftsResult",
"LoginCodeResult", "LoginCodeResult",
"NumbersResult", "NumbersResult",
"PremiumGiveawayResult",
"PremiumResult",
"StarsGiveawayResult",
"StarsResult",
"TerminateSessionsResult", "TerminateSessionsResult",
"UsernamesResult", "UsernamesResult",
"WalletInfo", # exceptions
"ClientError",
"ConfigurationError",
"CookieError",
"CookieResult",
"FragmentAPIError",
"FragmentError", "FragmentError",
"FragmentAPIError",
"FragmentPageError", "FragmentPageError",
"ConfigurationError",
"UserNotFoundError",
"WalletError",
"VerificationError",
"TransactionError",
"AnonymousNumberError", "AnonymousNumberError",
"ClientError",
"CookieError",
"OperationError", "OperationError",
"ParseError", "ParseError",
"TransactionError",
"UnexpectedError", "UnexpectedError",
"UserNotFoundError", # literal types
"VerificationError", "PaymentMethod",
"WalletError",
] ]
+25 -6
View File
@@ -36,6 +36,7 @@ from pyfragment.types.constants import (
FRAGMENT_BASE_URL, FRAGMENT_BASE_URL,
REQUIRED_COOKIE_KEYS, REQUIRED_COOKIE_KEYS,
SUPPORTED_WALLET_VERSIONS, SUPPORTED_WALLET_VERSIONS,
PaymentMethod,
WalletVersion, WalletVersion,
) )
from pyfragment.utils.http import fragment_request, get_fragment_hash, make_headers from pyfragment.utils.http import fragment_request, get_fragment_hash, make_headers
@@ -125,31 +126,45 @@ class FragmentClient:
def __repr__(self) -> str: def __repr__(self) -> str:
return f"FragmentClient(wallet_version='{self.wallet_version}', cookies={len(self.cookies)} keys)" 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. """Gift Telegram Premium to a user.
Args: Args:
username: Recipient's Telegram username (with or without ``@``). username: Recipient's Telegram username (with or without ``@``).
months: Duration — ``3``, ``6``, or ``12``. months: Duration — ``3``, ``6``, or ``12``.
show_sender: Show your name as the sender. Defaults to ``True``. show_sender: Show your name as the sender. Defaults to ``True``.
payment_method: Payment currency — ``"ton"`` (default) or ``"usdt_ton"``.
Returns: Returns:
:class:`PremiumResult` with ``transaction_id``, ``username``, and ``amount``. :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. """Send Telegram Stars to a user.
Args: Args:
username: Recipient's Telegram username (with or without ``@``). username: Recipient's Telegram username (with or without ``@``).
amount: Number of stars — integer from ``50`` to ``1 000 000``. amount: Number of stars — integer from ``50`` to ``1 000 000``.
show_sender: Show your name as the gift sender. Defaults to ``True``. show_sender: Show your name as the gift sender. Defaults to ``True``.
payment_method: Payment currency — ``"ton"`` (default) or ``"usdt_ton"``.
Returns: Returns:
:class:`StarsResult` with ``transaction_id``, ``username``, and ``amount``. :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: async def topup_ton(self, username: str, amount: int, show_sender: bool = True) -> AdsTopupResult:
"""Top up TON to a recipient's Telegram balance. """Top up TON to a recipient's Telegram balance.
@@ -191,6 +206,7 @@ class FragmentClient:
channel: str, channel: str,
winners: int, winners: int,
amount: int, amount: int,
payment_method: PaymentMethod = "ton",
) -> StarsGiveawayResult: ) -> StarsGiveawayResult:
"""Run a Telegram Stars giveaway for a channel. """Run a Telegram Stars giveaway for a channel.
@@ -198,18 +214,20 @@ class FragmentClient:
channel: Channel username (with or without ``@``). channel: Channel username (with or without ``@``).
winners: Number of winners — integer from ``1`` to ``5``. winners: Number of winners — integer from ``1`` to ``5``.
amount: Stars each winner receives — integer from ``500`` to ``1 000 000``. amount: Stars each winner receives — integer from ``500`` to ``1 000 000``.
payment_method: Payment currency — ``"ton"`` (default) or ``"usdt_ton"``.
Returns: Returns:
:class:`StarsGiveawayResult` with ``transaction_id``, ``channel``, :class:`StarsGiveawayResult` with ``transaction_id``, ``channel``,
``winners``, and ``amount``. ``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( async def giveaway_premium(
self, self,
channel: str, channel: str,
winners: int, winners: int,
months: int = 3, months: int = 3,
payment_method: PaymentMethod = "ton",
) -> PremiumGiveawayResult: ) -> PremiumGiveawayResult:
"""Run a Telegram Premium giveaway for a channel. """Run a Telegram Premium giveaway for a channel.
@@ -217,12 +235,13 @@ class FragmentClient:
channel: Channel username (with or without ``@``). channel: Channel username (with or without ``@``).
winners: Number of winners — positive integer. winners: Number of winners — positive integer.
months: Premium duration per winner — ``3``, ``6``, or ``12``. Defaults to ``3``. months: Premium duration per winner — ``3``, ``6``, or ``12``. Defaults to ``3``.
payment_method: Payment currency — ``"ton"`` (default) or ``"usdt_ton"``.
Returns: Returns:
:class:`PremiumGiveawayResult` with ``transaction_id``, ``channel``, :class:`PremiumGiveawayResult` with ``transaction_id``, ``channel``,
``winners``, and ``amount``. ``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: async def get_login_code(self, number: str) -> LoginCodeResult:
"""Fetch the current pending login code for an anonymous number. """Fetch the current pending login code for an anonymous number.
+16 -2
View File
@@ -12,7 +12,7 @@ from pyfragment.types import (
UserNotFoundError, UserNotFoundError,
VerificationError, 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 from pyfragment.utils import get_account_info, process_transaction
if TYPE_CHECKING: if TYPE_CHECKING:
@@ -24,6 +24,7 @@ async def giveaway_premium(
channel: str, channel: str,
winners: int, winners: int,
months: int = 3, months: int = 3,
payment_method: PaymentMethod = "ton",
) -> PremiumGiveawayResult: ) -> PremiumGiveawayResult:
"""Run a Telegram Premium giveaway for a channel. """Run a Telegram Premium giveaway for a channel.
@@ -32,6 +33,7 @@ async def giveaway_premium(
channel: Channel username (with or without ``@``). channel: Channel username (with or without ``@``).
winners: Number of winners — integer from ``1`` to ``24 000``. winners: Number of winners — integer from ``1`` to ``24 000``.
months: Premium duration per winner — ``3``, ``6``, or ``12``. Defaults to ``3``. months: Premium duration per winner — ``3``, ``6``, or ``12``. Defaults to ``3``.
payment_method: Payment currency — ``"ton"`` (default) or ``"usdt_ton"``.
Returns: Returns:
:class:`PremiumGiveawayResult` with ``transaction_id``, ``channel``, :class:`PremiumGiveawayResult` with ``transaction_id``, ``channel``,
@@ -47,6 +49,13 @@ async def giveaway_premium(
raise ConfigurationError(ConfigurationError.INVALID_WINNERS_PREMIUM) raise ConfigurationError(ConfigurationError.INVALID_WINNERS_PREMIUM)
if months not in (3, 6, 12): if months not in (3, 6, 12):
raise ConfigurationError(ConfigurationError.INVALID_MONTHS) 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: try:
result = await client.call( result = await client.call(
@@ -60,7 +69,12 @@ async def giveaway_premium(
result = await client.call( result = await client.call(
"initGiveawayPremiumRequest", "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, page_url=PREMIUM_GIVEAWAY_PAGE,
) )
req_id = result.get("req_id") req_id = result.get("req_id")
+16 -2
View File
@@ -12,7 +12,7 @@ from pyfragment.types import (
UserNotFoundError, UserNotFoundError,
VerificationError, 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 from pyfragment.utils import get_account_info, process_transaction
if TYPE_CHECKING: if TYPE_CHECKING:
@@ -24,6 +24,7 @@ async def giveaway_stars(
channel: str, channel: str,
winners: int, winners: int,
amount: int, amount: int,
payment_method: PaymentMethod = "ton",
) -> StarsGiveawayResult: ) -> StarsGiveawayResult:
"""Run a Telegram Stars giveaway for a channel. """Run a Telegram Stars giveaway for a channel.
@@ -32,6 +33,7 @@ async def giveaway_stars(
channel: Channel username (with or without ``@``). channel: Channel username (with or without ``@``).
winners: Number of winners — integer from ``1`` to ``5``. winners: Number of winners — integer from ``1`` to ``5``.
amount: Stars each winner receives — integer from ``500`` to ``1 000 000``. amount: Stars each winner receives — integer from ``500`` to ``1 000 000``.
payment_method: Payment currency — ``"ton"`` (default) or ``"usdt_ton"``.
Returns: Returns:
:class:`StarsGiveawayResult` with ``transaction_id``, ``channel``, :class:`StarsGiveawayResult` with ``transaction_id``, ``channel``,
@@ -47,6 +49,13 @@ async def giveaway_stars(
raise ConfigurationError(ConfigurationError.INVALID_WINNERS_STARS) raise ConfigurationError(ConfigurationError.INVALID_WINNERS_STARS)
if not isinstance(amount, int) or not (500 <= amount <= 1_000_000): if not isinstance(amount, int) or not (500 <= amount <= 1_000_000):
raise ConfigurationError(ConfigurationError.INVALID_STARS_PER_WINNER) 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: try:
result = await client.call("searchStarsGiveawayRecipient", {"query": channel}, page_url=STARS_GIVEAWAY_PAGE) result = await client.call("searchStarsGiveawayRecipient", {"query": channel}, page_url=STARS_GIVEAWAY_PAGE)
@@ -56,7 +65,12 @@ async def giveaway_stars(
result = await client.call( result = await client.call(
"initGiveawayStarsRequest", "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, page_url=STARS_GIVEAWAY_PAGE,
) )
req_id = result.get("req_id") req_id = result.get("req_id")
+21 -3
View File
@@ -13,14 +13,20 @@ from pyfragment.types import (
UserNotFoundError, UserNotFoundError,
VerificationError, 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 from pyfragment.utils import get_account_info, process_transaction
if TYPE_CHECKING: if TYPE_CHECKING:
from pyfragment.client import FragmentClient 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. """Gift Telegram Premium to a user.
Args: Args:
@@ -28,6 +34,7 @@ async def purchase_premium(client: FragmentClient, username: str, months: int, s
username: Recipient's Telegram username (with or without ``@``). username: Recipient's Telegram username (with or without ``@``).
months: Premium duration — ``3``, ``6``, or ``12``. months: Premium duration — ``3``, ``6``, or ``12``.
show_sender: Show your name as the gift sender. Defaults to ``True``. show_sender: Show your name as the gift sender. Defaults to ``True``.
payment_method: Payment currency — ``"ton"`` (default) or ``"usdt_ton"``.
Returns: Returns:
:class:`PremiumResult` with ``transaction_id``, ``username``, and ``amount``. :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): if months not in (3, 6, 12):
raise ConfigurationError(ConfigurationError.INVALID_MONTHS) 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: try:
result = await client.call("searchPremiumGiftRecipient", {"query": username, "months": months}, page_url=PREMIUM_PAGE) 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()))}, {"mode": "new", "lv": "false", "dh": str(int(time.time()))},
page_url=PREMIUM_PAGE, 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") req_id = result.get("req_id")
if not req_id: if not req_id:
raise FragmentAPIError(FragmentAPIError.NO_REQUEST_ID.format(context="Premium purchase")) raise FragmentAPIError(FragmentAPIError.NO_REQUEST_ID.format(context="Premium purchase"))
+23 -3
View File
@@ -1,6 +1,7 @@
from __future__ import annotations from __future__ import annotations
import json import json
import time
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from pyfragment.types import ( from pyfragment.types import (
@@ -12,14 +13,16 @@ from pyfragment.types import (
UserNotFoundError, UserNotFoundError,
VerificationError, 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 from pyfragment.utils import get_account_info, process_transaction
if TYPE_CHECKING: if TYPE_CHECKING:
from pyfragment.client import FragmentClient 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. """Send Telegram Stars to a user.
Args: Args:
@@ -27,6 +30,7 @@ async def purchase_stars(client: FragmentClient, username: str, amount: int, sho
username: Recipient's Telegram username (with or without ``@``). username: Recipient's Telegram username (with or without ``@``).
amount: Number of Stars to send — integer from ``50`` to ``1 000 000``. 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``. show_sender: Show your name as the gift sender. Defaults to ``True``.
payment_method: Payment currency — ``"ton"`` (default) or ``"usdt_ton"``.
Returns: Returns:
:class:`StarsResult` with ``transaction_id``, ``username``, and ``amount``. :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): if not isinstance(amount, int) or not (50 <= amount <= 1_000_000):
raise ConfigurationError(ConfigurationError.INVALID_STARS_AMOUNT) 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: try:
result = await client.call("searchStarsRecipient", {"query": username, "quantity": ""}, page_url=STARS_PAGE) 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: if not recipient:
raise UserNotFoundError(UserNotFoundError.NOT_FOUND.format(username=username)) 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") req_id = result.get("req_id")
if not req_id: if not req_id:
raise FragmentAPIError(FragmentAPIError.NO_REQUEST_ID.format(context="Stars purchase")) raise FragmentAPIError(FragmentAPIError.NO_REQUEST_ID.format(context="Stars purchase"))
+3
View File
@@ -1,3 +1,4 @@
from pyfragment.types.constants import PaymentMethod
from pyfragment.types.exceptions import ( from pyfragment.types.exceptions import (
AnonymousNumberError, AnonymousNumberError,
ClientError, ClientError,
@@ -61,4 +62,6 @@ __all__ = [
"TerminateSessionsResult", "TerminateSessionsResult",
"UsernamesResult", "UsernamesResult",
"WalletInfo", "WalletInfo",
# literal types
"PaymentMethod",
] ]
+9 -2
View File
@@ -5,6 +5,10 @@ from typing import Any, Literal, get_args
from tonutils.contracts.wallet import WalletV4R2, WalletV5R1 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 # Single source of truth for supported wallet versions
WalletVersion = Literal["V4R2", "V5R1"] WalletVersion = Literal["V4R2", "V5R1"]
SUPPORTED_WALLET_VERSIONS: frozenset[str] = frozenset(get_args(WalletVersion)) 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", "content-type": "application/x-www-form-urlencoded; charset=UTF-8",
"origin": FRAGMENT_BASE_URL, "origin": FRAGMENT_BASE_URL,
"priority": "u=1, i", "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-dest": "empty",
"sec-fetch-mode": "cors", "sec-fetch-mode": "cors",
"sec-fetch-site": "same-origin", "sec-fetch-site": "same-origin",
"user-agent": ( "user-agent": (
"Mozilla/5.0 (iPhone; CPU iPhone OS 18_5 like Mac OS X) " "Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) "
"AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.5 Mobile/15E148 Safari/604.1" "AppleWebKit/537.36 (KHTML, like Gecko) Chrome/147.0.0.0 Mobile Safari/537.36"
), ),
"x-requested-with": "XMLHttpRequest", "x-requested-with": "XMLHttpRequest",
} }
+1
View File
@@ -28,6 +28,7 @@ class ConfigurationError(ClientError):
INVALID_WINNERS_STARS = "Invalid winners count: must be an integer between 1 and 5." 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_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_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): class CookieError(ClientError):
+90 -11
View File
@@ -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] 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 # Stars purchase mocked tests
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_purchase_stars_success(client: FragmentClient) -> None: 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 ( with (
patch.object( patch.object(client, "call", call_mock),
client,
"call",
AsyncMock(
side_effect=[
{"found": {"recipient": FAKE_RECIPIENT}},
{"req_id": FAKE_REQ_ID},
FAKE_TRANSACTION,
]
),
),
patch.object(_purchase_stars_mod, "get_account_info", AsyncMock(return_value=FAKE_ACCOUNT)), 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)), 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 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 @pytest.mark.asyncio
async def test_purchase_stars_user_not_found(client: FragmentClient) -> None: async def test_purchase_stars_user_not_found(client: FragmentClient) -> None:
with patch.object(client, "call", AsyncMock(return_value={"found": {}})): 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] 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 # Stars giveaway mocked tests
@@ -135,6 +180,40 @@ async def test_giveaway_stars_success(client: FragmentClient) -> None:
assert result.amount == 1000 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 @pytest.mark.asyncio
async def test_giveaway_stars_channel_not_found(client: FragmentClient) -> None: async def test_giveaway_stars_channel_not_found(client: FragmentClient) -> None:
with patch.object(client, "call", AsyncMock(return_value={"found": {}})): with patch.object(client, "call", AsyncMock(return_value={"found": {}})):
+81
View File
@@ -26,6 +26,12 @@ async def test_purchase_premium_months_zero(client: FragmentClient) -> None:
await client.purchase_premium("@user", months=0) 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 # Premium purchase mocked tests
@@ -55,6 +61,41 @@ async def test_purchase_premium_success(client: FragmentClient) -> None:
assert result.amount == 3 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 @pytest.mark.asyncio
async def test_purchase_premium_user_not_found(client: FragmentClient) -> None: async def test_purchase_premium_user_not_found(client: FragmentClient) -> None:
with patch.object(client, "call", AsyncMock(return_value={"found": {}})): 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) 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 # Premium giveaway mocked tests
@@ -118,6 +165,40 @@ async def test_giveaway_premium_success(client: FragmentClient) -> None:
assert result.amount == 3 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 @pytest.mark.asyncio
async def test_giveaway_premium_channel_not_found(client: FragmentClient) -> None: async def test_giveaway_premium_channel_not_found(client: FragmentClient) -> None:
with patch.object(client, "call", AsyncMock(return_value={"found": {}})): with patch.object(client, "call", AsyncMock(return_value={"found": {}})):