From 4ce0aad021d00435c9a664ae5ab34821fca1b552 Mon Sep 17 00:00:00 2001 From: wisecolt Date: Sat, 7 Mar 2026 02:46:10 +0300 Subject: [PATCH] test: add live e2e happyfappy CLI tests with detailed output --- README.md | 14 +++ pyproject.toml | 10 ++ tests/conftest.py | 16 +++ tests/e2e/test_happyfappy_live.py | 163 ++++++++++++++++++++++++++++++ 4 files changed, 203 insertions(+) create mode 100644 tests/conftest.py create mode 100644 tests/e2e/test_happyfappy_live.py diff --git a/README.md b/README.md index a07d8e3..d8aa19f 100644 --- a/README.md +++ b/README.md @@ -91,3 +91,17 @@ wscraper hf -a dtf -u "https://www.happyfappy.net/torrents.php?id=110178" -c coo │ └── happyfappy.py └── README.md ``` + +## 6) E2E Test (Canli) + +Bu testler gercek siteye istek atar ve link/selector kirilmalarini yakalamayi hedefler. + +```bash +python -m pip install -e ".[test]" +WSCRAPER_E2E=1 pytest -m e2e -s -vv --color=yes +``` + +Opsiyonel degiskenler: + +- `WSCRAPER_COOKIE_FILE` (varsayilan: `cookies.txt`) +- `WSCRAPER_TEST_TORRENT_URL` (varsayilan: `https://www.happyfappy.net/torrents.php?id=110178`) diff --git a/pyproject.toml b/pyproject.toml index 1c31d88..4dbca05 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -12,6 +12,11 @@ dependencies = [ "scrapling[fetchers]==0.4.1", ] +[project.optional-dependencies] +test = [ + "pytest>=8.0", +] + [project.scripts] wscraper = "wscraper.cli:main" @@ -20,3 +25,8 @@ package-dir = {"" = "src"} [tool.setuptools.packages.find] where = ["src"] + +[tool.pytest.ini_options] +markers = [ + "e2e: live end-to-end tests against external services", +] diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..7db3884 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,16 @@ +from __future__ import annotations + + +def pytest_terminal_summary(terminalreporter, exitstatus, config): + _ = (exitstatus, config) + passed = len(terminalreporter.stats.get("passed", [])) + failed = len(terminalreporter.stats.get("failed", [])) + skipped = len(terminalreporter.stats.get("skipped", [])) + + terminalreporter.write_sep("=", "E2E SUMMARY", cyan=True) + terminalreporter.write_line(f"✅ Passed : {passed}", green=True) + if failed: + terminalreporter.write_line(f"❌ Failed : {failed}", red=True) + else: + terminalreporter.write_line(f"❌ Failed : {failed}", green=True) + terminalreporter.write_line(f"⚠️ Skipped: {skipped}", yellow=True) diff --git a/tests/e2e/test_happyfappy_live.py b/tests/e2e/test_happyfappy_live.py new file mode 100644 index 0000000..97a5004 --- /dev/null +++ b/tests/e2e/test_happyfappy_live.py @@ -0,0 +1,163 @@ +from __future__ import annotations + +import json +import os +import subprocess +import sys +import time +from pathlib import Path + +import pytest + + +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") +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}") + + return_code, output_text = _run_cli_live( + [ + "happyfappy", + "--action", + "get-bookmarks", + "-c", + str(cookie_file), + "-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", "isVR", "title", "backgroundImage"): + assert required_key in first, f"missing key: {required_key}" + assert isinstance(first["pageURL"], str) and first["pageURL"].startswith("http") + assert isinstance(first["isVR"], bool) + assert isinstance(first["title"], str) and first["title"].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 = Path(os.getenv("WSCRAPER_COOKIE_FILE", "cookies.txt")) + if not cookie_file.exists(): + pytest.skip(f"Cookie file not found: {cookie_file}") + + test_url = os.getenv( + "WSCRAPER_TEST_TORRENT_URL", + "https://www.happyfappy.net/torrents.php?id=110178", + ) + output_dir = tmp_path / "torrent" + _log(tr, f"Output dir: {output_dir}") + + return_code, output_text = _run_cli_live( + [ + "happyfappy", + "--action", + "download-torrent-files", + "-u", + test_url, + "-c", + str(cookie_file), + "-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"