diff --git a/README.md b/README.md index b3995ef..17800af 100644 --- a/README.md +++ b/README.md @@ -227,6 +227,105 @@ Bu script: Kurulum daha önce tamamsa script aynı işlemleri baştan yapmaz; sadece eksikleri tamamlar. +## Testler + +`wscraper` içinde canlı sistemlere karşı çalışan `pytest` tabanlı e2e testleri vardır. Bunlar varsayılan olarak kapalıdır; yalnızca açıkça etkinleştirildiğinde çalışırlar. + +Test dosyaları: + +- `tests/e2e/test_happyfappy_live.py` +- `tests/e2e/test_privatehd_live.py` +- `tests/e2e/_helpers.py` + +### Testleri Etkinleştirme + +Tüm live testler için: + +```bash +export WSCRAPER_E2E=1 +``` + +Bu değişken yoksa veya `1` değilse, e2e testleri `skip` olur. + +### HappyFappy Live Testleri + +Mevcut test kapsamı: + +- `get-bookmarks` +- `download-torrent-files` + +Kullanılan env değişkenleri: + +- `WSCRAPER_COOKIE_FILE` +- `WSCRAPER_TEST_TORRENT_URL` + +Örnek: + +```bash +export WSCRAPER_E2E=1 +export WSCRAPER_COOKIE_FILE=/absolute/path/to/happyfappy-cookies.txt +export WSCRAPER_TEST_TORRENT_URL="https://www.happyfappy.net/torrents.php?id=110178" +pytest tests/e2e/test_happyfappy_live.py -m e2e -s +``` + +### PrivateHD Live Testleri + +PrivateHD için eklenen test kapsamı: + +- `get-bookmarks` +- `download-torrent-files` +- `remove-bookmark` + +Kullanılan env değişkenleri: + +- `WSCRAPER_PRIVATEHD_COOKIE_FILE` +- `WSCRAPER_PRIVATEHD_WISHLIST_URL` +- `WSCRAPER_PRIVATEHD_TEST_TORRENT_URL` +- `WSCRAPER_PRIVATEHD_TEST_DOWNLOAD_URL` +- `WSCRAPER_PRIVATEHD_TEST_REMOVE_URL` +- `WSCRAPER_PRIVATEHD_TEST_REMOVE_TOKEN` + +Fallback kuralı: + +- `WSCRAPER_PRIVATEHD_COOKIE_FILE` yoksa `WSCRAPER_COOKIE_FILE` kullanılır + +Örnek: + +```bash +export WSCRAPER_E2E=1 +export WSCRAPER_PRIVATEHD_COOKIE_FILE=/absolute/path/to/privatehd-cookies.txt +export WSCRAPER_PRIVATEHD_WISHLIST_URL="https://privatehd.to/profile/blackdockers/wishlist" +export WSCRAPER_PRIVATEHD_TEST_TORRENT_URL="https://privatehd.to/torrent/12345-example" +export WSCRAPER_PRIVATEHD_TEST_DOWNLOAD_URL="https://privatehd.to/download/torrent/12345.example.torrent" +pytest tests/e2e/test_privatehd_live.py -m e2e -s +``` + +### remove-bookmark Testi Hakkında + +`PrivateHD remove-bookmark` testi gerçek wishlist kaydını sildiği için özellikle dikkatli kullanılmalıdır. + +Bu test: + +- yalnızca `WSCRAPER_PRIVATEHD_TEST_REMOVE_URL` ve `WSCRAPER_PRIVATEHD_TEST_REMOVE_TOKEN` verilirse çalışır +- aksi halde güvenli şekilde `skip` olur + +Örnek: + +```bash +export WSCRAPER_E2E=1 +export WSCRAPER_PRIVATEHD_COOKIE_FILE=/absolute/path/to/privatehd-cookies.txt +export WSCRAPER_PRIVATEHD_WISHLIST_URL="https://privatehd.to/profile/blackdockers/wishlist" +export WSCRAPER_PRIVATEHD_TEST_REMOVE_URL="https://privatehd.to/torrent/12345-example" +export WSCRAPER_PRIVATEHD_TEST_REMOVE_TOKEN="467471" +pytest tests/e2e/test_privatehd_live.py -m e2e -s -k remove +``` + +### Notlar + +- Bu testler gerçek tracker hesaplarına ve geçerli cookie'lere ihtiyaç duyar +- `remove-bookmark` testi mutasyon yapar; test datası bilinçli seçilmelidir +- `tests/e2e/_helpers.py`, tüm tracker live testlerinde ortak CLI çalıştırma ve loglama yardımcılarını içerir + ## Dizin Yapısı ```text diff --git a/tests/e2e/_helpers.py b/tests/e2e/_helpers.py new file mode 100644 index 0000000..82e0a7f --- /dev/null +++ b/tests/e2e/_helpers.py @@ -0,0 +1,75 @@ +from __future__ import annotations + +import os +import subprocess +import sys +import time +from pathlib import Path + + +def e2e_enabled() -> bool: + return os.getenv("WSCRAPER_E2E", "").strip() == "1" + + +def base_env() -> dict[str, str]: + env = os.environ.copy() + src_path = str(Path.cwd() / "src") + current_pythonpath = env.get("PYTHONPATH", "").strip() + env["PYTHONPATH"] = f"{src_path}{os.pathsep}{current_pythonpath}" if current_pythonpath else src_path + return env + + +def log(tr, message: str, kind: str = "info") -> None: + icon = "•" + style: dict[str, bool] = {} + if kind == "ok": + icon = "✅" + style = {"green": True} + elif kind == "err": + icon = "❌" + style = {"red": True} + elif kind == "warn": + icon = "⚠️" + style = {"yellow": True} + elif kind == "run": + icon = "🚀" + style = {"cyan": True} + + if tr is not None: + tr.write_line(f"{icon} {message}", **style) + else: + print(f"{icon} {message}") + + +def run_cli_live(args: list[str], tr, timeout: int = 900) -> tuple[int, str]: + cmd = [sys.executable, "-m", "wscraper"] + args + log(tr, f"Running: {' '.join(cmd)}", kind="run") + + started = time.time() + proc = subprocess.Popen( + cmd, + text=True, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + env=base_env(), + ) + + output_lines: list[str] = [] + assert proc.stdout is not None + for line in proc.stdout: + output_lines.append(line) + clean = line.rstrip("\n") + if clean: + if tr is not None: + tr.write_line(f" {clean}") + else: + print(f" {clean}") + + return_code = proc.wait(timeout=timeout) + duration = time.time() - started + if return_code == 0: + log(tr, f"Command finished successfully in {duration:.2f}s", kind="ok") + else: + log(tr, f"Command failed with exit code {return_code} in {duration:.2f}s", kind="err") + + return return_code, "".join(output_lines) diff --git a/tests/e2e/test_happyfappy_live.py b/tests/e2e/test_happyfappy_live.py index 97a5004..4f25bad 100644 --- a/tests/e2e/test_happyfappy_live.py +++ b/tests/e2e/test_happyfappy_live.py @@ -2,100 +2,32 @@ from __future__ import annotations import json import os -import subprocess -import sys -import time from pathlib import Path import pytest +from tests.e2e._helpers import e2e_enabled +from tests.e2e._helpers import log +from tests.e2e._helpers import run_cli_live pytestmark = [pytest.mark.e2e] -def _e2e_enabled() -> bool: - return os.getenv("WSCRAPER_E2E", "").strip() == "1" - - -def _base_env() -> dict[str, str]: - env = os.environ.copy() - src_path = str(Path.cwd() / "src") - current_pythonpath = env.get("PYTHONPATH", "").strip() - env["PYTHONPATH"] = f"{src_path}{os.pathsep}{current_pythonpath}" if current_pythonpath else src_path - return env - - -def _log(tr, message: str, kind: str = "info") -> None: - icon = "•" - style: dict[str, bool] = {} - if kind == "ok": - icon = "✅" - style = {"green": True} - elif kind == "err": - icon = "❌" - style = {"red": True} - elif kind == "warn": - icon = "⚠️" - style = {"yellow": True} - elif kind == "run": - icon = "🚀" - style = {"cyan": True} - - if tr is not None: - tr.write_line(f"{icon} {message}", **style) - else: - print(f"{icon} {message}") - - -def _run_cli_live(args: list[str], tr, timeout: int = 900) -> tuple[int, str]: - cmd = [sys.executable, "-m", "wscraper"] + args - _log(tr, f"Running: {' '.join(cmd)}", kind="run") - - started = time.time() - proc = subprocess.Popen( - cmd, - text=True, - stdout=subprocess.PIPE, - stderr=subprocess.STDOUT, - env=_base_env(), - ) - - output_lines: list[str] = [] - assert proc.stdout is not None - for line in proc.stdout: - output_lines.append(line) - clean = line.rstrip("\n") - if clean: - if tr is not None: - tr.write_line(f" {clean}") - else: - print(f" {clean}") - - return_code = proc.wait(timeout=timeout) - duration = time.time() - started - if return_code == 0: - _log(tr, f"Command finished successfully in {duration:.2f}s", kind="ok") - else: - _log(tr, f"Command failed with exit code {return_code} in {duration:.2f}s", kind="err") - - return return_code, "".join(output_lines) - - @pytest.fixture def tr(request): return request.config.pluginmanager.getplugin("terminalreporter") -@pytest.mark.skipif(not _e2e_enabled(), reason="Set WSCRAPER_E2E=1 to run live tests") +@pytest.mark.skipif(not e2e_enabled(), reason="Set WSCRAPER_E2E=1 to run live tests") def test_get_bookmarks_live(tmp_path: Path, tr) -> None: cookie_file = Path(os.getenv("WSCRAPER_COOKIE_FILE", "cookies.txt")) if not cookie_file.exists(): pytest.skip(f"Cookie file not found: {cookie_file}") output_file = tmp_path / "bookmarks.json" - _log(tr, f"Output file: {output_file}") + log(tr, f"Output file: {output_file}") - return_code, output_text = _run_cli_live( + return_code, output_text = run_cli_live( [ "happyfappy", "--action", @@ -113,7 +45,7 @@ def test_get_bookmarks_live(tmp_path: Path, tr) -> None: data = json.loads(output_file.read_text(encoding="utf-8")) assert isinstance(data, list), "bookmarks output must be a JSON list" assert len(data) >= 1, "expected at least one bookmark record" - _log(tr, f"Extracted records: {len(data)}", kind="ok") + log(tr, f"Extracted records: {len(data)}", kind="ok") first = data[0] assert isinstance(first, dict), "bookmark entry must be an object" @@ -124,7 +56,7 @@ def test_get_bookmarks_live(tmp_path: Path, tr) -> None: assert isinstance(first["title"], str) and first["title"].strip() != "" -@pytest.mark.skipif(not _e2e_enabled(), reason="Set WSCRAPER_E2E=1 to run live tests") +@pytest.mark.skipif(not e2e_enabled(), reason="Set WSCRAPER_E2E=1 to run live tests") def test_download_torrent_file_live(tmp_path: Path, tr) -> None: cookie_file = Path(os.getenv("WSCRAPER_COOKIE_FILE", "cookies.txt")) if not cookie_file.exists(): @@ -135,9 +67,9 @@ def test_download_torrent_file_live(tmp_path: Path, tr) -> None: "https://www.happyfappy.net/torrents.php?id=110178", ) output_dir = tmp_path / "torrent" - _log(tr, f"Output dir: {output_dir}") + log(tr, f"Output dir: {output_dir}") - return_code, output_text = _run_cli_live( + return_code, output_text = run_cli_live( [ "happyfappy", "--action", @@ -156,7 +88,7 @@ def test_download_torrent_file_live(tmp_path: Path, tr) -> None: torrent_files = list(output_dir.glob("*.torrent")) assert len(torrent_files) >= 1, "expected at least one .torrent file" - _log(tr, f"Downloaded .torrent files: {len(torrent_files)}", kind="ok") + log(tr, f"Downloaded .torrent files: {len(torrent_files)}", kind="ok") content = torrent_files[0].read_bytes() assert content.startswith(b"d"), "torrent file should start with bencode dictionary token 'd'" diff --git a/tests/e2e/test_privatehd_live.py b/tests/e2e/test_privatehd_live.py new file mode 100644 index 0000000..253b2b5 --- /dev/null +++ b/tests/e2e/test_privatehd_live.py @@ -0,0 +1,158 @@ +from __future__ import annotations + +import json +import os +from pathlib import Path + +import pytest + +from tests.e2e._helpers import e2e_enabled +from tests.e2e._helpers import log +from tests.e2e._helpers import run_cli_live + + +pytestmark = [pytest.mark.e2e] + + +@pytest.fixture +def tr(request): + return request.config.pluginmanager.getplugin("terminalreporter") + + +def _privatehd_cookie_file() -> Path: + path = os.getenv("WSCRAPER_PRIVATEHD_COOKIE_FILE") or os.getenv("WSCRAPER_COOKIE_FILE", "cookies.txt") + return Path(path) + + +def _privatehd_wishlist_url() -> str: + return os.getenv("WSCRAPER_PRIVATEHD_WISHLIST_URL", "").strip() + + +@pytest.mark.skipif(not e2e_enabled(), reason="Set WSCRAPER_E2E=1 to run live tests") +def test_get_bookmarks_live(tmp_path: Path, tr) -> None: + cookie_file = _privatehd_cookie_file() + if not cookie_file.exists(): + pytest.skip(f"Cookie file not found: {cookie_file}") + + wishlist_url = _privatehd_wishlist_url() + if not wishlist_url: + pytest.skip("Set WSCRAPER_PRIVATEHD_WISHLIST_URL to run PrivateHD live bookmark test") + + output_file = tmp_path / "bookmarks.json" + log(tr, f"Output file: {output_file}") + + return_code, output_text = run_cli_live( + [ + "privatehd", + "--action", + "get-bookmarks", + "-c", + str(cookie_file), + "--wishlist-url", + wishlist_url, + "-o", + str(output_file), + ], + tr, + ) + assert return_code == 0, f"CLI failed:\n{output_text}" + assert output_file.exists(), "bookmarks.json was not created" + + data = json.loads(output_file.read_text(encoding="utf-8")) + assert isinstance(data, list), "bookmarks output must be a JSON list" + assert len(data) >= 1, "expected at least one bookmark record" + log(tr, f"Extracted records: {len(data)}", kind="ok") + + first = data[0] + assert isinstance(first, dict), "bookmark entry must be an object" + for required_key in ("pageURL", "title", "backgroundImage", "downloadURL", "removeToken"): + assert required_key in first, f"missing key: {required_key}" + assert isinstance(first["pageURL"], str) and first["pageURL"].startswith("http") + assert isinstance(first["title"], str) and first["title"].strip() != "" + assert isinstance(first["downloadURL"], str) and first["downloadURL"].startswith("http") + assert isinstance(first["removeToken"], str) and first["removeToken"].strip() != "" + + +@pytest.mark.skipif(not e2e_enabled(), reason="Set WSCRAPER_E2E=1 to run live tests") +def test_download_torrent_file_live(tmp_path: Path, tr) -> None: + cookie_file = _privatehd_cookie_file() + if not cookie_file.exists(): + pytest.skip(f"Cookie file not found: {cookie_file}") + + wishlist_url = _privatehd_wishlist_url() + if not wishlist_url: + pytest.skip("Set WSCRAPER_PRIVATEHD_WISHLIST_URL to run PrivateHD live download test") + + test_url = os.getenv("WSCRAPER_PRIVATEHD_TEST_TORRENT_URL", "").strip() + download_url = os.getenv("WSCRAPER_PRIVATEHD_TEST_DOWNLOAD_URL", "").strip() + if not test_url or not download_url: + pytest.skip("Set WSCRAPER_PRIVATEHD_TEST_TORRENT_URL and WSCRAPER_PRIVATEHD_TEST_DOWNLOAD_URL") + + output_dir = tmp_path / "torrent" + log(tr, f"Output dir: {output_dir}") + + return_code, output_text = run_cli_live( + [ + "privatehd", + "--action", + "download-torrent-files", + "-u", + test_url, + "--download-url", + download_url, + "-c", + str(cookie_file), + "--wishlist-url", + wishlist_url, + "-o", + str(output_dir), + ], + tr, + ) + assert return_code == 0, f"CLI failed:\n{output_text}" + assert output_dir.exists(), "torrent output directory was not created" + + torrent_files = list(output_dir.glob("*.torrent")) + assert len(torrent_files) >= 1, "expected at least one .torrent file" + log(tr, f"Downloaded .torrent files: {len(torrent_files)}", kind="ok") + + content = torrent_files[0].read_bytes() + assert content.startswith(b"d"), "torrent file should start with bencode dictionary token 'd'" + assert b"4:info" in content[:4096], "torrent file should include 'info' dictionary marker" + + +@pytest.mark.skipif(not e2e_enabled(), reason="Set WSCRAPER_E2E=1 to run live tests") +def test_remove_bookmark_live(tr) -> None: + cookie_file = _privatehd_cookie_file() + if not cookie_file.exists(): + pytest.skip(f"Cookie file not found: {cookie_file}") + + wishlist_url = _privatehd_wishlist_url() + if not wishlist_url: + pytest.skip("Set WSCRAPER_PRIVATEHD_WISHLIST_URL to run PrivateHD live remove test") + + test_url = os.getenv("WSCRAPER_PRIVATEHD_TEST_REMOVE_URL", "").strip() + remove_token = os.getenv("WSCRAPER_PRIVATEHD_TEST_REMOVE_TOKEN", "").strip() + if not test_url or not remove_token: + pytest.skip("Set WSCRAPER_PRIVATEHD_TEST_REMOVE_URL and WSCRAPER_PRIVATEHD_TEST_REMOVE_TOKEN") + + return_code, output_text = run_cli_live( + [ + "privatehd", + "--action", + "remove-bookmark", + "-u", + test_url, + "--remove-token", + remove_token, + "-c", + str(cookie_file), + "--wishlist-url", + wishlist_url, + ], + tr, + timeout=240, + ) + assert return_code == 0, f"CLI failed:\n{output_text}" + assert "Bookmark removed successfully." in output_text + log(tr, "PrivateHD bookmark removal completed", kind="ok")