mirror of
https://github.com/bohd4nx/FragmentAPI.git
synced 2026-07-25 06:14:29 +00:00
feat: Enhance cookie handling and browser support
- Added new exceptions in `exceptions.py` for unsupported browsers and cookie read failures. - Introduced `get_cookies_from_browser` function in `cookies.py` to extract session cookies from various browsers. - Modified tests to cover new cookie extraction functionality, including success and failure cases. - Refactored existing tests to use `patch.object` for mocking client calls. - Updated dependencies in `pyproject.toml` to include `rookiepy` for cookie extraction.
This commit is contained in:
@@ -20,7 +20,7 @@ jobs:
|
||||
with:
|
||||
python-version: "3.12"
|
||||
|
||||
- uses: astral-sh/setup-uv@v7.6.0
|
||||
- uses: astral-sh/setup-uv@v8.0.0
|
||||
|
||||
- run: uv pip install --system ".[dev]"
|
||||
|
||||
|
||||
@@ -34,12 +34,14 @@ from pyfragment.types import (
|
||||
WalletError,
|
||||
WalletInfo,
|
||||
)
|
||||
from pyfragment.utils.cookies import get_cookies_from_browser
|
||||
|
||||
__version__: str = version("pyfragment")
|
||||
|
||||
__all__ = [
|
||||
"__version__",
|
||||
"FragmentClient",
|
||||
"get_cookies_from_browser",
|
||||
"AdsRechargeResult",
|
||||
"AdsTopupResult",
|
||||
"GiftsResult",
|
||||
|
||||
@@ -1,18 +1,14 @@
|
||||
import html
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import httpx
|
||||
|
||||
from pyfragment.types import AnonymousNumberError, FragmentAPIError, FragmentError, UnexpectedError
|
||||
from pyfragment.types.constants import NUMBERS_PAGE
|
||||
from pyfragment.types.results import LoginCodeResult, TerminateSessionsResult
|
||||
from pyfragment.utils import fragment_request, get_fragment_hash, make_headers, parse_login_code
|
||||
from pyfragment.utils import parse_login_code
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from pyfragment.client import FragmentClient
|
||||
|
||||
HEADERS: dict[str, str] = make_headers(NUMBERS_PAGE)
|
||||
|
||||
|
||||
def _strip_plus(number: str) -> str:
|
||||
return number.lstrip("+") if isinstance(number, str) else number
|
||||
@@ -35,13 +31,10 @@ async def get_login_code(client: "FragmentClient", number: str) -> LoginCodeResu
|
||||
"""
|
||||
try:
|
||||
clean = _strip_plus(number)
|
||||
async with httpx.AsyncClient(cookies=client.cookies, timeout=client.timeout) as session:
|
||||
fragment_hash = await get_fragment_hash(client.cookies, HEADERS, NUMBERS_PAGE, client.timeout)
|
||||
result = await fragment_request(
|
||||
session,
|
||||
fragment_hash,
|
||||
HEADERS,
|
||||
{"number": clean, "lt": "0", "from_app": "1", "method": "updateLoginCodes"},
|
||||
result = await client.call(
|
||||
"updateLoginCodes",
|
||||
{"number": clean, "lt": "0", "from_app": "1"},
|
||||
page_url=NUMBERS_PAGE,
|
||||
)
|
||||
|
||||
if result.get("html"):
|
||||
@@ -71,13 +64,10 @@ async def toggle_login_codes(client: "FragmentClient", number: str, can_receive:
|
||||
"""
|
||||
try:
|
||||
clean = _strip_plus(number)
|
||||
async with httpx.AsyncClient(cookies=client.cookies, timeout=client.timeout) as session:
|
||||
fragment_hash = await get_fragment_hash(client.cookies, HEADERS, NUMBERS_PAGE, client.timeout)
|
||||
result = await fragment_request(
|
||||
session,
|
||||
fragment_hash,
|
||||
HEADERS,
|
||||
{"number": clean, "can_receive": 1 if can_receive else 0, "method": "toggleLoginCodes"},
|
||||
result = await client.call(
|
||||
"toggleLoginCodes",
|
||||
{"number": clean, "can_receive": 1 if can_receive else 0},
|
||||
page_url=NUMBERS_PAGE,
|
||||
)
|
||||
|
||||
if result.get("error"):
|
||||
@@ -110,15 +100,11 @@ async def terminate_sessions(client: "FragmentClient", number: str) -> Terminate
|
||||
"""
|
||||
try:
|
||||
clean = _strip_plus(number)
|
||||
async with httpx.AsyncClient(cookies=client.cookies, timeout=client.timeout) as session:
|
||||
fragment_hash = await get_fragment_hash(client.cookies, HEADERS, NUMBERS_PAGE, client.timeout)
|
||||
|
||||
# Step 1: initiate — Fragment returns a confirmation hash.
|
||||
confirmation = await fragment_request(
|
||||
session,
|
||||
fragment_hash,
|
||||
HEADERS,
|
||||
{"number": clean, "method": "terminatePhoneSessions"},
|
||||
confirmation = await client.call(
|
||||
"terminatePhoneSessions",
|
||||
{"number": clean},
|
||||
page_url=NUMBERS_PAGE,
|
||||
)
|
||||
|
||||
if confirmation.get("error"):
|
||||
@@ -130,12 +116,10 @@ async def terminate_sessions(client: "FragmentClient", number: str) -> Terminate
|
||||
if not terminate_hash:
|
||||
raise AnonymousNumberError(AnonymousNumberError.NOT_OWNED.format(number=number))
|
||||
|
||||
# Step 2: confirm with the hash.
|
||||
result = await fragment_request(
|
||||
session,
|
||||
fragment_hash,
|
||||
HEADERS,
|
||||
{"number": clean, "terminate_hash": terminate_hash, "method": "terminatePhoneSessions"},
|
||||
result = await client.call(
|
||||
"terminatePhoneSessions",
|
||||
{"number": clean, "terminate_hash": terminate_hash},
|
||||
page_url=NUMBERS_PAGE,
|
||||
)
|
||||
|
||||
if result.get("error"):
|
||||
|
||||
@@ -1,79 +1,21 @@
|
||||
import json
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import httpx
|
||||
|
||||
from pyfragment.types import (
|
||||
ConfigurationError,
|
||||
FragmentAPIError,
|
||||
FragmentError,
|
||||
UnexpectedError,
|
||||
UserNotFoundError,
|
||||
VerificationError,
|
||||
)
|
||||
from pyfragment.types.constants import DEVICE, PREMIUM_GIVEAWAY_PAGE
|
||||
from pyfragment.types.results import PremiumGiveawayResult
|
||||
from pyfragment.utils import (
|
||||
execute_transaction_request,
|
||||
fragment_request,
|
||||
get_account_info,
|
||||
get_fragment_hash,
|
||||
make_headers,
|
||||
process_transaction,
|
||||
)
|
||||
from pyfragment.utils import get_account_info, process_transaction
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from pyfragment.client import FragmentClient
|
||||
|
||||
HEADERS: dict[str, str] = make_headers(PREMIUM_GIVEAWAY_PAGE)
|
||||
|
||||
|
||||
async def _search_recipient(
|
||||
session: httpx.AsyncClient,
|
||||
fragment_hash: str,
|
||||
channel: str,
|
||||
winners: int,
|
||||
months: int,
|
||||
) -> str:
|
||||
result = await fragment_request(
|
||||
session,
|
||||
fragment_hash,
|
||||
HEADERS,
|
||||
{
|
||||
"query": channel,
|
||||
"quantity": winners,
|
||||
"months": months,
|
||||
"method": "searchPremiumGiveawayRecipient",
|
||||
},
|
||||
)
|
||||
recipient = result.get("found", {}).get("recipient")
|
||||
if not recipient:
|
||||
raise UserNotFoundError(UserNotFoundError.NOT_FOUND.format(username=channel))
|
||||
return recipient
|
||||
|
||||
|
||||
async def _init_request(
|
||||
session: httpx.AsyncClient,
|
||||
fragment_hash: str,
|
||||
recipient: str,
|
||||
winners: int,
|
||||
months: int,
|
||||
) -> str:
|
||||
result = await fragment_request(
|
||||
session,
|
||||
fragment_hash,
|
||||
HEADERS,
|
||||
{
|
||||
"recipient": recipient,
|
||||
"quantity": str(winners),
|
||||
"months": str(months),
|
||||
"method": "initGiveawayPremiumRequest",
|
||||
},
|
||||
)
|
||||
req_id = result.get("req_id")
|
||||
if not req_id:
|
||||
raise FragmentAPIError(FragmentAPIError.NO_REQUEST_ID.format(context="Premium giveaway"))
|
||||
return req_id
|
||||
|
||||
|
||||
async def giveaway_premium(
|
||||
client: "FragmentClient",
|
||||
@@ -105,21 +47,37 @@ async def giveaway_premium(
|
||||
raise ConfigurationError(ConfigurationError.INVALID_MONTHS)
|
||||
|
||||
try:
|
||||
fragment_hash = await get_fragment_hash(client.cookies, HEADERS, PREMIUM_GIVEAWAY_PAGE, client.timeout)
|
||||
result = await client.call(
|
||||
"searchPremiumGiveawayRecipient",
|
||||
{"query": channel, "quantity": winners, "months": months},
|
||||
page_url=PREMIUM_GIVEAWAY_PAGE,
|
||||
)
|
||||
recipient = result.get("found", {}).get("recipient")
|
||||
if not recipient:
|
||||
raise UserNotFoundError(UserNotFoundError.NOT_FOUND.format(username=channel))
|
||||
|
||||
result = await client.call(
|
||||
"initGiveawayPremiumRequest",
|
||||
{"recipient": recipient, "quantity": str(winners), "months": str(months)},
|
||||
page_url=PREMIUM_GIVEAWAY_PAGE,
|
||||
)
|
||||
req_id = result.get("req_id")
|
||||
if not req_id:
|
||||
raise FragmentAPIError(FragmentAPIError.NO_REQUEST_ID.format(context="Premium giveaway"))
|
||||
|
||||
account = await get_account_info(client)
|
||||
|
||||
async with httpx.AsyncClient(cookies=client.cookies, timeout=client.timeout) as session:
|
||||
recipient = await _search_recipient(session, fragment_hash, channel, winners, months)
|
||||
req_id = await _init_request(session, fragment_hash, recipient, winners, months)
|
||||
|
||||
tx_data = {
|
||||
transaction = await client.call(
|
||||
"getGiveawayPremiumLink",
|
||||
{
|
||||
"account": json.dumps(account),
|
||||
"device": DEVICE,
|
||||
"transaction": 1,
|
||||
"id": req_id,
|
||||
"method": "getGiveawayPremiumLink",
|
||||
}
|
||||
transaction = await execute_transaction_request(session, HEADERS, tx_data, fragment_hash)
|
||||
},
|
||||
page_url=PREMIUM_GIVEAWAY_PAGE,
|
||||
)
|
||||
if transaction.get("need_verify"):
|
||||
raise VerificationError(VerificationError.KYC_REQUIRED)
|
||||
|
||||
tx_hash = await process_transaction(client, transaction)
|
||||
return PremiumGiveawayResult(
|
||||
|
||||
@@ -1,75 +1,21 @@
|
||||
import json
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import httpx
|
||||
|
||||
from pyfragment.types import (
|
||||
ConfigurationError,
|
||||
FragmentAPIError,
|
||||
FragmentError,
|
||||
UnexpectedError,
|
||||
UserNotFoundError,
|
||||
VerificationError,
|
||||
)
|
||||
from pyfragment.types.constants import DEVICE, STARS_GIVEAWAY_PAGE
|
||||
from pyfragment.types.results import StarsGiveawayResult
|
||||
from pyfragment.utils import (
|
||||
execute_transaction_request,
|
||||
fragment_request,
|
||||
get_account_info,
|
||||
get_fragment_hash,
|
||||
make_headers,
|
||||
process_transaction,
|
||||
)
|
||||
from pyfragment.utils import get_account_info, process_transaction
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from pyfragment.client import FragmentClient
|
||||
|
||||
HEADERS: dict[str, str] = make_headers(STARS_GIVEAWAY_PAGE)
|
||||
|
||||
|
||||
async def _search_recipient(
|
||||
session: httpx.AsyncClient,
|
||||
fragment_hash: str,
|
||||
channel: str,
|
||||
) -> str:
|
||||
result = await fragment_request(
|
||||
session,
|
||||
fragment_hash,
|
||||
HEADERS,
|
||||
{
|
||||
"query": channel,
|
||||
"method": "searchStarsGiveawayRecipient",
|
||||
},
|
||||
)
|
||||
recipient = result.get("found", {}).get("recipient")
|
||||
if not recipient:
|
||||
raise UserNotFoundError(UserNotFoundError.NOT_FOUND.format(username=channel))
|
||||
return recipient
|
||||
|
||||
|
||||
async def _init_request(
|
||||
session: httpx.AsyncClient,
|
||||
fragment_hash: str,
|
||||
recipient: str,
|
||||
winners: int,
|
||||
amount: int,
|
||||
) -> str:
|
||||
result = await fragment_request(
|
||||
session,
|
||||
fragment_hash,
|
||||
HEADERS,
|
||||
{
|
||||
"recipient": recipient,
|
||||
"quantity": str(winners),
|
||||
"stars": str(amount),
|
||||
"method": "initGiveawayStarsRequest",
|
||||
},
|
||||
)
|
||||
req_id = result.get("req_id")
|
||||
if not req_id:
|
||||
raise FragmentAPIError(FragmentAPIError.NO_REQUEST_ID.format(context="Stars giveaway"))
|
||||
return req_id
|
||||
|
||||
|
||||
async def giveaway_stars(
|
||||
client: "FragmentClient",
|
||||
@@ -101,21 +47,33 @@ async def giveaway_stars(
|
||||
raise ConfigurationError(ConfigurationError.INVALID_STARS_PER_WINNER)
|
||||
|
||||
try:
|
||||
fragment_hash = await get_fragment_hash(client.cookies, HEADERS, STARS_GIVEAWAY_PAGE, client.timeout)
|
||||
result = await client.call("searchStarsGiveawayRecipient", {"query": channel}, page_url=STARS_GIVEAWAY_PAGE)
|
||||
recipient = result.get("found", {}).get("recipient")
|
||||
if not recipient:
|
||||
raise UserNotFoundError(UserNotFoundError.NOT_FOUND.format(username=channel))
|
||||
|
||||
result = await client.call(
|
||||
"initGiveawayStarsRequest",
|
||||
{"recipient": recipient, "quantity": str(winners), "stars": str(amount)},
|
||||
page_url=STARS_GIVEAWAY_PAGE,
|
||||
)
|
||||
req_id = result.get("req_id")
|
||||
if not req_id:
|
||||
raise FragmentAPIError(FragmentAPIError.NO_REQUEST_ID.format(context="Stars giveaway"))
|
||||
|
||||
account = await get_account_info(client)
|
||||
|
||||
async with httpx.AsyncClient(cookies=client.cookies, timeout=client.timeout) as session:
|
||||
recipient = await _search_recipient(session, fragment_hash, channel)
|
||||
req_id = await _init_request(session, fragment_hash, recipient, winners, amount)
|
||||
|
||||
tx_data = {
|
||||
transaction = await client.call(
|
||||
"getGiveawayStarsLink",
|
||||
{
|
||||
"account": json.dumps(account),
|
||||
"device": DEVICE,
|
||||
"transaction": 1,
|
||||
"id": req_id,
|
||||
"method": "getGiveawayStarsLink",
|
||||
}
|
||||
transaction = await execute_transaction_request(session, HEADERS, tx_data, fragment_hash)
|
||||
},
|
||||
page_url=STARS_GIVEAWAY_PAGE,
|
||||
)
|
||||
if transaction.get("need_verify"):
|
||||
raise VerificationError(VerificationError.KYC_REQUIRED)
|
||||
|
||||
tx_hash = await process_transaction(client, transaction)
|
||||
return StarsGiveawayResult(
|
||||
|
||||
@@ -2,8 +2,6 @@ import json
|
||||
import time
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import httpx
|
||||
|
||||
from pyfragment.types import (
|
||||
ConfigurationError,
|
||||
FragmentAPIError,
|
||||
@@ -11,77 +9,14 @@ from pyfragment.types import (
|
||||
PremiumResult,
|
||||
UnexpectedError,
|
||||
UserNotFoundError,
|
||||
VerificationError,
|
||||
)
|
||||
from pyfragment.types.constants import DEVICE, PREMIUM_PAGE
|
||||
from pyfragment.utils import (
|
||||
execute_transaction_request,
|
||||
fragment_request,
|
||||
get_account_info,
|
||||
get_fragment_hash,
|
||||
make_headers,
|
||||
process_transaction,
|
||||
)
|
||||
from pyfragment.utils import get_account_info, process_transaction
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from pyfragment.client import FragmentClient
|
||||
|
||||
HEADERS: dict[str, str] = make_headers(PREMIUM_PAGE)
|
||||
|
||||
|
||||
async def _search_recipient(
|
||||
session: httpx.AsyncClient,
|
||||
fragment_hash: str,
|
||||
username: str,
|
||||
months: int,
|
||||
) -> str:
|
||||
result = await fragment_request(
|
||||
session,
|
||||
fragment_hash,
|
||||
HEADERS,
|
||||
{
|
||||
"query": username,
|
||||
"months": months,
|
||||
"method": "searchPremiumGiftRecipient",
|
||||
},
|
||||
)
|
||||
recipient = result.get("found", {}).get("recipient")
|
||||
if not recipient:
|
||||
raise UserNotFoundError(UserNotFoundError.NOT_FOUND.format(username=username))
|
||||
return recipient
|
||||
|
||||
|
||||
async def _init_request(
|
||||
session: httpx.AsyncClient,
|
||||
fragment_hash: str,
|
||||
recipient: str,
|
||||
months: int,
|
||||
) -> str:
|
||||
await fragment_request(
|
||||
session,
|
||||
fragment_hash,
|
||||
HEADERS,
|
||||
{
|
||||
"mode": "new",
|
||||
"lv": "false",
|
||||
"dh": str(int(time.time())),
|
||||
"method": "updatePremiumState",
|
||||
},
|
||||
)
|
||||
result = await fragment_request(
|
||||
session,
|
||||
fragment_hash,
|
||||
HEADERS,
|
||||
{
|
||||
"recipient": recipient,
|
||||
"months": months,
|
||||
"method": "initGiftPremiumRequest",
|
||||
},
|
||||
)
|
||||
req_id = result.get("req_id")
|
||||
if not req_id:
|
||||
raise FragmentAPIError(FragmentAPIError.NO_REQUEST_ID.format(context="Premium purchase"))
|
||||
return req_id
|
||||
|
||||
|
||||
async def purchase_premium(client: "FragmentClient", username: str, months: int, show_sender: bool = True) -> PremiumResult:
|
||||
"""Gift Telegram Premium to a user.
|
||||
@@ -105,22 +40,35 @@ async def purchase_premium(client: "FragmentClient", username: str, months: int,
|
||||
raise ConfigurationError(ConfigurationError.INVALID_MONTHS)
|
||||
|
||||
try:
|
||||
fragment_hash = await get_fragment_hash(client.cookies, HEADERS, PREMIUM_PAGE, client.timeout)
|
||||
result = await client.call("searchPremiumGiftRecipient", {"query": username, "months": months}, page_url=PREMIUM_PAGE)
|
||||
recipient = result.get("found", {}).get("recipient")
|
||||
if not recipient:
|
||||
raise UserNotFoundError(UserNotFoundError.NOT_FOUND.format(username=username))
|
||||
|
||||
await client.call(
|
||||
"updatePremiumState",
|
||||
{"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)
|
||||
req_id = result.get("req_id")
|
||||
if not req_id:
|
||||
raise FragmentAPIError(FragmentAPIError.NO_REQUEST_ID.format(context="Premium purchase"))
|
||||
|
||||
account = await get_account_info(client)
|
||||
|
||||
async with httpx.AsyncClient(cookies=client.cookies, timeout=client.timeout) as session:
|
||||
recipient = await _search_recipient(session, fragment_hash, username, months)
|
||||
req_id = await _init_request(session, fragment_hash, recipient, months)
|
||||
|
||||
tx_data = {
|
||||
transaction = await client.call(
|
||||
"getGiftPremiumLink",
|
||||
{
|
||||
"account": json.dumps(account),
|
||||
"device": DEVICE,
|
||||
"transaction": 1,
|
||||
"id": req_id,
|
||||
"show_sender": int(show_sender),
|
||||
"method": "getGiftPremiumLink",
|
||||
}
|
||||
transaction = await execute_transaction_request(session, HEADERS, tx_data, fragment_hash)
|
||||
},
|
||||
page_url=PREMIUM_PAGE,
|
||||
)
|
||||
if transaction.get("need_verify"):
|
||||
raise VerificationError(VerificationError.KYC_REQUIRED)
|
||||
|
||||
tx_hash = await process_transaction(client, transaction)
|
||||
return PremiumResult(transaction_id=tx_hash, username=username, amount=months)
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import json
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import httpx
|
||||
|
||||
from pyfragment.types import (
|
||||
ConfigurationError,
|
||||
FragmentAPIError,
|
||||
@@ -10,65 +8,14 @@ from pyfragment.types import (
|
||||
StarsResult,
|
||||
UnexpectedError,
|
||||
UserNotFoundError,
|
||||
VerificationError,
|
||||
)
|
||||
from pyfragment.types.constants import DEVICE, STARS_PAGE
|
||||
from pyfragment.utils import (
|
||||
execute_transaction_request,
|
||||
fragment_request,
|
||||
get_account_info,
|
||||
get_fragment_hash,
|
||||
make_headers,
|
||||
process_transaction,
|
||||
)
|
||||
from pyfragment.utils import get_account_info, process_transaction
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from pyfragment.client import FragmentClient
|
||||
|
||||
HEADERS: dict[str, str] = make_headers(STARS_PAGE)
|
||||
|
||||
|
||||
async def _search_recipient(
|
||||
session: httpx.AsyncClient,
|
||||
fragment_hash: str,
|
||||
username: str,
|
||||
) -> str:
|
||||
result = await fragment_request(
|
||||
session,
|
||||
fragment_hash,
|
||||
HEADERS,
|
||||
{
|
||||
"query": username,
|
||||
"quantity": "",
|
||||
"method": "searchStarsRecipient",
|
||||
},
|
||||
)
|
||||
recipient = result.get("found", {}).get("recipient")
|
||||
if not recipient:
|
||||
raise UserNotFoundError(UserNotFoundError.NOT_FOUND.format(username=username))
|
||||
return recipient
|
||||
|
||||
|
||||
async def _init_request(
|
||||
session: httpx.AsyncClient,
|
||||
fragment_hash: str,
|
||||
recipient: str,
|
||||
amount: int,
|
||||
) -> str:
|
||||
result = await fragment_request(
|
||||
session,
|
||||
fragment_hash,
|
||||
HEADERS,
|
||||
{
|
||||
"recipient": recipient,
|
||||
"quantity": amount,
|
||||
"method": "initBuyStarsRequest",
|
||||
},
|
||||
)
|
||||
req_id = result.get("req_id")
|
||||
if not req_id:
|
||||
raise FragmentAPIError(FragmentAPIError.NO_REQUEST_ID.format(context="Stars purchase"))
|
||||
return req_id
|
||||
|
||||
|
||||
async def purchase_stars(client: "FragmentClient", username: str, amount: int, show_sender: bool = True) -> StarsResult:
|
||||
"""Send Telegram Stars to a user.
|
||||
@@ -92,22 +39,30 @@ async def purchase_stars(client: "FragmentClient", username: str, amount: int, s
|
||||
raise ConfigurationError(ConfigurationError.INVALID_STARS_AMOUNT)
|
||||
|
||||
try:
|
||||
fragment_hash = await get_fragment_hash(client.cookies, HEADERS, STARS_PAGE, client.timeout)
|
||||
result = await client.call("searchStarsRecipient", {"query": username, "quantity": ""}, page_url=STARS_PAGE)
|
||||
recipient = result.get("found", {}).get("recipient")
|
||||
if not recipient:
|
||||
raise UserNotFoundError(UserNotFoundError.NOT_FOUND.format(username=username))
|
||||
|
||||
result = await client.call("initBuyStarsRequest", {"recipient": recipient, "quantity": amount}, page_url=STARS_PAGE)
|
||||
req_id = result.get("req_id")
|
||||
if not req_id:
|
||||
raise FragmentAPIError(FragmentAPIError.NO_REQUEST_ID.format(context="Stars purchase"))
|
||||
|
||||
account = await get_account_info(client)
|
||||
|
||||
async with httpx.AsyncClient(cookies=client.cookies, timeout=client.timeout) as session:
|
||||
recipient = await _search_recipient(session, fragment_hash, username)
|
||||
req_id = await _init_request(session, fragment_hash, recipient, amount)
|
||||
|
||||
tx_data = {
|
||||
transaction = await client.call(
|
||||
"getBuyStarsLink",
|
||||
{
|
||||
"account": json.dumps(account),
|
||||
"device": DEVICE,
|
||||
"transaction": 1,
|
||||
"id": req_id,
|
||||
"show_sender": int(show_sender),
|
||||
"method": "getBuyStarsLink",
|
||||
}
|
||||
transaction = await execute_transaction_request(session, HEADERS, tx_data, fragment_hash)
|
||||
},
|
||||
page_url=STARS_PAGE,
|
||||
)
|
||||
if transaction.get("need_verify"):
|
||||
raise VerificationError(VerificationError.KYC_REQUIRED)
|
||||
|
||||
tx_hash = await process_transaction(client, transaction)
|
||||
return StarsResult(transaction_id=tx_hash, username=username, amount=amount)
|
||||
|
||||
@@ -1,52 +1,20 @@
|
||||
import json
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import httpx
|
||||
|
||||
from pyfragment.types import (
|
||||
ConfigurationError,
|
||||
FragmentAPIError,
|
||||
FragmentError,
|
||||
UnexpectedError,
|
||||
VerificationError,
|
||||
)
|
||||
from pyfragment.types.constants import ADS_TOPUP_PAGE, DEVICE
|
||||
from pyfragment.types.results import AdsRechargeResult
|
||||
from pyfragment.utils import (
|
||||
execute_transaction_request,
|
||||
fragment_request,
|
||||
get_account_info,
|
||||
get_fragment_hash,
|
||||
make_headers,
|
||||
process_transaction,
|
||||
)
|
||||
from pyfragment.utils import get_account_info, process_transaction
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from pyfragment.client import FragmentClient
|
||||
|
||||
HEADERS: dict[str, str] = make_headers(ADS_TOPUP_PAGE)
|
||||
|
||||
|
||||
async def _init_request(
|
||||
session: httpx.AsyncClient,
|
||||
fragment_hash: str,
|
||||
account: str,
|
||||
amount: int,
|
||||
) -> str:
|
||||
result = await fragment_request(
|
||||
session,
|
||||
fragment_hash,
|
||||
HEADERS,
|
||||
{
|
||||
"account": account,
|
||||
"amount": amount,
|
||||
"method": "initAdsRechargeRequest",
|
||||
},
|
||||
)
|
||||
req_id = result.get("req_id")
|
||||
if not req_id:
|
||||
raise FragmentAPIError(FragmentAPIError.NO_REQUEST_ID.format(context="Ads recharge"))
|
||||
return req_id
|
||||
|
||||
|
||||
async def recharge_ads(client: "FragmentClient", account: str, amount: int) -> AdsRechargeResult:
|
||||
"""Add funds to your own Telegram Ads account.
|
||||
@@ -69,21 +37,26 @@ async def recharge_ads(client: "FragmentClient", account: str, amount: int) -> A
|
||||
raise ConfigurationError(ConfigurationError.INVALID_TON_AMOUNT)
|
||||
|
||||
try:
|
||||
fragment_hash = await get_fragment_hash(client.cookies, HEADERS, ADS_TOPUP_PAGE, client.timeout)
|
||||
await client.call("updateAdsState", {"mode": "new"}, page_url=ADS_TOPUP_PAGE)
|
||||
|
||||
result = await client.call("initAdsRechargeRequest", {"account": account, "amount": amount}, page_url=ADS_TOPUP_PAGE)
|
||||
req_id = result.get("req_id")
|
||||
if not req_id:
|
||||
raise FragmentAPIError(FragmentAPIError.NO_REQUEST_ID.format(context="Ads recharge"))
|
||||
|
||||
account_info = await get_account_info(client)
|
||||
|
||||
async with httpx.AsyncClient(cookies=client.cookies, timeout=client.timeout) as session:
|
||||
await fragment_request(session, fragment_hash, HEADERS, {"method": "updateAdsState", "mode": "new"})
|
||||
req_id = await _init_request(session, fragment_hash, account, amount)
|
||||
|
||||
tx_data = {
|
||||
transaction = await client.call(
|
||||
"getAdsRechargeLink",
|
||||
{
|
||||
"account": json.dumps(account_info),
|
||||
"device": DEVICE,
|
||||
"transaction": 1,
|
||||
"id": req_id,
|
||||
"method": "getAdsRechargeLink",
|
||||
}
|
||||
transaction = await execute_transaction_request(session, HEADERS, tx_data, fragment_hash)
|
||||
},
|
||||
page_url=ADS_TOPUP_PAGE,
|
||||
)
|
||||
if transaction.get("need_verify"):
|
||||
raise VerificationError(VerificationError.KYC_REQUIRED)
|
||||
|
||||
tx_hash = await process_transaction(client, transaction)
|
||||
return AdsRechargeResult(transaction_id=tx_hash, amount=amount)
|
||||
|
||||
@@ -1,17 +1,13 @@
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
import httpx
|
||||
|
||||
from pyfragment.types import FragmentAPIError, FragmentError, UnexpectedError
|
||||
from pyfragment.types.constants import GIFTS_PAGE
|
||||
from pyfragment.types.results import GiftsResult
|
||||
from pyfragment.utils import fragment_request, get_fragment_hash, make_headers, parse_gift_items
|
||||
from pyfragment.utils import parse_gift_items
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from pyfragment.client import FragmentClient
|
||||
|
||||
HEADERS: dict[str, str] = make_headers(GIFTS_PAGE)
|
||||
|
||||
|
||||
async def search_gifts(
|
||||
client: "FragmentClient",
|
||||
@@ -64,9 +60,7 @@ async def search_gifts(
|
||||
data["offset"] = offset
|
||||
|
||||
try:
|
||||
fragment_hash = await get_fragment_hash(client.cookies, HEADERS, GIFTS_PAGE, client.timeout)
|
||||
async with httpx.AsyncClient(cookies=client.cookies, timeout=client.timeout) as session:
|
||||
result = await fragment_request(session, fragment_hash, HEADERS, data)
|
||||
result = await client.call("searchAuctions", data, page_url=GIFTS_PAGE)
|
||||
|
||||
if result.get("error"):
|
||||
raise FragmentAPIError(result["error"])
|
||||
|
||||
@@ -1,17 +1,13 @@
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
import httpx
|
||||
|
||||
from pyfragment.types import FragmentAPIError, FragmentError, UnexpectedError
|
||||
from pyfragment.types.constants import NUMBERS_PAGE
|
||||
from pyfragment.types.results import NumbersResult
|
||||
from pyfragment.utils import fragment_request, get_fragment_hash, make_headers, parse_auction_rows
|
||||
from pyfragment.utils import parse_auction_rows
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from pyfragment.client import FragmentClient
|
||||
|
||||
HEADERS: dict[str, str] = make_headers(NUMBERS_PAGE)
|
||||
|
||||
|
||||
async def search_numbers(
|
||||
client: "FragmentClient",
|
||||
@@ -49,9 +45,7 @@ async def search_numbers(
|
||||
data["offset_id"] = offset_id
|
||||
|
||||
try:
|
||||
fragment_hash = await get_fragment_hash(client.cookies, HEADERS, NUMBERS_PAGE, client.timeout)
|
||||
async with httpx.AsyncClient(cookies=client.cookies, timeout=client.timeout) as session:
|
||||
result = await fragment_request(session, fragment_hash, HEADERS, data)
|
||||
result = await client.call("searchAuctions", data, page_url=NUMBERS_PAGE)
|
||||
|
||||
if result.get("error"):
|
||||
raise FragmentAPIError(result["error"])
|
||||
|
||||
@@ -1,17 +1,13 @@
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
import httpx
|
||||
|
||||
from pyfragment.types import FragmentAPIError, FragmentError, UnexpectedError
|
||||
from pyfragment.types.constants import FRAGMENT_BASE_URL
|
||||
from pyfragment.types.results import UsernamesResult
|
||||
from pyfragment.utils import fragment_request, get_fragment_hash, make_headers, parse_auction_rows
|
||||
from pyfragment.utils import parse_auction_rows
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from pyfragment.client import FragmentClient
|
||||
|
||||
HEADERS: dict[str, str] = make_headers(FRAGMENT_BASE_URL)
|
||||
|
||||
|
||||
async def search_usernames(
|
||||
client: "FragmentClient",
|
||||
@@ -49,9 +45,7 @@ async def search_usernames(
|
||||
data["offset_id"] = offset_id
|
||||
|
||||
try:
|
||||
fragment_hash = await get_fragment_hash(client.cookies, HEADERS, FRAGMENT_BASE_URL, client.timeout)
|
||||
async with httpx.AsyncClient(cookies=client.cookies, timeout=client.timeout) as session:
|
||||
result = await fragment_request(session, fragment_hash, HEADERS, data)
|
||||
result = await client.call("searchAuctions", data, page_url=FRAGMENT_BASE_URL)
|
||||
|
||||
if result.get("error"):
|
||||
raise FragmentAPIError(result["error"])
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import json
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import httpx
|
||||
|
||||
from pyfragment.types import (
|
||||
AdsTopupResult,
|
||||
ConfigurationError,
|
||||
@@ -10,65 +8,14 @@ from pyfragment.types import (
|
||||
FragmentError,
|
||||
UnexpectedError,
|
||||
UserNotFoundError,
|
||||
VerificationError,
|
||||
)
|
||||
from pyfragment.types.constants import ADS_TOPUP_PAGE, DEVICE
|
||||
from pyfragment.utils import (
|
||||
execute_transaction_request,
|
||||
fragment_request,
|
||||
get_account_info,
|
||||
get_fragment_hash,
|
||||
make_headers,
|
||||
process_transaction,
|
||||
)
|
||||
from pyfragment.utils import get_account_info, process_transaction
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from pyfragment.client import FragmentClient
|
||||
|
||||
HEADERS: dict[str, str] = make_headers(ADS_TOPUP_PAGE)
|
||||
|
||||
|
||||
async def _search_recipient(
|
||||
session: httpx.AsyncClient,
|
||||
fragment_hash: str,
|
||||
username: str,
|
||||
) -> str:
|
||||
await fragment_request(session, fragment_hash, HEADERS, {"mode": "new", "method": "updateAdsTopupState"})
|
||||
result = await fragment_request(
|
||||
session,
|
||||
fragment_hash,
|
||||
HEADERS,
|
||||
{
|
||||
"query": username,
|
||||
"method": "searchAdsTopupRecipient",
|
||||
},
|
||||
)
|
||||
recipient = result.get("found", {}).get("recipient")
|
||||
if not recipient:
|
||||
raise UserNotFoundError(UserNotFoundError.NOT_FOUND.format(username=username))
|
||||
return recipient
|
||||
|
||||
|
||||
async def _init_request(
|
||||
session: httpx.AsyncClient,
|
||||
fragment_hash: str,
|
||||
recipient: str,
|
||||
amount: int,
|
||||
) -> str:
|
||||
result = await fragment_request(
|
||||
session,
|
||||
fragment_hash,
|
||||
HEADERS,
|
||||
{
|
||||
"recipient": recipient,
|
||||
"amount": amount,
|
||||
"method": "initAdsTopupRequest",
|
||||
},
|
||||
)
|
||||
req_id = result.get("req_id")
|
||||
if not req_id:
|
||||
raise FragmentAPIError(FragmentAPIError.NO_REQUEST_ID.format(context="TON topup"))
|
||||
return req_id
|
||||
|
||||
|
||||
async def topup_ton(client: "FragmentClient", username: str, amount: int, show_sender: bool = True) -> AdsTopupResult:
|
||||
"""Topup ton to recipient's Telegram balance.
|
||||
@@ -92,22 +39,32 @@ async def topup_ton(client: "FragmentClient", username: str, amount: int, show_s
|
||||
raise ConfigurationError(ConfigurationError.INVALID_TON_AMOUNT)
|
||||
|
||||
try:
|
||||
fragment_hash = await get_fragment_hash(client.cookies, HEADERS, ADS_TOPUP_PAGE, client.timeout)
|
||||
await client.call("updateAdsTopupState", {"mode": "new"}, page_url=ADS_TOPUP_PAGE)
|
||||
|
||||
result = await client.call("searchAdsTopupRecipient", {"query": username}, page_url=ADS_TOPUP_PAGE)
|
||||
recipient = result.get("found", {}).get("recipient")
|
||||
if not recipient:
|
||||
raise UserNotFoundError(UserNotFoundError.NOT_FOUND.format(username=username))
|
||||
|
||||
result = await client.call("initAdsTopupRequest", {"recipient": recipient, "amount": amount}, page_url=ADS_TOPUP_PAGE)
|
||||
req_id = result.get("req_id")
|
||||
if not req_id:
|
||||
raise FragmentAPIError(FragmentAPIError.NO_REQUEST_ID.format(context="TON topup"))
|
||||
|
||||
account = await get_account_info(client)
|
||||
|
||||
async with httpx.AsyncClient(cookies=client.cookies, timeout=client.timeout) as session:
|
||||
recipient = await _search_recipient(session, fragment_hash, username)
|
||||
req_id = await _init_request(session, fragment_hash, recipient, amount)
|
||||
|
||||
tx_data = {
|
||||
transaction = await client.call(
|
||||
"getAdsTopupLink",
|
||||
{
|
||||
"account": json.dumps(account),
|
||||
"device": DEVICE,
|
||||
"transaction": 1,
|
||||
"id": req_id,
|
||||
"show_sender": int(show_sender),
|
||||
"method": "getAdsTopupLink",
|
||||
}
|
||||
transaction = await execute_transaction_request(session, HEADERS, tx_data, fragment_hash)
|
||||
},
|
||||
page_url=ADS_TOPUP_PAGE,
|
||||
)
|
||||
if transaction.get("need_verify"):
|
||||
raise VerificationError(VerificationError.KYC_REQUIRED)
|
||||
|
||||
tx_hash = await process_transaction(client, transaction)
|
||||
return AdsTopupResult(transaction_id=tx_hash, username=username, amount=amount)
|
||||
|
||||
@@ -19,8 +19,9 @@ DEFAULT_TIMEOUT: float = 30.0
|
||||
# Required Fragment session cookie keys
|
||||
REQUIRED_COOKIE_KEYS: tuple[str, ...] = ("stel_ssid", "stel_dt", "stel_token", "stel_ton_token")
|
||||
|
||||
# Fragment page URLs
|
||||
FRAGMENT_BASE_URL: str = "https://fragment.com"
|
||||
# Fragment domain and page URLs
|
||||
FRAGMENT_DOMAIN: str = "fragment.com" # for rookiepy
|
||||
FRAGMENT_BASE_URL: str = f"https://{FRAGMENT_DOMAIN}"
|
||||
STARS_PAGE: str = f"{FRAGMENT_BASE_URL}/stars/buy"
|
||||
STARS_GIVEAWAY_PAGE: str = f"{FRAGMENT_BASE_URL}/stars/giveaway"
|
||||
PREMIUM_PAGE: str = f"{FRAGMENT_BASE_URL}/premium/gift"
|
||||
@@ -29,6 +30,25 @@ ADS_TOPUP_PAGE: str = f"{FRAGMENT_BASE_URL}/ads/topup"
|
||||
NUMBERS_PAGE: str = f"{FRAGMENT_BASE_URL}/numbers"
|
||||
GIFTS_PAGE: str = f"{FRAGMENT_BASE_URL}/gifts"
|
||||
|
||||
# Browsers supported by get_cookies_from_browser()
|
||||
SUPPORTED_BROWSERS: frozenset[str] = frozenset(
|
||||
{
|
||||
"arc",
|
||||
"brave",
|
||||
"chrome",
|
||||
"chromium",
|
||||
"chromium_based",
|
||||
"edge",
|
||||
"firefox",
|
||||
"firefox_based",
|
||||
"librewolf",
|
||||
"opera",
|
||||
"opera_gx",
|
||||
"safari",
|
||||
"vivaldi",
|
||||
}
|
||||
)
|
||||
|
||||
# Tonkeeper device fingerprint — serialized once, reused in every tx_data payload.
|
||||
DEVICE: str = json.dumps(
|
||||
{
|
||||
|
||||
@@ -35,6 +35,14 @@ class CookieError(ClientError):
|
||||
"Fragment cookies are missing or empty for key(s): {keys}. "
|
||||
"Open fragment.com in your browser, log in, and copy fresh cookies."
|
||||
)
|
||||
UNSUPPORTED_BROWSER = "Unsupported browser: '{browser}'. Supported: {supported}."
|
||||
BROWSER_READ_FAILED = (
|
||||
"Failed to read {browser} cookies: {exc}. " "Make sure {browser} is installed and you are logged in to {url}."
|
||||
)
|
||||
MISSING_BROWSER_KEYS = (
|
||||
"Fragment cookies not found in {browser}: {keys}. "
|
||||
"Make sure you are logged in to {url} and have connected your TON wallet in {browser}."
|
||||
)
|
||||
|
||||
|
||||
class FragmentAPIError(FragmentError):
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
from pyfragment.utils.cookies import get_cookies_from_browser
|
||||
from pyfragment.utils.decoder import clean_decode
|
||||
from pyfragment.utils.html import parse_auction_rows, parse_gift_items, parse_login_code
|
||||
from pyfragment.utils.http import (
|
||||
@@ -11,6 +12,7 @@ from pyfragment.utils.wallet import get_account_info, process_transaction
|
||||
|
||||
__all__ = [
|
||||
"clean_decode",
|
||||
"get_cookies_from_browser",
|
||||
"parse_auction_rows",
|
||||
"parse_gift_items",
|
||||
"parse_login_code",
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import rookiepy
|
||||
|
||||
from pyfragment.types import CookieError
|
||||
from pyfragment.types.constants import FRAGMENT_BASE_URL, FRAGMENT_DOMAIN, REQUIRED_COOKIE_KEYS, SUPPORTED_BROWSERS
|
||||
|
||||
|
||||
def get_cookies_from_browser(browser: str = "chrome") -> dict[str, str]:
|
||||
"""Extract Fragment session cookies directly from an installed browser.
|
||||
|
||||
Reads the browser's on-disk cookie store (no extension required) and
|
||||
returns the four cookies required by :class:`~pyfragment.FragmentClient`.
|
||||
|
||||
Args:
|
||||
browser: Browser name to read cookies from — case-insensitive. Supported values:
|
||||
``"chrome"`` (default), ``"firefox"``, ``"edge"``, ``"brave"``, ``"arc"``,
|
||||
``"opera"``, ``"opera_gx"``, ``"chromium"``, ``"chromium_based"``,
|
||||
``"firefox_based"``, ``"vivaldi"``, ``"librewolf"``, ``"safari"``.
|
||||
|
||||
Returns:
|
||||
A dict with the four required Fragment cookie keys:
|
||||
``stel_ssid``, ``stel_dt``, ``stel_token``, ``stel_ton_token``.
|
||||
|
||||
Raises:
|
||||
CookieError: If the browser is not supported, cookies cannot be read,
|
||||
or required keys are missing.
|
||||
"""
|
||||
key = browser.lower()
|
||||
if key not in SUPPORTED_BROWSERS:
|
||||
supported = ", ".join(sorted(SUPPORTED_BROWSERS))
|
||||
raise CookieError(CookieError.UNSUPPORTED_BROWSER.format(browser=browser, supported=supported))
|
||||
|
||||
try:
|
||||
jar: list[dict] = getattr(rookiepy, key)([FRAGMENT_DOMAIN])
|
||||
except Exception as exc:
|
||||
raise CookieError(CookieError.BROWSER_READ_FAILED.format(browser=browser, exc=exc, url=FRAGMENT_BASE_URL)) from exc
|
||||
|
||||
cookie_map: dict[str, str] = {c["name"]: c["value"] for c in jar if c.get("name") and c.get("value")}
|
||||
|
||||
missing = [k for k in REQUIRED_COOKIE_KEYS if not str(cookie_map.get(k, "")).strip()]
|
||||
if missing:
|
||||
raise CookieError(CookieError.MISSING_BROWSER_KEYS.format(browser=browser, keys=missing, url=FRAGMENT_BASE_URL))
|
||||
|
||||
return {k: cookie_map[k] for k in REQUIRED_COOKIE_KEYS}
|
||||
@@ -1,6 +1,6 @@
|
||||
import base64
|
||||
|
||||
from pytoniq_core import Cell
|
||||
from ton_core import Cell
|
||||
|
||||
from pyfragment.types import ParseError
|
||||
|
||||
|
||||
@@ -3,9 +3,9 @@ import base64
|
||||
import ssl
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from ton_core import NetworkGlobalID
|
||||
from tonutils.clients import TonapiClient
|
||||
from tonutils.exceptions import ProviderResponseError
|
||||
from tonutils.types import NetworkGlobalID
|
||||
|
||||
from pyfragment.types import TransactionError, WalletError
|
||||
from pyfragment.types.constants import MIN_TON_BALANCE, WALLET_CLASSES
|
||||
|
||||
+3
-2
@@ -27,12 +27,13 @@ classifiers = [
|
||||
]
|
||||
dependencies = [
|
||||
"httpx==0.28.1",
|
||||
"tonutils[pytoniq]==2.0.5",
|
||||
"rookiepy==0.5.6",
|
||||
"tonutils==2.1.0",
|
||||
]
|
||||
|
||||
[project.optional-dependencies]
|
||||
dev = [
|
||||
"pytest==9.0.2",
|
||||
"pytest==9.0.3",
|
||||
"pytest-asyncio==1.3.0",
|
||||
"pytest-mock",
|
||||
"mypy",
|
||||
|
||||
+25
-23
@@ -6,7 +6,7 @@ import pytest
|
||||
|
||||
from pyfragment import FragmentClient
|
||||
from pyfragment.types import ConfigurationError, StarsGiveawayResult, StarsResult, UserNotFoundError
|
||||
from tests.shared import FAKE_ACCOUNT, FAKE_HASH, FAKE_RECIPIENT, FAKE_REQ_ID, FAKE_TRANSACTION, FAKE_TX_HASH
|
||||
from tests.shared import FAKE_ACCOUNT, FAKE_RECIPIENT, FAKE_REQ_ID, FAKE_TRANSACTION, FAKE_TX_HASH
|
||||
|
||||
# Stars purchase validation tests
|
||||
|
||||
@@ -35,13 +35,18 @@ async def test_purchase_stars_float_amount(client: FragmentClient) -> None:
|
||||
@pytest.mark.asyncio
|
||||
async def test_purchase_stars_success(client: FragmentClient) -> None:
|
||||
with (
|
||||
patch("pyfragment.methods.purchase_stars.get_fragment_hash", AsyncMock(return_value=FAKE_HASH)),
|
||||
patch("pyfragment.methods.purchase_stars.get_account_info", AsyncMock(return_value=FAKE_ACCOUNT)),
|
||||
patch(
|
||||
"pyfragment.methods.purchase_stars.fragment_request",
|
||||
AsyncMock(side_effect=[{"found": {"recipient": FAKE_RECIPIENT}}, {"req_id": FAKE_REQ_ID}]),
|
||||
patch.object(
|
||||
client,
|
||||
"call",
|
||||
AsyncMock(
|
||||
side_effect=[
|
||||
{"found": {"recipient": FAKE_RECIPIENT}},
|
||||
{"req_id": FAKE_REQ_ID},
|
||||
FAKE_TRANSACTION,
|
||||
]
|
||||
),
|
||||
patch("pyfragment.methods.purchase_stars.execute_transaction_request", AsyncMock(return_value=FAKE_TRANSACTION)),
|
||||
),
|
||||
patch("pyfragment.methods.purchase_stars.get_account_info", AsyncMock(return_value=FAKE_ACCOUNT)),
|
||||
patch("pyfragment.methods.purchase_stars.process_transaction", AsyncMock(return_value=FAKE_TX_HASH)),
|
||||
):
|
||||
result = await client.purchase_stars("@user", amount=500)
|
||||
@@ -54,11 +59,7 @@ async def test_purchase_stars_success(client: FragmentClient) -> None:
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_purchase_stars_user_not_found(client: FragmentClient) -> None:
|
||||
with (
|
||||
patch("pyfragment.methods.purchase_stars.get_fragment_hash", AsyncMock(return_value=FAKE_HASH)),
|
||||
patch("pyfragment.methods.purchase_stars.get_account_info", AsyncMock(return_value=FAKE_ACCOUNT)),
|
||||
patch("pyfragment.methods.purchase_stars.fragment_request", AsyncMock(return_value={"found": {}})),
|
||||
):
|
||||
with patch.object(client, "call", AsyncMock(return_value={"found": {}})):
|
||||
with pytest.raises(UserNotFoundError):
|
||||
await client.purchase_stars("@ghost", amount=500)
|
||||
|
||||
@@ -108,13 +109,18 @@ async def test_giveaway_stars_float_amount(client: FragmentClient) -> None:
|
||||
@pytest.mark.asyncio
|
||||
async def test_giveaway_stars_success(client: FragmentClient) -> None:
|
||||
with (
|
||||
patch("pyfragment.methods.giveaway_stars.get_fragment_hash", AsyncMock(return_value=FAKE_HASH)),
|
||||
patch("pyfragment.methods.giveaway_stars.get_account_info", AsyncMock(return_value=FAKE_ACCOUNT)),
|
||||
patch(
|
||||
"pyfragment.methods.giveaway_stars.fragment_request",
|
||||
AsyncMock(side_effect=[{"found": {"recipient": FAKE_RECIPIENT}}, {"req_id": FAKE_REQ_ID}]),
|
||||
patch.object(
|
||||
client,
|
||||
"call",
|
||||
AsyncMock(
|
||||
side_effect=[
|
||||
{"found": {"recipient": FAKE_RECIPIENT}},
|
||||
{"req_id": FAKE_REQ_ID},
|
||||
FAKE_TRANSACTION,
|
||||
]
|
||||
),
|
||||
patch("pyfragment.methods.giveaway_stars.execute_transaction_request", AsyncMock(return_value=FAKE_TRANSACTION)),
|
||||
),
|
||||
patch("pyfragment.methods.giveaway_stars.get_account_info", AsyncMock(return_value=FAKE_ACCOUNT)),
|
||||
patch("pyfragment.methods.giveaway_stars.process_transaction", AsyncMock(return_value=FAKE_TX_HASH)),
|
||||
):
|
||||
result = await client.giveaway_stars("@channel", winners=3, amount=1000)
|
||||
@@ -128,10 +134,6 @@ async def test_giveaway_stars_success(client: FragmentClient) -> None:
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_giveaway_stars_channel_not_found(client: FragmentClient) -> None:
|
||||
with (
|
||||
patch("pyfragment.methods.giveaway_stars.get_fragment_hash", AsyncMock(return_value=FAKE_HASH)),
|
||||
patch("pyfragment.methods.giveaway_stars.get_account_info", AsyncMock(return_value=FAKE_ACCOUNT)),
|
||||
patch("pyfragment.methods.giveaway_stars.fragment_request", AsyncMock(return_value={"found": {}})),
|
||||
):
|
||||
with patch.object(client, "call", AsyncMock(return_value={"found": {}})):
|
||||
with pytest.raises(UserNotFoundError):
|
||||
await client.giveaway_stars("@ghost", winners=1, amount=500)
|
||||
|
||||
+19
-22
@@ -6,7 +6,7 @@ import pytest
|
||||
|
||||
from pyfragment import FragmentClient
|
||||
from pyfragment.types import ConfigurationError, PremiumGiveawayResult, PremiumResult, UserNotFoundError
|
||||
from tests.shared import FAKE_ACCOUNT, FAKE_HASH, FAKE_RECIPIENT, FAKE_REQ_ID, FAKE_TRANSACTION, FAKE_TX_HASH
|
||||
from tests.shared import FAKE_ACCOUNT, FAKE_RECIPIENT, FAKE_REQ_ID, FAKE_TRANSACTION, FAKE_TX_HASH
|
||||
|
||||
# Premium purchase validation tests
|
||||
|
||||
@@ -29,19 +29,19 @@ async def test_purchase_premium_months_zero(client: FragmentClient) -> None:
|
||||
@pytest.mark.asyncio
|
||||
async def test_purchase_premium_success(client: FragmentClient) -> None:
|
||||
with (
|
||||
patch("pyfragment.methods.purchase_premium.get_fragment_hash", AsyncMock(return_value=FAKE_HASH)),
|
||||
patch("pyfragment.methods.purchase_premium.get_account_info", AsyncMock(return_value=FAKE_ACCOUNT)),
|
||||
patch(
|
||||
"pyfragment.methods.purchase_premium.fragment_request",
|
||||
patch.object(
|
||||
client,
|
||||
"call",
|
||||
AsyncMock(
|
||||
side_effect=[
|
||||
{"found": {"recipient": FAKE_RECIPIENT}},
|
||||
{}, # updatePremiumState
|
||||
{"req_id": FAKE_REQ_ID},
|
||||
FAKE_TRANSACTION,
|
||||
]
|
||||
),
|
||||
),
|
||||
patch("pyfragment.methods.purchase_premium.execute_transaction_request", AsyncMock(return_value=FAKE_TRANSACTION)),
|
||||
patch("pyfragment.methods.purchase_premium.get_account_info", AsyncMock(return_value=FAKE_ACCOUNT)),
|
||||
patch("pyfragment.methods.purchase_premium.process_transaction", AsyncMock(return_value=FAKE_TX_HASH)),
|
||||
):
|
||||
result = await client.purchase_premium("@user", months=3)
|
||||
@@ -54,11 +54,7 @@ async def test_purchase_premium_success(client: FragmentClient) -> None:
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_purchase_premium_user_not_found(client: FragmentClient) -> None:
|
||||
with (
|
||||
patch("pyfragment.methods.purchase_premium.get_fragment_hash", AsyncMock(return_value=FAKE_HASH)),
|
||||
patch("pyfragment.methods.purchase_premium.get_account_info", AsyncMock(return_value=FAKE_ACCOUNT)),
|
||||
patch("pyfragment.methods.purchase_premium.fragment_request", AsyncMock(return_value={"found": {}})),
|
||||
):
|
||||
with patch.object(client, "call", AsyncMock(return_value={"found": {}})):
|
||||
with pytest.raises(UserNotFoundError):
|
||||
await client.purchase_premium("@ghost", months=3)
|
||||
|
||||
@@ -96,13 +92,18 @@ async def test_giveaway_premium_invalid_months(client: FragmentClient) -> None:
|
||||
@pytest.mark.asyncio
|
||||
async def test_giveaway_premium_success(client: FragmentClient) -> None:
|
||||
with (
|
||||
patch("pyfragment.methods.giveaway_premium.get_fragment_hash", AsyncMock(return_value=FAKE_HASH)),
|
||||
patch("pyfragment.methods.giveaway_premium.get_account_info", AsyncMock(return_value=FAKE_ACCOUNT)),
|
||||
patch(
|
||||
"pyfragment.methods.giveaway_premium.fragment_request",
|
||||
AsyncMock(side_effect=[{"found": {"recipient": FAKE_RECIPIENT}}, {"req_id": FAKE_REQ_ID}]),
|
||||
patch.object(
|
||||
client,
|
||||
"call",
|
||||
AsyncMock(
|
||||
side_effect=[
|
||||
{"found": {"recipient": FAKE_RECIPIENT}},
|
||||
{"req_id": FAKE_REQ_ID},
|
||||
FAKE_TRANSACTION,
|
||||
]
|
||||
),
|
||||
patch("pyfragment.methods.giveaway_premium.execute_transaction_request", AsyncMock(return_value=FAKE_TRANSACTION)),
|
||||
),
|
||||
patch("pyfragment.methods.giveaway_premium.get_account_info", AsyncMock(return_value=FAKE_ACCOUNT)),
|
||||
patch("pyfragment.methods.giveaway_premium.process_transaction", AsyncMock(return_value=FAKE_TX_HASH)),
|
||||
):
|
||||
result = await client.giveaway_premium("@channel", winners=10, months=3)
|
||||
@@ -116,10 +117,6 @@ async def test_giveaway_premium_success(client: FragmentClient) -> None:
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_giveaway_premium_channel_not_found(client: FragmentClient) -> None:
|
||||
with (
|
||||
patch("pyfragment.methods.giveaway_premium.get_fragment_hash", AsyncMock(return_value=FAKE_HASH)),
|
||||
patch("pyfragment.methods.giveaway_premium.get_account_info", AsyncMock(return_value=FAKE_ACCOUNT)),
|
||||
patch("pyfragment.methods.giveaway_premium.fragment_request", AsyncMock(return_value={"found": {}})),
|
||||
):
|
||||
with patch.object(client, "call", AsyncMock(return_value={"found": {}})):
|
||||
with pytest.raises(UserNotFoundError):
|
||||
await client.giveaway_premium("@ghost", winners=1, months=3)
|
||||
|
||||
+14
-12
@@ -6,7 +6,7 @@ import pytest
|
||||
|
||||
from pyfragment import FragmentClient
|
||||
from pyfragment.types import AdsTopupResult, ConfigurationError, UserNotFoundError
|
||||
from tests.shared import FAKE_ACCOUNT, FAKE_HASH, FAKE_RECIPIENT, FAKE_REQ_ID, FAKE_TRANSACTION, FAKE_TX_HASH
|
||||
from tests.shared import FAKE_ACCOUNT, FAKE_RECIPIENT, FAKE_REQ_ID, FAKE_TRANSACTION, FAKE_TX_HASH
|
||||
|
||||
# Topup TON validation tests
|
||||
|
||||
@@ -35,19 +35,19 @@ async def test_topup_ton_float_amount(client: FragmentClient) -> None:
|
||||
@pytest.mark.asyncio
|
||||
async def test_topup_ton_success(client: FragmentClient) -> None:
|
||||
with (
|
||||
patch("pyfragment.methods.topup_ton.get_fragment_hash", AsyncMock(return_value=FAKE_HASH)),
|
||||
patch("pyfragment.methods.topup_ton.get_account_info", AsyncMock(return_value=FAKE_ACCOUNT)),
|
||||
patch(
|
||||
"pyfragment.methods.topup_ton.fragment_request",
|
||||
patch.object(
|
||||
client,
|
||||
"call",
|
||||
AsyncMock(
|
||||
side_effect=[
|
||||
{}, # updateAdsTopupState
|
||||
{"found": {"recipient": FAKE_RECIPIENT}},
|
||||
{"req_id": FAKE_REQ_ID},
|
||||
FAKE_TRANSACTION,
|
||||
]
|
||||
),
|
||||
),
|
||||
patch("pyfragment.methods.topup_ton.execute_transaction_request", AsyncMock(return_value=FAKE_TRANSACTION)),
|
||||
patch("pyfragment.methods.topup_ton.get_account_info", AsyncMock(return_value=FAKE_ACCOUNT)),
|
||||
patch("pyfragment.methods.topup_ton.process_transaction", AsyncMock(return_value=FAKE_TX_HASH)),
|
||||
):
|
||||
result = await client.topup_ton("@user", amount=10)
|
||||
@@ -60,12 +60,14 @@ async def test_topup_ton_success(client: FragmentClient) -> None:
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_topup_ton_user_not_found(client: FragmentClient) -> None:
|
||||
with (
|
||||
patch("pyfragment.methods.topup_ton.get_fragment_hash", AsyncMock(return_value=FAKE_HASH)),
|
||||
patch("pyfragment.methods.topup_ton.get_account_info", AsyncMock(return_value=FAKE_ACCOUNT)),
|
||||
patch(
|
||||
"pyfragment.methods.topup_ton.fragment_request",
|
||||
AsyncMock(side_effect=[{}, {"found": {}}]),
|
||||
with patch.object(
|
||||
client,
|
||||
"call",
|
||||
AsyncMock(
|
||||
side_effect=[
|
||||
{}, # updateAdsTopupState
|
||||
{"found": {}},
|
||||
]
|
||||
),
|
||||
):
|
||||
with pytest.raises(UserNotFoundError):
|
||||
|
||||
+25
-60
@@ -5,20 +5,14 @@ from unittest.mock import AsyncMock, patch
|
||||
import pytest
|
||||
|
||||
from pyfragment import AnonymousNumberError, FragmentClient, LoginCodeResult, TerminateSessionsResult
|
||||
from tests.shared import FAKE_HASH, FAKE_HTML_NO_CODE, FAKE_HTML_WITH_CODE, FAKE_TERMINATE_HASH
|
||||
from tests.shared import FAKE_HTML_NO_CODE, FAKE_HTML_WITH_CODE, FAKE_TERMINATE_HASH
|
||||
|
||||
# get_login_code tests
|
||||
# get_login_code mocked tests
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_login_code_returns_code(client: FragmentClient) -> None:
|
||||
with (
|
||||
patch("pyfragment.methods.anonymous_number.get_fragment_hash", AsyncMock(return_value=FAKE_HASH)),
|
||||
patch(
|
||||
"pyfragment.methods.anonymous_number.fragment_request",
|
||||
AsyncMock(return_value={"html": FAKE_HTML_WITH_CODE}),
|
||||
),
|
||||
):
|
||||
with patch.object(client, "call", AsyncMock(return_value={"html": FAKE_HTML_WITH_CODE})):
|
||||
result = await client.get_login_code("+1234567890")
|
||||
|
||||
assert isinstance(result, LoginCodeResult)
|
||||
@@ -29,13 +23,7 @@ async def test_get_login_code_returns_code(client: FragmentClient) -> None:
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_login_code_no_pending_code(client: FragmentClient) -> None:
|
||||
with (
|
||||
patch("pyfragment.methods.anonymous_number.get_fragment_hash", AsyncMock(return_value=FAKE_HASH)),
|
||||
patch(
|
||||
"pyfragment.methods.anonymous_number.fragment_request",
|
||||
AsyncMock(return_value={"html": FAKE_HTML_NO_CODE}),
|
||||
),
|
||||
):
|
||||
with patch.object(client, "call", AsyncMock(return_value={"html": FAKE_HTML_NO_CODE})):
|
||||
result = await client.get_login_code("1234567890")
|
||||
|
||||
assert result.code is None
|
||||
@@ -44,32 +32,27 @@ async def test_get_login_code_no_pending_code(client: FragmentClient) -> None:
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_login_code_no_html_returns_none(client: FragmentClient) -> None:
|
||||
with (
|
||||
patch("pyfragment.methods.anonymous_number.get_fragment_hash", AsyncMock(return_value=FAKE_HASH)),
|
||||
patch("pyfragment.methods.anonymous_number.fragment_request", AsyncMock(return_value={})),
|
||||
):
|
||||
with patch.object(client, "call", AsyncMock(return_value={})):
|
||||
result = await client.get_login_code("+1234567890")
|
||||
|
||||
assert result.code is None
|
||||
assert result.active_sessions == 0
|
||||
|
||||
|
||||
# terminate_sessions tests
|
||||
# terminate_sessions mocked tests
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_terminate_sessions_success(client: FragmentClient) -> None:
|
||||
with (
|
||||
patch("pyfragment.methods.anonymous_number.get_fragment_hash", AsyncMock(return_value=FAKE_HASH)),
|
||||
patch(
|
||||
"pyfragment.methods.anonymous_number.fragment_request",
|
||||
with patch.object(
|
||||
client,
|
||||
"call",
|
||||
AsyncMock(
|
||||
side_effect=[
|
||||
{"terminate_hash": FAKE_TERMINATE_HASH}, # step 1: confirmation
|
||||
{"msg": "All sessions terminated"}, # step 2: confirmed
|
||||
]
|
||||
),
|
||||
),
|
||||
):
|
||||
result = await client.terminate_sessions("+1234567890")
|
||||
|
||||
@@ -80,84 +63,66 @@ async def test_terminate_sessions_success(client: FragmentClient) -> None:
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_terminate_sessions_not_owned_raises(client: FragmentClient) -> None:
|
||||
with (
|
||||
patch("pyfragment.methods.anonymous_number.get_fragment_hash", AsyncMock(return_value=FAKE_HASH)),
|
||||
patch("pyfragment.methods.anonymous_number.fragment_request", AsyncMock(return_value={})),
|
||||
):
|
||||
with patch.object(client, "call", AsyncMock(return_value={})):
|
||||
with pytest.raises(AnonymousNumberError, match="not associated"):
|
||||
await client.terminate_sessions("+1234567890")
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_terminate_sessions_api_error_raises(client: FragmentClient) -> None:
|
||||
with (
|
||||
patch("pyfragment.methods.anonymous_number.get_fragment_hash", AsyncMock(return_value=FAKE_HASH)),
|
||||
patch(
|
||||
"pyfragment.methods.anonymous_number.fragment_request",
|
||||
AsyncMock(return_value={"error": "SESSION_ALREADY_TERMINATED"}),
|
||||
),
|
||||
):
|
||||
with patch.object(client, "call", AsyncMock(return_value={"error": "SESSION_ALREADY_TERMINATED"})):
|
||||
with pytest.raises(AnonymousNumberError, match="SESSION_ALREADY_TERMINATED"):
|
||||
await client.terminate_sessions("+1234567890")
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_terminate_sessions_confirm_error_raises(client: FragmentClient) -> None:
|
||||
with (
|
||||
patch("pyfragment.methods.anonymous_number.get_fragment_hash", AsyncMock(return_value=FAKE_HASH)),
|
||||
patch(
|
||||
"pyfragment.methods.anonymous_number.fragment_request",
|
||||
with patch.object(
|
||||
client,
|
||||
"call",
|
||||
AsyncMock(
|
||||
side_effect=[
|
||||
{"terminate_hash": FAKE_TERMINATE_HASH},
|
||||
{"error": "INTERNAL_ERROR"},
|
||||
]
|
||||
),
|
||||
),
|
||||
):
|
||||
with pytest.raises(AnonymousNumberError, match="INTERNAL_ERROR"):
|
||||
await client.terminate_sessions("+1234567890")
|
||||
|
||||
|
||||
# toggle_login_codes tests
|
||||
# toggle_login_codes mocked tests
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_toggle_login_codes_enable(client: FragmentClient) -> None:
|
||||
with (
|
||||
patch("pyfragment.methods.anonymous_number.get_fragment_hash", AsyncMock(return_value=FAKE_HASH)),
|
||||
patch("pyfragment.methods.anonymous_number.fragment_request", AsyncMock(return_value={"ok": True})) as mock_req,
|
||||
):
|
||||
mock_call = AsyncMock(return_value={"ok": True})
|
||||
with patch.object(client, "call", mock_call):
|
||||
await client.toggle_login_codes("+1234567890", can_receive=True)
|
||||
|
||||
call_data = mock_req.call_args[0][3]
|
||||
call_data = mock_call.call_args[0][1]
|
||||
assert call_data["can_receive"] == 1
|
||||
assert call_data["method"] == "toggleLoginCodes"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_toggle_login_codes_disable(client: FragmentClient) -> None:
|
||||
with (
|
||||
patch("pyfragment.methods.anonymous_number.get_fragment_hash", AsyncMock(return_value=FAKE_HASH)),
|
||||
patch("pyfragment.methods.anonymous_number.fragment_request", AsyncMock(return_value={"ok": True})) as mock_req,
|
||||
):
|
||||
mock_call = AsyncMock(return_value={"ok": True})
|
||||
with patch.object(client, "call", mock_call):
|
||||
await client.toggle_login_codes("+1234567890", can_receive=False)
|
||||
|
||||
call_data = mock_req.call_args[0][3]
|
||||
call_data = mock_call.call_args[0][1]
|
||||
assert call_data["can_receive"] == 0
|
||||
|
||||
|
||||
# strip_plus tests
|
||||
# strip_plus mocked tests
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_login_code_strips_plus(client: FragmentClient) -> None:
|
||||
"""Number passed with '+' is stripped before the API call."""
|
||||
with (
|
||||
patch("pyfragment.methods.anonymous_number.get_fragment_hash", AsyncMock(return_value=FAKE_HASH)),
|
||||
patch("pyfragment.methods.anonymous_number.fragment_request", AsyncMock(return_value={})) as mock_req,
|
||||
):
|
||||
mock_call = AsyncMock(return_value={})
|
||||
with patch.object(client, "call", mock_call):
|
||||
await client.get_login_code("+1234567890")
|
||||
|
||||
call_data = mock_req.call_args[0][3]
|
||||
call_data = mock_call.call_args[0][1]
|
||||
assert call_data["number"] == "1234567890"
|
||||
|
||||
@@ -6,7 +6,7 @@ import pytest
|
||||
|
||||
from pyfragment import FragmentClient
|
||||
from pyfragment.types import AdsRechargeResult, ConfigurationError
|
||||
from tests.shared import FAKE_ACCOUNT, FAKE_ADS_ACCOUNT, FAKE_HASH, FAKE_REQ_ID, FAKE_TRANSACTION, FAKE_TX_HASH
|
||||
from tests.shared import FAKE_ACCOUNT, FAKE_ADS_ACCOUNT, FAKE_REQ_ID, FAKE_TRANSACTION, FAKE_TX_HASH
|
||||
|
||||
# recharge_ads validation tests
|
||||
|
||||
@@ -35,18 +35,18 @@ async def test_recharge_ads_float_amount(client: FragmentClient) -> None:
|
||||
@pytest.mark.asyncio
|
||||
async def test_recharge_ads_success(client: FragmentClient) -> None:
|
||||
with (
|
||||
patch("pyfragment.methods.recharge_ads.get_fragment_hash", AsyncMock(return_value=FAKE_HASH)),
|
||||
patch("pyfragment.methods.recharge_ads.get_account_info", AsyncMock(return_value=FAKE_ACCOUNT)),
|
||||
patch(
|
||||
"pyfragment.methods.recharge_ads.fragment_request",
|
||||
patch.object(
|
||||
client,
|
||||
"call",
|
||||
AsyncMock(
|
||||
side_effect=[
|
||||
{}, # updateAdsState
|
||||
{"req_id": FAKE_REQ_ID}, # initAdsRechargeRequest
|
||||
FAKE_TRANSACTION, # getAdsRechargeLink
|
||||
]
|
||||
),
|
||||
),
|
||||
patch("pyfragment.methods.recharge_ads.execute_transaction_request", AsyncMock(return_value=FAKE_TRANSACTION)),
|
||||
patch("pyfragment.methods.recharge_ads.get_account_info", AsyncMock(return_value=FAKE_ACCOUNT)),
|
||||
patch("pyfragment.methods.recharge_ads.process_transaction", AsyncMock(return_value=FAKE_TX_HASH)),
|
||||
):
|
||||
result = await client.recharge_ads(FAKE_ADS_ACCOUNT, amount=10)
|
||||
|
||||
+15
-41
@@ -6,7 +6,6 @@ import pytest
|
||||
|
||||
from pyfragment import FragmentClient
|
||||
from pyfragment.types import UsernamesResult
|
||||
from tests.shared import FAKE_HASH
|
||||
|
||||
FAKE_HTML = """
|
||||
<tr class="tm-row-selectable">
|
||||
@@ -22,18 +21,12 @@ FAKE_HTML = """
|
||||
"""
|
||||
|
||||
|
||||
# search_usernames mocked tests
|
||||
# search_usernames result parsing tests
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_search_usernames_basic(client: FragmentClient) -> None:
|
||||
with (
|
||||
patch("pyfragment.methods.search_usernames.get_fragment_hash", AsyncMock(return_value=FAKE_HASH)),
|
||||
patch(
|
||||
"pyfragment.methods.search_usernames.fragment_request",
|
||||
AsyncMock(return_value={"ok": True, "html": FAKE_HTML}),
|
||||
),
|
||||
):
|
||||
with patch.object(client, "call", AsyncMock(return_value={"ok": True, "html": FAKE_HTML})):
|
||||
result = await client.search_usernames("coolname")
|
||||
|
||||
assert isinstance(result, UsernamesResult)
|
||||
@@ -46,13 +39,7 @@ async def test_search_usernames_basic(client: FragmentClient) -> None:
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_search_usernames_empty_html(client: FragmentClient) -> None:
|
||||
with (
|
||||
patch("pyfragment.methods.search_usernames.get_fragment_hash", AsyncMock(return_value=FAKE_HASH)),
|
||||
patch(
|
||||
"pyfragment.methods.search_usernames.fragment_request",
|
||||
AsyncMock(return_value={"ok": True}),
|
||||
),
|
||||
):
|
||||
with patch.object(client, "call", AsyncMock(return_value={"ok": True})):
|
||||
result = await client.search_usernames("zzz_no_results")
|
||||
|
||||
assert isinstance(result, UsernamesResult)
|
||||
@@ -60,21 +47,18 @@ async def test_search_usernames_empty_html(client: FragmentClient) -> None:
|
||||
assert result.next_offset_id is None
|
||||
|
||||
|
||||
# search_usernames parameter forwarding tests
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_search_usernames_with_sort_and_filter(client: FragmentClient) -> None:
|
||||
with (
|
||||
patch("pyfragment.methods.search_usernames.get_fragment_hash", AsyncMock(return_value=FAKE_HASH)),
|
||||
patch(
|
||||
"pyfragment.methods.search_usernames.fragment_request",
|
||||
AsyncMock(return_value={"ok": True, "html": FAKE_HTML}),
|
||||
) as mock_request,
|
||||
):
|
||||
mock_call = AsyncMock(return_value={"ok": True, "html": FAKE_HTML})
|
||||
with patch.object(client, "call", mock_call):
|
||||
result = await client.search_usernames("durov", sort="price_desc", filter="auction")
|
||||
|
||||
assert isinstance(result, UsernamesResult)
|
||||
call_data = mock_request.call_args[0][3]
|
||||
call_data = mock_call.call_args[0][1]
|
||||
assert call_data["type"] == "usernames"
|
||||
assert call_data["method"] == "searchAuctions"
|
||||
assert call_data["sort"] == "price_desc"
|
||||
assert call_data["filter"] == "auction"
|
||||
assert call_data["query"] == "durov"
|
||||
@@ -82,33 +66,23 @@ async def test_search_usernames_with_sort_and_filter(client: FragmentClient) ->
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_search_usernames_with_offset_id(client: FragmentClient) -> None:
|
||||
with (
|
||||
patch("pyfragment.methods.search_usernames.get_fragment_hash", AsyncMock(return_value=FAKE_HASH)),
|
||||
patch(
|
||||
"pyfragment.methods.search_usernames.fragment_request",
|
||||
AsyncMock(return_value={"ok": True, "html": FAKE_HTML, "next_offset_id": "offset_99"}),
|
||||
) as mock_request,
|
||||
):
|
||||
mock_call = AsyncMock(return_value={"ok": True, "html": FAKE_HTML, "next_offset_id": "offset_99"})
|
||||
with patch.object(client, "call", mock_call):
|
||||
result = await client.search_usernames("durov", offset_id="offset_10")
|
||||
|
||||
assert isinstance(result, UsernamesResult)
|
||||
assert result.next_offset_id == "offset_99"
|
||||
call_data = mock_request.call_args[0][3]
|
||||
call_data = mock_call.call_args[0][1]
|
||||
assert call_data["offset_id"] == "offset_10"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_search_usernames_default_query(client: FragmentClient) -> None:
|
||||
with (
|
||||
patch("pyfragment.methods.search_usernames.get_fragment_hash", AsyncMock(return_value=FAKE_HASH)),
|
||||
patch(
|
||||
"pyfragment.methods.search_usernames.fragment_request",
|
||||
AsyncMock(return_value={"ok": True, "html": FAKE_HTML}),
|
||||
) as mock_request,
|
||||
):
|
||||
mock_call = AsyncMock(return_value={"ok": True, "html": FAKE_HTML})
|
||||
with patch.object(client, "call", mock_call):
|
||||
result = await client.search_usernames()
|
||||
|
||||
assert isinstance(result, UsernamesResult)
|
||||
call_data = mock_request.call_args[0][3]
|
||||
call_data = mock_call.call_args[0][1]
|
||||
assert call_data["query"] == ""
|
||||
assert call_data["type"] == "usernames"
|
||||
|
||||
+15
-41
@@ -6,7 +6,6 @@ import pytest
|
||||
|
||||
from pyfragment import FragmentClient
|
||||
from pyfragment.types import NumbersResult
|
||||
from tests.shared import FAKE_HASH
|
||||
|
||||
FAKE_HTML = """
|
||||
<tr class="tm-row-selectable">
|
||||
@@ -22,18 +21,12 @@ FAKE_HTML = """
|
||||
"""
|
||||
|
||||
|
||||
# search_numbers mocked tests
|
||||
# search_numbers result parsing tests
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_search_numbers_basic(client: FragmentClient) -> None:
|
||||
with (
|
||||
patch("pyfragment.methods.search_numbers.get_fragment_hash", AsyncMock(return_value=FAKE_HASH)),
|
||||
patch(
|
||||
"pyfragment.methods.search_numbers.fragment_request",
|
||||
AsyncMock(return_value={"ok": True, "html": FAKE_HTML}),
|
||||
),
|
||||
):
|
||||
with patch.object(client, "call", AsyncMock(return_value={"ok": True, "html": FAKE_HTML})):
|
||||
result = await client.search_numbers("888")
|
||||
|
||||
assert isinstance(result, NumbersResult)
|
||||
@@ -46,13 +39,7 @@ async def test_search_numbers_basic(client: FragmentClient) -> None:
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_search_numbers_empty_html(client: FragmentClient) -> None:
|
||||
with (
|
||||
patch("pyfragment.methods.search_numbers.get_fragment_hash", AsyncMock(return_value=FAKE_HASH)),
|
||||
patch(
|
||||
"pyfragment.methods.search_numbers.fragment_request",
|
||||
AsyncMock(return_value={"ok": True}),
|
||||
),
|
||||
):
|
||||
with patch.object(client, "call", AsyncMock(return_value={"ok": True})):
|
||||
result = await client.search_numbers("zzz_no_results")
|
||||
|
||||
assert isinstance(result, NumbersResult)
|
||||
@@ -60,21 +47,18 @@ async def test_search_numbers_empty_html(client: FragmentClient) -> None:
|
||||
assert result.next_offset_id is None
|
||||
|
||||
|
||||
# search_numbers parameter forwarding tests
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_search_numbers_with_sort_and_filter(client: FragmentClient) -> None:
|
||||
with (
|
||||
patch("pyfragment.methods.search_numbers.get_fragment_hash", AsyncMock(return_value=FAKE_HASH)),
|
||||
patch(
|
||||
"pyfragment.methods.search_numbers.fragment_request",
|
||||
AsyncMock(return_value={"ok": True, "html": FAKE_HTML}),
|
||||
) as mock_request,
|
||||
):
|
||||
mock_call = AsyncMock(return_value={"ok": True, "html": FAKE_HTML})
|
||||
with patch.object(client, "call", mock_call):
|
||||
result = await client.search_numbers("888", sort="price_asc", filter="sale")
|
||||
|
||||
assert isinstance(result, NumbersResult)
|
||||
call_data = mock_request.call_args[0][3]
|
||||
call_data = mock_call.call_args[0][1]
|
||||
assert call_data["type"] == "numbers"
|
||||
assert call_data["method"] == "searchAuctions"
|
||||
assert call_data["sort"] == "price_asc"
|
||||
assert call_data["filter"] == "sale"
|
||||
assert call_data["query"] == "888"
|
||||
@@ -82,33 +66,23 @@ async def test_search_numbers_with_sort_and_filter(client: FragmentClient) -> No
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_search_numbers_with_offset_id(client: FragmentClient) -> None:
|
||||
with (
|
||||
patch("pyfragment.methods.search_numbers.get_fragment_hash", AsyncMock(return_value=FAKE_HASH)),
|
||||
patch(
|
||||
"pyfragment.methods.search_numbers.fragment_request",
|
||||
AsyncMock(return_value={"ok": True, "html": FAKE_HTML, "next_offset_id": "offset_50"}),
|
||||
) as mock_request,
|
||||
):
|
||||
mock_call = AsyncMock(return_value={"ok": True, "html": FAKE_HTML, "next_offset_id": "offset_50"})
|
||||
with patch.object(client, "call", mock_call):
|
||||
result = await client.search_numbers("888", offset_id="offset_50")
|
||||
|
||||
assert isinstance(result, NumbersResult)
|
||||
assert result.next_offset_id == "offset_50"
|
||||
call_data = mock_request.call_args[0][3]
|
||||
call_data = mock_call.call_args[0][1]
|
||||
assert call_data["offset_id"] == "offset_50"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_search_numbers_default_query(client: FragmentClient) -> None:
|
||||
with (
|
||||
patch("pyfragment.methods.search_numbers.get_fragment_hash", AsyncMock(return_value=FAKE_HASH)),
|
||||
patch(
|
||||
"pyfragment.methods.search_numbers.fragment_request",
|
||||
AsyncMock(return_value={"ok": True, "html": FAKE_HTML}),
|
||||
) as mock_request,
|
||||
):
|
||||
mock_call = AsyncMock(return_value={"ok": True, "html": FAKE_HTML})
|
||||
with patch.object(client, "call", mock_call):
|
||||
result = await client.search_numbers()
|
||||
|
||||
assert isinstance(result, NumbersResult)
|
||||
call_data = mock_request.call_args[0][3]
|
||||
call_data = mock_call.call_args[0][1]
|
||||
assert call_data["query"] == ""
|
||||
assert call_data["type"] == "numbers"
|
||||
|
||||
+21
-57
@@ -6,7 +6,6 @@ import pytest
|
||||
|
||||
from pyfragment import FragmentClient
|
||||
from pyfragment.types import GiftsResult
|
||||
from tests.shared import FAKE_HASH
|
||||
|
||||
FAKE_GIFTS_HTML = """
|
||||
<div class="tm-catalog-grid">
|
||||
@@ -48,18 +47,12 @@ FAKE_GIFTS_HTML = """
|
||||
"""
|
||||
|
||||
|
||||
# search_gifts mocked tests
|
||||
# search_gifts result parsing tests
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_search_gifts_basic(client: FragmentClient) -> None:
|
||||
with (
|
||||
patch("pyfragment.methods.search_gifts.get_fragment_hash", AsyncMock(return_value=FAKE_HASH)),
|
||||
patch(
|
||||
"pyfragment.methods.search_gifts.fragment_request",
|
||||
AsyncMock(return_value={"ok": True, "html": FAKE_GIFTS_HTML}),
|
||||
),
|
||||
):
|
||||
with patch.object(client, "call", AsyncMock(return_value={"ok": True, "html": FAKE_GIFTS_HTML})):
|
||||
result = await client.search_gifts()
|
||||
|
||||
assert isinstance(result, GiftsResult)
|
||||
@@ -76,13 +69,7 @@ async def test_search_gifts_basic(client: FragmentClient) -> None:
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_search_gifts_empty(client: FragmentClient) -> None:
|
||||
with (
|
||||
patch("pyfragment.methods.search_gifts.get_fragment_hash", AsyncMock(return_value=FAKE_HASH)),
|
||||
patch(
|
||||
"pyfragment.methods.search_gifts.fragment_request",
|
||||
AsyncMock(return_value={"ok": True}),
|
||||
),
|
||||
):
|
||||
with patch.object(client, "call", AsyncMock(return_value={"ok": True})):
|
||||
result = await client.search_gifts(query="zzz_no_results")
|
||||
|
||||
assert isinstance(result, GiftsResult)
|
||||
@@ -90,68 +77,50 @@ async def test_search_gifts_empty(client: FragmentClient) -> None:
|
||||
assert result.next_offset is None
|
||||
|
||||
|
||||
# search_gifts parameter forwarding tests
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_search_gifts_with_collection_and_sort(client: FragmentClient) -> None:
|
||||
with (
|
||||
patch("pyfragment.methods.search_gifts.get_fragment_hash", AsyncMock(return_value=FAKE_HASH)),
|
||||
patch(
|
||||
"pyfragment.methods.search_gifts.fragment_request",
|
||||
AsyncMock(return_value={"ok": True, "html": FAKE_GIFTS_HTML}),
|
||||
) as mock_request,
|
||||
):
|
||||
mock_call = AsyncMock(return_value={"ok": True, "html": FAKE_GIFTS_HTML})
|
||||
with patch.object(client, "call", mock_call):
|
||||
result = await client.search_gifts(collection="plushpepe", sort="price_desc", filter="sold")
|
||||
|
||||
assert isinstance(result, GiftsResult)
|
||||
call_data = mock_request.call_args[0][3]
|
||||
call_data = mock_call.call_args[0][1]
|
||||
assert call_data["collection"] == "plushpepe"
|
||||
assert call_data["sort"] == "price_desc"
|
||||
assert call_data["filter"] == "sold"
|
||||
assert call_data["type"] == "gifts"
|
||||
assert call_data["method"] == "searchAuctions"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_search_gifts_with_offset(client: FragmentClient) -> None:
|
||||
with (
|
||||
patch("pyfragment.methods.search_gifts.get_fragment_hash", AsyncMock(return_value=FAKE_HASH)),
|
||||
patch(
|
||||
"pyfragment.methods.search_gifts.fragment_request",
|
||||
AsyncMock(return_value={"ok": True, "html": FAKE_GIFTS_HTML}),
|
||||
) as mock_request,
|
||||
):
|
||||
mock_call = AsyncMock(return_value={"ok": True, "html": FAKE_GIFTS_HTML})
|
||||
with patch.object(client, "call", mock_call):
|
||||
result = await client.search_gifts(offset=60)
|
||||
|
||||
assert isinstance(result, GiftsResult)
|
||||
call_data = mock_request.call_args[0][3]
|
||||
call_data = mock_call.call_args[0][1]
|
||||
assert call_data["offset"] == 60
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_search_gifts_with_view(client: FragmentClient) -> None:
|
||||
with (
|
||||
patch("pyfragment.methods.search_gifts.get_fragment_hash", AsyncMock(return_value=FAKE_HASH)),
|
||||
patch(
|
||||
"pyfragment.methods.search_gifts.fragment_request",
|
||||
AsyncMock(return_value={"ok": True, "html": FAKE_GIFTS_HTML}),
|
||||
) as mock_request,
|
||||
):
|
||||
mock_call = AsyncMock(return_value={"ok": True, "html": FAKE_GIFTS_HTML})
|
||||
with patch.object(client, "call", mock_call):
|
||||
result = await client.search_gifts(collection="artisanbrick", view="Model")
|
||||
|
||||
assert isinstance(result, GiftsResult)
|
||||
call_data = mock_request.call_args[0][3]
|
||||
call_data = mock_call.call_args[0][1]
|
||||
assert call_data["view"] == "Model"
|
||||
assert call_data["collection"] == "artisanbrick"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_search_gifts_with_attr(client: FragmentClient) -> None:
|
||||
with (
|
||||
patch("pyfragment.methods.search_gifts.get_fragment_hash", AsyncMock(return_value=FAKE_HASH)),
|
||||
patch(
|
||||
"pyfragment.methods.search_gifts.fragment_request",
|
||||
AsyncMock(return_value={"ok": True, "html": FAKE_GIFTS_HTML}),
|
||||
) as mock_request,
|
||||
):
|
||||
mock_call = AsyncMock(return_value={"ok": True, "html": FAKE_GIFTS_HTML})
|
||||
with patch.object(client, "call", mock_call):
|
||||
result = await client.search_gifts(
|
||||
collection="artisanbrick",
|
||||
sort="listed",
|
||||
@@ -164,7 +133,7 @@ async def test_search_gifts_with_attr(client: FragmentClient) -> None:
|
||||
)
|
||||
|
||||
assert isinstance(result, GiftsResult)
|
||||
call_data = mock_request.call_args[0][3]
|
||||
call_data = mock_call.call_args[0][1]
|
||||
assert call_data["attr[Model]"] == ["Delicate Wash", "Foosball", "Chocolate"]
|
||||
assert call_data["attr[Backdrop]"] == ["Celtic Blue", "Carrot Juice", "Orange"]
|
||||
assert call_data["attr[Symbol]"] == ["Crystal Ball", "Tetsubin", "Acorn"]
|
||||
@@ -176,16 +145,11 @@ async def test_search_gifts_with_attr(client: FragmentClient) -> None:
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_search_gifts_attr_not_in_data_when_none(client: FragmentClient) -> None:
|
||||
with (
|
||||
patch("pyfragment.methods.search_gifts.get_fragment_hash", AsyncMock(return_value=FAKE_HASH)),
|
||||
patch(
|
||||
"pyfragment.methods.search_gifts.fragment_request",
|
||||
AsyncMock(return_value={"ok": True, "html": FAKE_GIFTS_HTML}),
|
||||
) as mock_request,
|
||||
):
|
||||
mock_call = AsyncMock(return_value={"ok": True, "html": FAKE_GIFTS_HTML})
|
||||
with patch.object(client, "call", mock_call):
|
||||
result = await client.search_gifts()
|
||||
|
||||
assert isinstance(result, GiftsResult)
|
||||
call_data = mock_request.call_args[0][3]
|
||||
call_data = mock_call.call_args[0][1]
|
||||
assert "view" not in call_data
|
||||
assert not any(k.startswith("attr[") for k in call_data)
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
"""Unit tests for get_cookies_from_browser() — browser cookie extraction helper."""
|
||||
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from pyfragment import get_cookies_from_browser
|
||||
from pyfragment.types import CookieError
|
||||
from pyfragment.types.constants import REQUIRED_COOKIE_KEYS
|
||||
|
||||
FAKE_JAR = [
|
||||
{"name": "stel_ssid", "value": "abc123", "domain": "fragment.com"},
|
||||
{"name": "stel_dt", "value": "-120", "domain": "fragment.com"},
|
||||
{"name": "stel_token", "value": "tok_xyz", "domain": "fragment.com"},
|
||||
{"name": "stel_ton_token", "value": "ton_xyz", "domain": "fragment.com"},
|
||||
{"name": "unrelated", "value": "noise", "domain": "fragment.com"},
|
||||
]
|
||||
|
||||
|
||||
def _mock_rookiepy(jar: list[dict] | None = None) -> MagicMock:
|
||||
mock = MagicMock()
|
||||
mock.chrome.return_value = jar if jar is not None else FAKE_JAR
|
||||
return mock
|
||||
|
||||
|
||||
# unsupported browser tests
|
||||
|
||||
|
||||
def test_unsupported_browser_raises() -> None:
|
||||
with pytest.raises(CookieError, match="Unsupported browser"):
|
||||
get_cookies_from_browser("internet_explorer")
|
||||
|
||||
|
||||
# successful extraction tests
|
||||
|
||||
|
||||
def test_returns_required_keys_only() -> None:
|
||||
with patch.dict("sys.modules", {"rookiepy": _mock_rookiepy()}):
|
||||
result = get_cookies_from_browser("chrome")
|
||||
|
||||
assert set(result.keys()) == set(REQUIRED_COOKIE_KEYS)
|
||||
assert result["stel_ssid"] == "abc123"
|
||||
assert result["stel_dt"] == "-120"
|
||||
assert result["stel_token"] == "tok_xyz"
|
||||
assert result["stel_ton_token"] == "ton_xyz"
|
||||
assert "unrelated" not in result
|
||||
|
||||
|
||||
def test_default_browser_is_chrome() -> None:
|
||||
mock_rp = _mock_rookiepy()
|
||||
with patch.dict("sys.modules", {"rookiepy": mock_rp}):
|
||||
get_cookies_from_browser()
|
||||
|
||||
mock_rp.chrome.assert_called_once_with(["fragment.com"])
|
||||
|
||||
|
||||
def test_browser_name_is_case_insensitive() -> None:
|
||||
mock_rp = _mock_rookiepy()
|
||||
with patch.dict("sys.modules", {"rookiepy": mock_rp}):
|
||||
result = get_cookies_from_browser("Chrome")
|
||||
|
||||
assert result["stel_ssid"] == "abc123"
|
||||
|
||||
|
||||
# missing cookies tests
|
||||
|
||||
|
||||
def test_missing_cookies_raises() -> None:
|
||||
partial_jar = [
|
||||
{"name": "stel_ssid", "value": "abc123"},
|
||||
{"name": "stel_dt", "value": "-120"},
|
||||
# stel_token and stel_ton_token missing
|
||||
]
|
||||
with patch.dict("sys.modules", {"rookiepy": _mock_rookiepy(partial_jar)}):
|
||||
with pytest.raises(CookieError, match="Fragment cookies not found in chrome"):
|
||||
get_cookies_from_browser("chrome")
|
||||
|
||||
|
||||
def test_empty_cookie_value_treated_as_missing() -> None:
|
||||
jar_with_empty = [
|
||||
{"name": "stel_ssid", "value": "abc123"},
|
||||
{"name": "stel_dt", "value": ""}, # empty — should be treated as missing
|
||||
{"name": "stel_token", "value": "tok_xyz"},
|
||||
{"name": "stel_ton_token", "value": "ton_xyz"},
|
||||
]
|
||||
with patch.dict("sys.modules", {"rookiepy": _mock_rookiepy(jar_with_empty)}):
|
||||
with pytest.raises(CookieError, match="Fragment cookies not found in chrome"):
|
||||
get_cookies_from_browser("chrome")
|
||||
|
||||
|
||||
# read failure tests
|
||||
|
||||
|
||||
def test_browser_read_error_raises() -> None:
|
||||
mock_rp = MagicMock()
|
||||
mock_rp.chrome.side_effect = PermissionError("locked")
|
||||
with patch.dict("sys.modules", {"rookiepy": mock_rp}):
|
||||
with pytest.raises(CookieError, match="Failed to read chrome cookies"):
|
||||
get_cookies_from_browser("chrome")
|
||||
Reference in New Issue
Block a user