diff --git a/pyfragment/methods/search_gifts.py b/pyfragment/methods/search_gifts.py index 33fd431..badea9e 100644 --- a/pyfragment/methods/search_gifts.py +++ b/pyfragment/methods/search_gifts.py @@ -45,7 +45,7 @@ async def search_gifts( FragmentAPIError: If the Fragment API returns an error. 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: data["collection"] = collection if sort is not None: diff --git a/pyfragment/methods/search_numbers.py b/pyfragment/methods/search_numbers.py index e82e32e..87b02fa 100644 --- a/pyfragment/methods/search_numbers.py +++ b/pyfragment/methods/search_numbers.py @@ -37,7 +37,7 @@ async def search_numbers( FragmentAPIError: If the Fragment API returns an error. 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: data["sort"] = sort if filter is not None: diff --git a/pyfragment/methods/search_usernames.py b/pyfragment/methods/search_usernames.py index 0fd4c39..890dff2 100644 --- a/pyfragment/methods/search_usernames.py +++ b/pyfragment/methods/search_usernames.py @@ -37,7 +37,7 @@ async def search_usernames( FragmentAPIError: If the Fragment API returns an error. 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: data["sort"] = sort if filter is not None: diff --git a/pyfragment/types/exceptions.py b/pyfragment/types/exceptions.py index be28af0..5f34a29 100644 --- a/pyfragment/types/exceptions.py +++ b/pyfragment/types/exceptions.py @@ -46,6 +46,7 @@ class CookieError(ClientError): "Fragment cookies not found in {browser}: {keys}. " "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): diff --git a/pyfragment/utils/cookies.py b/pyfragment/utils/cookies.py index 8e24495..7f9aaa4 100644 --- a/pyfragment/utils/cookies.py +++ b/pyfragment/utils/cookies.py @@ -61,6 +61,11 @@ def get_cookies_from_browser(browser: str = "chrome") -> CookieResult: continue 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( cookies={k: cookie_map[k] for k in REQUIRED_COOKIE_KEYS}, expires=expires_iso, diff --git a/pyfragment/utils/http.py b/pyfragment/utils/http.py index a9672d1..5009506 100644 --- a/pyfragment/utils/http.py +++ b/pyfragment/utils/http.py @@ -111,6 +111,8 @@ async def fragment_request( headers=headers, 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")) diff --git a/pyfragment/utils/wallet.py b/pyfragment/utils/wallet.py index 49fdec6..7a67018 100644 --- a/pyfragment/utils/wallet.py +++ b/pyfragment/utils/wallet.py @@ -34,7 +34,7 @@ async def process_transaction(client: FragmentClient, transaction_data: dict[str TransactionError: If the payload is malformed or the broadcast fails. 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) message = transaction_data["transaction"]["messages"][0] diff --git a/tests/003_test_balance.py b/tests/003_test_balance.py index ab95492..6fa91b5 100644 --- a/tests/003_test_balance.py +++ b/tests/003_test_balance.py @@ -103,6 +103,12 @@ async def test_invalid_payload_raises() -> None: 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 async def test_balance_check_failed_raises_wallet_error() -> None: wallet = _make_wallet(balance_nanotons=1_000_000_000) diff --git a/tests/008_test_call.py b/tests/008_test_call.py index 14b6b59..78404cb 100644 --- a/tests/008_test_call.py +++ b/tests/008_test_call.py @@ -1,10 +1,13 @@ """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 from pyfragment import FragmentClient +from pyfragment.types import FragmentPageError +from pyfragment.utils.http import fragment_request from tests.shared import FAKE_HASH, FAKE_RESPONSE # 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 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"}) diff --git a/tests/014_test_cookies.py b/tests/014_test_cookies.py index 6f927b4..83d6c3d 100644 --- a/tests/014_test_cookies.py +++ b/tests/014_test_cookies.py @@ -106,6 +106,21 @@ def test_empty_cookie_value_treated_as_missing() -> None: 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