fix: remove unnecessary 'method' key from API request data and improve error handling for expired cookies and non-200 HTTP responses

This commit is contained in:
bohd4nx
2026-04-21 20:06:11 +03:00
parent 8c7423a6ab
commit 7923dff8b2
10 changed files with 53 additions and 5 deletions
+1 -1
View File
@@ -45,7 +45,7 @@ async def search_gifts(
FragmentAPIError: If the Fragment API returns an error. FragmentAPIError: If the Fragment API returns an error.
UnexpectedError: For any other unexpected failure. UnexpectedError: For any other unexpected failure.
""" """
data: dict[str, Any] = {"method": "searchAuctions", "type": "gifts", "query": query} data: dict[str, Any] = {"type": "gifts", "query": query}
if collection is not None: if collection is not None:
data["collection"] = collection data["collection"] = collection
if sort is not None: if sort is not None:
+1 -1
View File
@@ -37,7 +37,7 @@ async def search_numbers(
FragmentAPIError: If the Fragment API returns an error. FragmentAPIError: If the Fragment API returns an error.
UnexpectedError: For any other unexpected failure. UnexpectedError: For any other unexpected failure.
""" """
data: dict[str, Any] = {"method": "searchAuctions", "type": "numbers", "query": query} data: dict[str, Any] = {"type": "numbers", "query": query}
if sort is not None: if sort is not None:
data["sort"] = sort data["sort"] = sort
if filter is not None: if filter is not None:
+1 -1
View File
@@ -37,7 +37,7 @@ async def search_usernames(
FragmentAPIError: If the Fragment API returns an error. FragmentAPIError: If the Fragment API returns an error.
UnexpectedError: For any other unexpected failure. UnexpectedError: For any other unexpected failure.
""" """
data: dict[str, Any] = {"method": "searchAuctions", "type": "usernames", "query": query} data: dict[str, Any] = {"type": "usernames", "query": query}
if sort is not None: if sort is not None:
data["sort"] = sort data["sort"] = sort
if filter is not None: if filter is not None:
+1
View File
@@ -46,6 +46,7 @@ class CookieError(ClientError):
"Fragment cookies not found in {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}." "Make sure you are logged in to {url} and have connected your TON wallet in {browser}."
) )
EXPIRED = "Fragment session cookie expired at {expires}. Log in to fragment.com in your browser and extract fresh cookies."
class FragmentAPIError(FragmentError): class FragmentAPIError(FragmentError):
+5
View File
@@ -61,6 +61,11 @@ def get_cookies_from_browser(browser: str = "chrome") -> CookieResult:
continue continue
break break
if expires_iso:
expires_dt = datetime.fromisoformat(expires_iso)
if expires_dt < datetime.now(timezone.utc):
raise CookieError(CookieError.EXPIRED.format(expires=expires_iso))
return CookieResult( return CookieResult(
cookies={k: cookie_map[k] for k in REQUIRED_COOKIE_KEYS}, cookies={k: cookie_map[k] for k in REQUIRED_COOKIE_KEYS},
expires=expires_iso, expires=expires_iso,
+2
View File
@@ -111,6 +111,8 @@ async def fragment_request(
headers=headers, headers=headers,
data=data, data=data,
) )
if resp.status_code != 200:
raise FragmentPageError(FragmentPageError.BAD_STATUS.format(status=resp.status_code, url=f"{FRAGMENT_BASE_URL}/api"))
return parse_json_response(resp, data.get("method", "request")) return parse_json_response(resp, data.get("method", "request"))
+1 -1
View File
@@ -34,7 +34,7 @@ async def process_transaction(client: FragmentClient, transaction_data: dict[str
TransactionError: If the payload is malformed or the broadcast fails. TransactionError: If the payload is malformed or the broadcast fails.
WalletError: If the wallet balance is too low or cannot be fetched. WalletError: If the wallet balance is too low or cannot be fetched.
""" """
if "transaction" not in transaction_data or "messages" not in transaction_data["transaction"]: if "transaction" not in transaction_data or not transaction_data["transaction"].get("messages"):
raise TransactionError(TransactionError.INVALID_PAYLOAD) raise TransactionError(TransactionError.INVALID_PAYLOAD)
message = transaction_data["transaction"]["messages"][0] message = transaction_data["transaction"]["messages"][0]
+6
View File
@@ -103,6 +103,12 @@ async def test_invalid_payload_raises() -> None:
await process_transaction(_make_client(), {"transaction": {}}) await process_transaction(_make_client(), {"transaction": {}})
@pytest.mark.asyncio
async def test_empty_messages_list_raises() -> None:
with pytest.raises(TransactionError):
await process_transaction(_make_client(), {"transaction": {"messages": []}})
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_balance_check_failed_raises_wallet_error() -> None: async def test_balance_check_failed_raises_wallet_error() -> None:
wallet = _make_wallet(balance_nanotons=1_000_000_000) wallet = _make_wallet(balance_nanotons=1_000_000_000)
+20 -1
View File
@@ -1,10 +1,13 @@
"""Unit tests for FragmentClient.call() — raw Fragment API access.""" """Unit tests for FragmentClient.call() — raw Fragment API access."""
from unittest.mock import AsyncMock, patch from unittest.mock import AsyncMock, MagicMock, patch
import httpx
import pytest import pytest
from pyfragment import FragmentClient from pyfragment import FragmentClient
from pyfragment.types import FragmentPageError
from pyfragment.utils.http import fragment_request
from tests.shared import FAKE_HASH, FAKE_RESPONSE from tests.shared import FAKE_HASH, FAKE_RESPONSE
# client.call() mocked tests # client.call() mocked tests
@@ -61,3 +64,19 @@ async def test_call_merges_extra_data(client: FragmentClient) -> None:
_, _, _, sent_data = mock_request.call_args.args _, _, _, sent_data = mock_request.call_args.args
assert sent_data == {"method": "anyMethod", "key": "value", "num": 7} assert sent_data == {"method": "anyMethod", "key": "value", "num": 7}
# fragment_request HTTP status tests
@pytest.mark.asyncio
async def test_fragment_request_non_200_raises() -> None:
"""fragment_request raises FragmentPageError on non-200 HTTP responses."""
response = MagicMock(spec=httpx.Response)
response.status_code = 429
session = AsyncMock(spec=httpx.AsyncClient)
session.post = AsyncMock(return_value=response)
with pytest.raises(FragmentPageError, match="429"):
await fragment_request(session, FAKE_HASH, {}, {"method": "anyMethod"})
+15
View File
@@ -106,6 +106,21 @@ def test_empty_cookie_value_treated_as_missing() -> None:
get_cookies_from_browser("chrome") get_cookies_from_browser("chrome")
# expired cookie tests
def test_expired_cookie_raises() -> None:
expired_jar = [
{"name": "stel_ssid", "value": "abc123", "expires": "2020-01-01T00:00:00.000Z"},
{"name": "stel_dt", "value": "-120"},
{"name": "stel_token", "value": "tok_xyz"},
{"name": "stel_ton_token", "value": "ton_xyz"},
]
with patch(PATCH, _mock_rookiepy(expired_jar)):
with pytest.raises(CookieError, match="expired"):
get_cookies_from_browser("chrome")
# read failure tests # read failure tests