feat: add comprehensive unit tests for FragmentClient methods; include shared test constants

This commit is contained in:
bohd4nx
2026-03-21 16:30:44 +02:00
parent 2d1a119768
commit c7b6c6c933
9 changed files with 454 additions and 30 deletions
+6
View File
@@ -23,6 +23,9 @@ PAYLOADS = [
]
# Decode valid payload tests
@pytest.mark.parametrize("payload", PAYLOADS)
def test_decode_payload(payload: str) -> None:
result = clean_decode(payload)
@@ -31,6 +34,9 @@ def test_decode_payload(payload: str) -> None:
assert all(ord(c) < 128 for c in result), f"non-ASCII chars in {result!r}"
# Edge case tests
def test_empty_payload_returns_empty_string() -> None:
assert clean_decode("") == ""
+34 -28
View File
@@ -6,15 +6,9 @@ import pytest
from pyfragment import FragmentClient
from pyfragment.types import ConfigurationError, CookieError
from tests.shared import VALID_API_KEY, VALID_COOKIES, VALID_SEED
VALID_SEED = "abandon " * 23 + "about"
VALID_API_KEY = "A" * 68
VALID_COOKIES = {
"stel_ssid": "x",
"stel_dt": "x",
"stel_token": "x",
"stel_ton_token": "x",
}
# Client init tests
def test_valid_init() -> None:
@@ -24,6 +18,9 @@ def test_valid_init() -> None:
assert client.wallet_version == "V5R1"
# Wallet version tests
def test_wallet_version_v4r2() -> None:
client = FragmentClient(seed=VALID_SEED, api_key=VALID_API_KEY, cookies=VALID_COOKIES, wallet_version="V4R2")
assert client.wallet_version == "V4R2"
@@ -34,6 +31,14 @@ def test_wallet_version_is_case_insensitive() -> None:
assert client.wallet_version == "V5R1"
def test_unsupported_wallet_version_raises() -> None:
with pytest.raises(ConfigurationError):
FragmentClient(seed=VALID_SEED, api_key=VALID_API_KEY, cookies=VALID_COOKIES, wallet_version="V3R2")
# Seed and mnemonic validation tests
def test_missing_seed_raises() -> None:
with pytest.raises(ConfigurationError):
FragmentClient(seed="", api_key=VALID_API_KEY, cookies=VALID_COOKIES)
@@ -44,14 +49,33 @@ def test_whitespace_only_seed_raises() -> None:
FragmentClient(seed=" ", api_key=VALID_API_KEY, cookies=VALID_COOKIES)
def test_invalid_mnemonic_length_raises() -> None:
bad_seed = " ".join(["word"] * 23)
with pytest.raises(ConfigurationError):
FragmentClient(seed=bad_seed, api_key=VALID_API_KEY, cookies=VALID_COOKIES)
def test_valid_mnemonic_lengths() -> None:
for length in (12, 18, 24):
seed = " ".join(["abandon"] * (length - 1) + ["about"])
client = FragmentClient(seed=seed, api_key=VALID_API_KEY, cookies=VALID_COOKIES)
assert len(client.seed.split()) == length
# API key validation tests
def test_missing_api_key_raises() -> None:
with pytest.raises(ConfigurationError):
FragmentClient(seed=VALID_SEED, api_key="", cookies=VALID_COOKIES)
def test_unsupported_wallet_version_raises() -> None:
def test_short_api_key_raises() -> None:
with pytest.raises(ConfigurationError):
FragmentClient(seed=VALID_SEED, api_key=VALID_API_KEY, cookies=VALID_COOKIES, wallet_version="V3R2")
FragmentClient(seed=VALID_SEED, api_key="A" * 42, cookies=VALID_COOKIES)
# Cookie validation tests
def test_cookies_as_json_string() -> None:
@@ -81,24 +105,6 @@ def test_whitespace_cookie_value_raises() -> None:
FragmentClient(seed=VALID_SEED, api_key=VALID_API_KEY, cookies=bad)
def test_invalid_mnemonic_length_raises() -> None:
bad_seed = " ".join(["word"] * 23)
with pytest.raises(ConfigurationError):
FragmentClient(seed=bad_seed, api_key=VALID_API_KEY, cookies=VALID_COOKIES)
def test_valid_mnemonic_lengths() -> None:
for length in (12, 18, 24):
seed = " ".join(["abandon"] * (length - 1) + ["about"])
client = FragmentClient(seed=seed, api_key=VALID_API_KEY, cookies=VALID_COOKIES)
assert len(client.seed.split()) == length
def test_short_api_key_raises() -> None:
with pytest.raises(ConfigurationError):
FragmentClient(seed=VALID_SEED, api_key="A" * 42, cookies=VALID_COOKIES)
def test_repr() -> None:
client = FragmentClient(seed=VALID_SEED, api_key=VALID_API_KEY, cookies=VALID_COOKIES)
r = repr(client)
+7 -2
View File
@@ -7,8 +7,7 @@ import pytest
from pyfragment.types import TransactionError, WalletError
from pyfragment.utils.wallet import process_transaction
VALID_SEED = "abandon " * 23 + "about"
from tests.shared import VALID_SEED
TRANSACTION_DATA = {
"transaction": {
@@ -51,6 +50,9 @@ def _patch_wallet(wallet: MagicMock):
yield
# Balance threshold tests
@pytest.mark.asyncio
async def test_sufficient_balance_broadcasts() -> None:
wallet = _make_wallet(balance_nanotons=1_000_000_000) # 1 TON, needs 0.556 TON
@@ -85,6 +87,9 @@ async def test_one_nanoton_below_minimum_raises() -> None:
await process_transaction(_make_client(), TRANSACTION_DATA)
# Error handling tests
@pytest.mark.asyncio
async def test_invalid_payload_raises() -> None:
with pytest.raises(TransactionError):
+125
View File
@@ -0,0 +1,125 @@
"""Unit tests for purchase_stars and giveaway_stars — validation and mocked network calls."""
from unittest.mock import AsyncMock, patch
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
# Stars purchase validation tests
@pytest.mark.asyncio
async def test_purchase_stars_amount_too_low(client: FragmentClient) -> None:
with pytest.raises(ConfigurationError):
await client.purchase_stars("@user", amount=49)
@pytest.mark.asyncio
async def test_purchase_stars_amount_too_high(client: FragmentClient) -> None:
with pytest.raises(ConfigurationError):
await client.purchase_stars("@user", amount=1_000_001)
@pytest.mark.asyncio
async def test_purchase_stars_float_amount(client: FragmentClient) -> None:
with pytest.raises(ConfigurationError):
await client.purchase_stars("@user", amount=100.5) # type: ignore[arg-type]
# Stars purchase mocked tests
@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("pyfragment.methods.purchase_stars.execute_transaction_request", AsyncMock(return_value=FAKE_TRANSACTION)),
patch("pyfragment.methods.purchase_stars.process_transaction", AsyncMock(return_value=FAKE_TX_HASH)),
):
result = await client.purchase_stars("@user", amount=500)
assert isinstance(result, StarsResult)
assert result.transaction_id == FAKE_TX_HASH
assert result.username == "@user"
assert result.amount == 500
@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 pytest.raises(UserNotFoundError):
await client.purchase_stars("@ghost", amount=500)
# Stars giveaway validation tests
@pytest.mark.asyncio
async def test_giveaway_stars_winners_too_low(client: FragmentClient) -> None:
with pytest.raises(ConfigurationError):
await client.giveaway_stars("@channel", winners=0, amount=500)
@pytest.mark.asyncio
async def test_giveaway_stars_winners_too_high(client: FragmentClient) -> None:
with pytest.raises(ConfigurationError):
await client.giveaway_stars("@channel", winners=6, amount=500)
@pytest.mark.asyncio
async def test_giveaway_stars_amount_too_low(client: FragmentClient) -> None:
with pytest.raises(ConfigurationError):
await client.giveaway_stars("@channel", winners=1, amount=499)
@pytest.mark.asyncio
async def test_giveaway_stars_amount_too_high(client: FragmentClient) -> None:
with pytest.raises(ConfigurationError):
await client.giveaway_stars("@channel", winners=1, amount=1_000_001)
# Stars giveaway mocked tests
@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("pyfragment.methods.giveaway_stars.execute_transaction_request", AsyncMock(return_value=FAKE_TRANSACTION)),
patch("pyfragment.methods.giveaway_stars.process_transaction", AsyncMock(return_value=FAKE_TX_HASH)),
):
result = await client.giveaway_stars("@channel", winners=3, amount=1000)
assert isinstance(result, StarsGiveawayResult)
assert result.transaction_id == FAKE_TX_HASH
assert result.channel == "@channel"
assert result.winners == 3
assert result.amount == 1000
@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 pytest.raises(UserNotFoundError):
await client.giveaway_stars("@ghost", winners=1, amount=500)
+119
View File
@@ -0,0 +1,119 @@
"""Unit tests for purchase_premium and giveaway_premium — validation and mocked network calls."""
from unittest.mock import AsyncMock, patch
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
# Premium purchase validation tests
@pytest.mark.asyncio
async def test_purchase_premium_invalid_months(client: FragmentClient) -> None:
with pytest.raises(ConfigurationError):
await client.purchase_premium("@user", months=5)
@pytest.mark.asyncio
async def test_purchase_premium_months_zero(client: FragmentClient) -> None:
with pytest.raises(ConfigurationError):
await client.purchase_premium("@user", months=0)
# Premium purchase mocked tests
@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",
AsyncMock(
side_effect=[
{"found": {"recipient": FAKE_RECIPIENT}},
{}, # updatePremiumState
{"req_id": FAKE_REQ_ID},
]
),
),
patch("pyfragment.methods.purchase_premium.execute_transaction_request", AsyncMock(return_value=FAKE_TRANSACTION)),
patch("pyfragment.methods.purchase_premium.process_transaction", AsyncMock(return_value=FAKE_TX_HASH)),
):
result = await client.purchase_premium("@user", months=3)
assert isinstance(result, PremiumResult)
assert result.transaction_id == FAKE_TX_HASH
assert result.username == "@user"
assert result.amount == 3
@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 pytest.raises(UserNotFoundError):
await client.purchase_premium("@ghost", months=3)
# Premium giveaway validation tests
@pytest.mark.asyncio
async def test_giveaway_premium_winners_too_low(client: FragmentClient) -> None:
with pytest.raises(ConfigurationError):
await client.giveaway_premium("@channel", winners=0, months=3)
@pytest.mark.asyncio
async def test_giveaway_premium_winners_too_high(client: FragmentClient) -> None:
with pytest.raises(ConfigurationError):
await client.giveaway_premium("@channel", winners=24_001, months=3)
@pytest.mark.asyncio
async def test_giveaway_premium_invalid_months(client: FragmentClient) -> None:
with pytest.raises(ConfigurationError):
await client.giveaway_premium("@channel", winners=10, months=5)
# Premium giveaway mocked tests
@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("pyfragment.methods.giveaway_premium.execute_transaction_request", AsyncMock(return_value=FAKE_TRANSACTION)),
patch("pyfragment.methods.giveaway_premium.process_transaction", AsyncMock(return_value=FAKE_TX_HASH)),
):
result = await client.giveaway_premium("@channel", winners=10, months=3)
assert isinstance(result, PremiumGiveawayResult)
assert result.transaction_id == FAKE_TX_HASH
assert result.channel == "@channel"
assert result.winners == 10
assert result.amount == 3
@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 pytest.raises(UserNotFoundError):
await client.giveaway_premium("@ghost", winners=1, months=3)
+72
View File
@@ -0,0 +1,72 @@
"""Unit tests for topup_ton — validation and mocked network calls."""
from unittest.mock import AsyncMock, patch
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
# Topup TON validation tests
@pytest.mark.asyncio
async def test_topup_ton_amount_zero(client: FragmentClient) -> None:
with pytest.raises(ConfigurationError):
await client.topup_ton("@user", amount=0)
@pytest.mark.asyncio
async def test_topup_ton_amount_too_high(client: FragmentClient) -> None:
with pytest.raises(ConfigurationError):
await client.topup_ton("@user", amount=1_000_000_001)
@pytest.mark.asyncio
async def test_topup_ton_float_amount(client: FragmentClient) -> None:
with pytest.raises(ConfigurationError):
await client.topup_ton("@user", amount=1.5) # type: ignore[arg-type]
# Topup TON mocked tests
@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",
AsyncMock(
side_effect=[
{}, # updateAdsTopupState
{"found": {"recipient": FAKE_RECIPIENT}},
{"req_id": FAKE_REQ_ID},
]
),
),
patch("pyfragment.methods.topup_ton.execute_transaction_request", AsyncMock(return_value=FAKE_TRANSACTION)),
patch("pyfragment.methods.topup_ton.process_transaction", AsyncMock(return_value=FAKE_TX_HASH)),
):
result = await client.topup_ton("@user", amount=10)
assert isinstance(result, AdsTopupResult)
assert result.transaction_id == FAKE_TX_HASH
assert result.username == "@user"
assert result.amount == 10
@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 pytest.raises(UserNotFoundError):
await client.topup_ton("@ghost", amount=10)
+59
View File
@@ -0,0 +1,59 @@
"""Unit tests for get_wallet — mocked network calls."""
from unittest.mock import AsyncMock, MagicMock, patch
import pytest
from pyfragment import FragmentClient, WalletInfo
FAKE_ADDRESS = "UQCppfw5DxWgdVHf3zkmZS8k1mt9oAUYxQLwq2fz3nhO8No5"
FAKE_BALANCE_NANOTON = 1_500_000_000 # 1.5 TON
# Wallet mocked tests
@pytest.mark.asyncio
async def test_get_wallet_returns_wallet_info(client: FragmentClient) -> None:
mock_wallet = MagicMock()
mock_wallet.refresh = AsyncMock()
mock_wallet.balance = FAKE_BALANCE_NANOTON
mock_wallet.state = MagicMock(value="active")
mock_wallet.address.to_str.return_value = FAKE_ADDRESS
with (
patch("pyfragment.utils.wallet.TonapiClient") as mock_tonapi,
patch("pyfragment.utils.wallet.WALLET_CLASSES") as mock_classes,
):
mock_tonapi.return_value.__aenter__ = AsyncMock(return_value=MagicMock())
mock_tonapi.return_value.__aexit__ = AsyncMock(return_value=False)
mock_classes["V5R1"].from_mnemonic.return_value = (mock_wallet, MagicMock(), None, None)
result = await client.get_wallet()
assert isinstance(result, WalletInfo)
assert result.address == FAKE_ADDRESS
assert result.state == "active"
assert result.balance == round(FAKE_BALANCE_NANOTON / 1_000_000_000, 4)
@pytest.mark.asyncio
async def test_get_wallet_balance_is_zero(client: FragmentClient) -> None:
mock_wallet = MagicMock()
mock_wallet.refresh = AsyncMock()
mock_wallet.balance = 0
mock_wallet.state = MagicMock(value="uninit")
mock_wallet.address.to_str.return_value = FAKE_ADDRESS
with (
patch("pyfragment.utils.wallet.TonapiClient") as mock_tonapi,
patch("pyfragment.utils.wallet.WALLET_CLASSES") as mock_classes,
):
mock_tonapi.return_value.__aenter__ = AsyncMock(return_value=MagicMock())
mock_tonapi.return_value.__aexit__ = AsyncMock(return_value=False)
mock_classes["V5R1"].from_mnemonic.return_value = (mock_wallet, MagicMock(), None, None)
result = await client.get_wallet()
assert result.balance == 0.0
assert result.state == "uninit"
+9
View File
@@ -3,6 +3,9 @@ import os
import pytest
from pyfragment import FragmentClient
from tests.shared import VALID_API_KEY, VALID_COOKIES, VALID_SEED
@pytest.fixture
def cookies():
@@ -14,3 +17,9 @@ def cookies():
return json.loads(raw)
except Exception as exc:
pytest.skip(f"Cookies unavailable — {exc}")
@pytest.fixture
def client() -> FragmentClient:
"""Pre-built FragmentClient with dummy credentials."""
return FragmentClient(seed=VALID_SEED, api_key=VALID_API_KEY, cookies=VALID_COOKIES)
+23
View File
@@ -0,0 +1,23 @@
"""Shared test constants — imported by all test modules that need them."""
from typing import Any
# Shared test credentials
VALID_SEED: str = "abandon " * 23 + "about"
VALID_API_KEY: str = "A" * 68
VALID_COOKIES: dict[str, str] = {
"stel_ssid": "x",
"stel_dt": "x",
"stel_token": "x",
"stel_ton_token": "x",
}
# Fake values for mocked tests
FAKE_HASH: str = "abc123"
FAKE_RECIPIENT: str = "recipient_token"
FAKE_REQ_ID: str = "req_42"
FAKE_TX_HASH: str = "deadbeef" * 8
FAKE_ACCOUNT: dict[str, Any] = {"address": "0:abc", "publicKey": "pub", "chain": "-239", "walletStateInit": "base64=="}
FAKE_TRANSACTION: dict[str, Any] = {"transaction": {"messages": [{"address": "0:abc", "amount": "100000000", "payload": ""}]}}