refactor: restructure as installable PyPI package

- Rename app/ → fragmentapi/ for proper package naming
- Add FragmentClient class with gift_premium, gift_stars, topup_ton methods
- Restructure core/ → types/ (exceptions, results, constants)
- Merge utils/hash.py into utils/client.py
- Replace _version.py with importlib.metadata
- Add input validation with min/max bounds
- Add unit tests: decode, client init/cookies
- Rename 002_test_hash → 003_test_hash
- Clean up pyproject.toml: pin deps, production classifiers
This commit is contained in:
bohd4nx
2026-03-15 21:03:03 +02:00
parent 4e017cb7a1
commit d2d046a2b9
38 changed files with 1051 additions and 959 deletions
+12 -7
View File
@@ -1,11 +1,11 @@
"""Tests for clean_decode() — BOC-encoded Fragment payloads decode to
human-readable UTF-8 with the Telegram label and Ref# intact."""
"""Tests for clean_decode() — BOC-encoded Fragment payloads decode to UTF-8."""
import re
import pytest
from app.utils.decoder import clean_decode
from fragmentapi.types import RequestError
from fragmentapi.utils.decoder import clean_decode
PAYLOADS = [
pytest.param(
@@ -24,12 +24,17 @@ PAYLOADS = [
@pytest.mark.parametrize("payload", PAYLOADS)
def test_payload(payload: str) -> None:
def test_decode_payload(payload: str) -> None:
result = clean_decode(payload)
assert "Telegram" in result
assert re.search(r"Ref#[A-Za-z0-9]+", result), f"no Ref# in {result!r}"
assert all(ord(c) <= 127 for c in result), f"non-ASCII chars in {result!r}"
assert all(ord(c) < 128 for c in result), f"non-ASCII chars in {result!r}"
def test_empty_input_returns_string() -> None:
assert isinstance(clean_decode(""), str)
def test_empty_payload_returns_empty_string() -> None:
assert clean_decode("") == ""
def test_invalid_payload_raises_request_error() -> None:
with pytest.raises(RequestError):
clean_decode("!!!not-valid-base64!!!")
+81
View File
@@ -0,0 +1,81 @@
"""Unit tests for FragmentClient — init validation and cookie parsing (no network calls)."""
import json
import pytest
from fragmentapi import FragmentClient
from fragmentapi.types import ConfigError, CookiesError
VALID_SEED = "abandon " * 23 + "about"
VALID_API_KEY = "test_api_key"
VALID_COOKIES = {
"stel_ssid": "x",
"stel_dt": "x",
"stel_token": "x",
"stel_ton_token": "x",
}
def test_valid_init() -> None:
client = FragmentClient(seed=VALID_SEED, api_key=VALID_API_KEY, cookies=VALID_COOKIES)
assert client.seed == VALID_SEED.strip()
assert client.api_key == VALID_API_KEY
assert client.wallet_version == "V5R1"
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"
def test_wallet_version_is_case_insensitive() -> None:
client = FragmentClient(seed=VALID_SEED, api_key=VALID_API_KEY, cookies=VALID_COOKIES, wallet_version="v5r1")
assert client.wallet_version == "V5R1"
def test_missing_seed_raises() -> None:
with pytest.raises(ConfigError):
FragmentClient(seed="", api_key=VALID_API_KEY, cookies=VALID_COOKIES)
def test_whitespace_only_seed_raises() -> None:
with pytest.raises(ConfigError):
FragmentClient(seed=" ", api_key=VALID_API_KEY, cookies=VALID_COOKIES)
def test_missing_api_key_raises() -> None:
with pytest.raises(ConfigError):
FragmentClient(seed=VALID_SEED, api_key="", cookies=VALID_COOKIES)
def test_unsupported_wallet_version_raises() -> None:
with pytest.raises(ConfigError):
FragmentClient(seed=VALID_SEED, api_key=VALID_API_KEY, cookies=VALID_COOKIES, wallet_version="V3R2")
def test_cookies_as_json_string() -> None:
client = FragmentClient(seed=VALID_SEED, api_key=VALID_API_KEY, cookies=json.dumps(VALID_COOKIES))
assert client.cookies == VALID_COOKIES
def test_invalid_cookies_json_raises() -> None:
with pytest.raises(CookiesError):
FragmentClient(seed=VALID_SEED, api_key=VALID_API_KEY, cookies="{not valid json}")
def test_missing_cookie_key_raises() -> None:
with pytest.raises(CookiesError):
FragmentClient(seed=VALID_SEED, api_key=VALID_API_KEY, cookies={"stel_ssid": "x"})
def test_empty_cookie_value_raises() -> None:
bad = {**VALID_COOKIES, "stel_token": ""}
with pytest.raises(CookiesError):
FragmentClient(seed=VALID_SEED, api_key=VALID_API_KEY, cookies=bad)
def test_whitespace_cookie_value_raises() -> None:
bad = {**VALID_COOKIES, "stel_ton_token": " "}
with pytest.raises(CookiesError):
FragmentClient(seed=VALID_SEED, api_key=VALID_API_KEY, cookies=bad)
@@ -5,8 +5,8 @@ import re
import pytest
from app.core.constants import BASE_HEADERS, STARS_PAGE
from app.utils.hash import get_fragment_hash
from fragmentapi.types import BASE_HEADERS, STARS_PAGE
from fragmentapi.utils import get_fragment_hash
@pytest.mark.asyncio
+10 -6
View File
@@ -1,13 +1,17 @@
import pytest
import json
from pathlib import Path
from app.core.cookies import load_cookies
from app.core.exceptions import CookiesError
import pytest
@pytest.fixture
def cookies():
"""Load Fragment cookies; skip the test if they are unavailable."""
"""Load Fragment cookies from cookies.json; skip the test if unavailable."""
cookies_path = Path(__file__).resolve().parents[1] / "cookies.json"
if not cookies_path.exists():
pytest.skip("cookies.json not found")
try:
return load_cookies()
except CookiesError as exc:
with cookies_path.open("r", encoding="utf-8") as f:
return json.load(f)
except Exception as exc:
pytest.skip(f"Cookies unavailable — {exc}")