mirror of
https://github.com/feder-cr/invisible_playwright.git
synced 2026-06-07 08:35:12 +02:00
377 lines
14 KiB
Python
377 lines
14 KiB
Python
import hashlib
|
|
import io
|
|
import tarfile
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
import responses
|
|
|
|
from invisible_playwright.constants import BINARY_VERSION
|
|
from invisible_playwright.download import (
|
|
_extract,
|
|
_github_token,
|
|
_parse_checksums,
|
|
_parse_owner_repo,
|
|
_sha256_file,
|
|
ensure_binary,
|
|
)
|
|
|
|
|
|
def _make_zip(path: Path, inner_name: str, payload: bytes) -> bytes:
|
|
import zipfile
|
|
buf = io.BytesIO()
|
|
with zipfile.ZipFile(buf, "w") as zf:
|
|
zf.writestr(inner_name, payload)
|
|
data = buf.getvalue()
|
|
path.write_bytes(data)
|
|
return data
|
|
|
|
|
|
def _make_targz(path: Path, inner_name: str, payload: bytes) -> bytes:
|
|
buf = io.BytesIO()
|
|
with tarfile.open(fileobj=buf, mode="w:gz") as tf:
|
|
info = tarfile.TarInfo(name=inner_name)
|
|
info.size = len(payload)
|
|
tf.addfile(info, io.BytesIO(payload))
|
|
data = buf.getvalue()
|
|
path.write_bytes(data)
|
|
return data
|
|
|
|
|
|
@pytest.mark.unit
|
|
@responses.activate
|
|
def test_ensure_binary_downloads_and_verifies(tmp_path, monkeypatch):
|
|
"""Full path: cache miss -> HTTP GET -> SHA256 check -> extract -> return path."""
|
|
cache = tmp_path / "cache"
|
|
monkeypatch.setattr("invisible_playwright.download.cache_root", lambda: cache)
|
|
|
|
archive_path = tmp_path / "archive.zip"
|
|
archive_bytes = _make_zip(archive_path, "firefox.exe", b"PEX!")
|
|
archive_sha = hashlib.sha256(archive_bytes).hexdigest()
|
|
from invisible_playwright.constants import ARCHIVE_NAME
|
|
asset = ARCHIVE_NAME("win32", "AMD64")
|
|
|
|
url_archive = f"https://github.com/feder-cr/invisible_playwright/releases/download/{BINARY_VERSION}/{asset}"
|
|
url_sums = f"https://github.com/feder-cr/invisible_playwright/releases/download/{BINARY_VERSION}/checksums.txt"
|
|
|
|
responses.add(responses.GET, url_archive, body=archive_bytes, status=200,
|
|
content_type="application/zip")
|
|
responses.add(responses.GET, url_sums,
|
|
body=f"{archive_sha} {asset}\n", status=200)
|
|
|
|
monkeypatch.setattr("sys.platform", "win32")
|
|
import platform
|
|
monkeypatch.setattr(platform, "machine", lambda: "AMD64")
|
|
|
|
path = ensure_binary()
|
|
assert Path(path).exists()
|
|
assert Path(path).name == "firefox.exe"
|
|
|
|
|
|
@pytest.mark.unit
|
|
@responses.activate
|
|
def test_ensure_binary_rejects_sha_mismatch(tmp_path, monkeypatch):
|
|
cache = tmp_path / "cache"
|
|
monkeypatch.setattr("invisible_playwright.download.cache_root", lambda: cache)
|
|
archive_path = tmp_path / "archive.zip"
|
|
archive_bytes = _make_zip(archive_path, "firefox.exe", b"PEX!")
|
|
wrong_sha = "0" * 64
|
|
from invisible_playwright.constants import ARCHIVE_NAME
|
|
asset = ARCHIVE_NAME("win32", "AMD64")
|
|
|
|
url_archive = f"https://github.com/feder-cr/invisible_playwright/releases/download/{BINARY_VERSION}/{asset}"
|
|
url_sums = f"https://github.com/feder-cr/invisible_playwright/releases/download/{BINARY_VERSION}/checksums.txt"
|
|
responses.add(responses.GET, url_archive, body=archive_bytes, status=200)
|
|
responses.add(responses.GET, url_sums, body=f"{wrong_sha} {asset}\n", status=200)
|
|
|
|
monkeypatch.setattr("sys.platform", "win32")
|
|
import platform
|
|
monkeypatch.setattr(platform, "machine", lambda: "AMD64")
|
|
|
|
with pytest.raises(RuntimeError, match="SHA256"):
|
|
ensure_binary()
|
|
|
|
|
|
# DL1: cache hit returns cached path without HTTP call
|
|
@pytest.mark.unit
|
|
def test_ensure_binary_cache_hit_skips_http(tmp_path, monkeypatch):
|
|
"""When the binary already exists in cache, ensure_binary returns immediately
|
|
without issuing any HTTP request."""
|
|
cache = tmp_path / "cache"
|
|
version_dir = cache / BINARY_VERSION
|
|
version_dir.mkdir(parents=True)
|
|
pre_cached = version_dir / "firefox.exe"
|
|
pre_cached.write_text("cached-content")
|
|
|
|
monkeypatch.setattr("invisible_playwright.download.cache_root", lambda: cache)
|
|
monkeypatch.setattr("sys.platform", "win32")
|
|
import platform
|
|
monkeypatch.setattr(platform, "machine", lambda: "AMD64")
|
|
|
|
def _fail_get(*args, **kwargs):
|
|
raise AssertionError("HTTP must not be called on cache hit")
|
|
monkeypatch.setattr("invisible_playwright.download.requests.get", _fail_get)
|
|
|
|
path = ensure_binary()
|
|
assert path == pre_cached
|
|
assert path.read_text() == "cached-content"
|
|
|
|
|
|
# DL2: .tar.gz extraction works
|
|
@pytest.mark.unit
|
|
def test_extract_tar_gz(tmp_path):
|
|
"""_extract handles .tar.gz archives and unpacks the inner files."""
|
|
archive = tmp_path / "bundle.tar.gz"
|
|
_make_targz(archive, "firefox", b"ELF!")
|
|
dst = tmp_path / "out"
|
|
|
|
_extract(archive, dst)
|
|
|
|
assert (dst / "firefox").exists()
|
|
assert (dst / "firefox").read_bytes() == b"ELF!"
|
|
|
|
|
|
# DL3: checksum line with comment (#) is skipped
|
|
@pytest.mark.unit
|
|
def test_parse_checksums_skips_comments_and_blanks():
|
|
text = (
|
|
"# this is a comment\n"
|
|
"\n"
|
|
" # indented comment\n"
|
|
"abc123 file1.zip\n"
|
|
"def456 file2.tar.gz\n"
|
|
)
|
|
out = _parse_checksums(text)
|
|
assert out == {"file1.zip": "abc123", "file2.tar.gz": "def456"}
|
|
|
|
|
|
# DL3 sibling: malformed lines (fewer than 2 fields) are silently ignored
|
|
@pytest.mark.unit
|
|
def test_parse_checksums_ignores_single_field_lines():
|
|
text = "loner\nabc123 file.zip\n"
|
|
out = _parse_checksums(text)
|
|
assert out == {"file.zip": "abc123"}
|
|
|
|
|
|
# DL3 sibling: last field is treated as filename (supports trailing whitespace tokens)
|
|
@pytest.mark.unit
|
|
def test_parse_checksums_uses_last_token_as_filename():
|
|
text = "abc123 some/nested/file.zip\n"
|
|
out = _parse_checksums(text)
|
|
assert "some/nested/file.zip" in out
|
|
|
|
|
|
# DL4: unknown archive format (.rar) raises RuntimeError
|
|
@pytest.mark.unit
|
|
def test_extract_unknown_format_raises(tmp_path):
|
|
archive = tmp_path / "thing.rar"
|
|
archive.write_bytes(b"not-a-real-rar")
|
|
dst = tmp_path / "out"
|
|
|
|
with pytest.raises(RuntimeError, match="unknown archive format"):
|
|
_extract(archive, dst)
|
|
|
|
|
|
# DL5: binary not found after extraction raises RuntimeError
|
|
@pytest.mark.unit
|
|
@responses.activate
|
|
def test_ensure_binary_missing_entry_after_extract_raises(tmp_path, monkeypatch):
|
|
"""If the archive extracts cleanly but the expected entry isn't present,
|
|
ensure_binary raises RuntimeError."""
|
|
cache = tmp_path / "cache"
|
|
monkeypatch.setattr("invisible_playwright.download.cache_root", lambda: cache)
|
|
|
|
archive_path = tmp_path / "archive.zip"
|
|
# zip without firefox.exe inside
|
|
archive_bytes = _make_zip(archive_path, "other.bin", b"X")
|
|
archive_sha = hashlib.sha256(archive_bytes).hexdigest()
|
|
from invisible_playwright.constants import ARCHIVE_NAME
|
|
asset = ARCHIVE_NAME("win32", "AMD64")
|
|
|
|
url_archive = f"https://github.com/feder-cr/invisible_playwright/releases/download/{BINARY_VERSION}/{asset}"
|
|
url_sums = f"https://github.com/feder-cr/invisible_playwright/releases/download/{BINARY_VERSION}/checksums.txt"
|
|
|
|
responses.add(responses.GET, url_archive, body=archive_bytes, status=200)
|
|
responses.add(responses.GET, url_sums, body=f"{archive_sha} {asset}\n", status=200)
|
|
|
|
monkeypatch.setattr("sys.platform", "win32")
|
|
import platform
|
|
monkeypatch.setattr(platform, "machine", lambda: "AMD64")
|
|
|
|
with pytest.raises(RuntimeError, match="binary not found after extraction"):
|
|
ensure_binary()
|
|
|
|
|
|
# Pure helper: _parse_owner_repo
|
|
@pytest.mark.unit
|
|
def test_parse_owner_repo_valid():
|
|
owner, repo = _parse_owner_repo(
|
|
"https://github.com/feder-cr/invisible_playwright/releases/download/x/y"
|
|
)
|
|
assert owner == "feder-cr"
|
|
assert repo == "invisible_playwright"
|
|
|
|
|
|
@pytest.mark.unit
|
|
def test_parse_owner_repo_invalid_raises():
|
|
with pytest.raises(RuntimeError, match="cannot parse owner/repo"):
|
|
_parse_owner_repo("not-a-github-url")
|
|
|
|
|
|
# Pure helper: _sha256_file matches hashlib output
|
|
@pytest.mark.unit
|
|
def test_sha256_file_matches_hashlib(tmp_path):
|
|
payload = b"hello world"
|
|
f = tmp_path / "file.bin"
|
|
f.write_bytes(payload)
|
|
expected = hashlib.sha256(payload).hexdigest()
|
|
assert _sha256_file(f) == expected
|
|
|
|
|
|
# _github_token precedence: STEALTHFOX_GITHUB_TOKEN beats GITHUB_TOKEN
|
|
@pytest.mark.unit
|
|
def test_github_token_stealthfox_wins(monkeypatch):
|
|
monkeypatch.setenv("STEALTHFOX_GITHUB_TOKEN", "stealth")
|
|
monkeypatch.setenv("GITHUB_TOKEN", "generic")
|
|
assert _github_token() == "stealth"
|
|
|
|
|
|
@pytest.mark.unit
|
|
def test_github_token_falls_back_to_github_token(monkeypatch):
|
|
monkeypatch.delenv("STEALTHFOX_GITHUB_TOKEN", raising=False)
|
|
monkeypatch.setenv("GITHUB_TOKEN", "generic")
|
|
assert _github_token() == "generic"
|
|
|
|
|
|
@pytest.mark.unit
|
|
def test_github_token_none_when_unset(monkeypatch):
|
|
monkeypatch.delenv("STEALTHFOX_GITHUB_TOKEN", raising=False)
|
|
monkeypatch.delenv("GITHUB_TOKEN", raising=False)
|
|
assert _github_token() is None
|
|
|
|
|
|
# Bonus coverage: unsupported platform raises NotImplementedError before any HTTP
|
|
@pytest.mark.unit
|
|
def test_ensure_binary_unsupported_platform_raises(monkeypatch):
|
|
monkeypatch.setattr("sys.platform", "darwin")
|
|
import platform
|
|
monkeypatch.setattr(platform, "machine", lambda: "AMD64")
|
|
with pytest.raises(NotImplementedError, match="unsupported platform"):
|
|
ensure_binary()
|
|
|
|
|
|
# ──────────────────────────────────────────────────────────────────────
|
|
# Linux platform tests — exercise the tar.gz extraction path. Mirrors
|
|
# the Windows .zip tests above so both archive formats are covered.
|
|
# ──────────────────────────────────────────────────────────────────────
|
|
|
|
|
|
@pytest.mark.unit
|
|
@responses.activate
|
|
def test_ensure_binary_downloads_and_verifies_linux(tmp_path, monkeypatch):
|
|
"""Linux happy path: tar.gz download → SHA256 check → extract → return path."""
|
|
cache = tmp_path / "cache"
|
|
monkeypatch.setattr("invisible_playwright.download.cache_root", lambda: cache)
|
|
|
|
archive_path = tmp_path / "archive.tar.gz"
|
|
archive_bytes = _make_targz(archive_path, "firefox", b"ELF!")
|
|
archive_sha = hashlib.sha256(archive_bytes).hexdigest()
|
|
from invisible_playwright.constants import ARCHIVE_NAME
|
|
asset = ARCHIVE_NAME("linux", "x86_64")
|
|
|
|
url_archive = f"https://github.com/feder-cr/invisible_playwright/releases/download/{BINARY_VERSION}/{asset}"
|
|
url_sums = f"https://github.com/feder-cr/invisible_playwright/releases/download/{BINARY_VERSION}/checksums.txt"
|
|
|
|
responses.add(responses.GET, url_archive, body=archive_bytes, status=200,
|
|
content_type="application/gzip")
|
|
responses.add(responses.GET, url_sums,
|
|
body=f"{archive_sha} {asset}\n", status=200)
|
|
|
|
monkeypatch.setattr("sys.platform", "linux")
|
|
import platform
|
|
monkeypatch.setattr(platform, "machine", lambda: "x86_64")
|
|
|
|
path = ensure_binary()
|
|
assert Path(path).exists()
|
|
assert Path(path).name == "firefox"
|
|
|
|
|
|
@pytest.mark.unit
|
|
@responses.activate
|
|
def test_ensure_binary_rejects_sha_mismatch_linux(tmp_path, monkeypatch):
|
|
"""Linux SHA mismatch must raise — the tar.gz path runs the same
|
|
verifier as the .zip path, so a corrupted archive is rejected before
|
|
extraction regardless of platform."""
|
|
cache = tmp_path / "cache"
|
|
monkeypatch.setattr("invisible_playwright.download.cache_root", lambda: cache)
|
|
archive_path = tmp_path / "archive.tar.gz"
|
|
archive_bytes = _make_targz(archive_path, "firefox", b"ELF!")
|
|
wrong_sha = "0" * 64
|
|
from invisible_playwright.constants import ARCHIVE_NAME
|
|
asset = ARCHIVE_NAME("linux", "x86_64")
|
|
|
|
url_archive = f"https://github.com/feder-cr/invisible_playwright/releases/download/{BINARY_VERSION}/{asset}"
|
|
url_sums = f"https://github.com/feder-cr/invisible_playwright/releases/download/{BINARY_VERSION}/checksums.txt"
|
|
responses.add(responses.GET, url_archive, body=archive_bytes, status=200)
|
|
responses.add(responses.GET, url_sums, body=f"{wrong_sha} {asset}\n", status=200)
|
|
|
|
monkeypatch.setattr("sys.platform", "linux")
|
|
import platform
|
|
monkeypatch.setattr(platform, "machine", lambda: "x86_64")
|
|
|
|
with pytest.raises(RuntimeError, match="SHA256"):
|
|
ensure_binary()
|
|
|
|
|
|
@pytest.mark.unit
|
|
def test_ensure_binary_cache_hit_skips_http_linux(tmp_path, monkeypatch):
|
|
"""Linux cache hit short-circuits before any HTTP. Looks for the
|
|
``firefox`` entry (not ``firefox.exe``) per ``BINARY_ENTRY_REL``."""
|
|
cache = tmp_path / "cache"
|
|
version_dir = cache / BINARY_VERSION
|
|
version_dir.mkdir(parents=True)
|
|
pre_cached = version_dir / "firefox"
|
|
pre_cached.write_text("cached-content")
|
|
|
|
monkeypatch.setattr("invisible_playwright.download.cache_root", lambda: cache)
|
|
monkeypatch.setattr("sys.platform", "linux")
|
|
import platform
|
|
monkeypatch.setattr(platform, "machine", lambda: "x86_64")
|
|
|
|
def _fail_get(*args, **kwargs):
|
|
raise AssertionError("HTTP must not be called on cache hit")
|
|
monkeypatch.setattr("invisible_playwright.download.requests.get", _fail_get)
|
|
|
|
path = ensure_binary()
|
|
assert path == pre_cached
|
|
assert path.read_text() == "cached-content"
|
|
|
|
|
|
@pytest.mark.unit
|
|
@responses.activate
|
|
def test_ensure_binary_missing_entry_after_extract_raises_linux(tmp_path, monkeypatch):
|
|
"""Linux post-extract sanity check: if the tar.gz lacks a ``firefox``
|
|
entry, raise rather than returning a non-existent path. Mirrors the
|
|
Windows test and guards against an upstream release artifact regression."""
|
|
cache = tmp_path / "cache"
|
|
monkeypatch.setattr("invisible_playwright.download.cache_root", lambda: cache)
|
|
|
|
archive_path = tmp_path / "archive.tar.gz"
|
|
# tar.gz without ``firefox`` inside
|
|
archive_bytes = _make_targz(archive_path, "other.bin", b"X")
|
|
archive_sha = hashlib.sha256(archive_bytes).hexdigest()
|
|
from invisible_playwright.constants import ARCHIVE_NAME
|
|
asset = ARCHIVE_NAME("linux", "x86_64")
|
|
|
|
url_archive = f"https://github.com/feder-cr/invisible_playwright/releases/download/{BINARY_VERSION}/{asset}"
|
|
url_sums = f"https://github.com/feder-cr/invisible_playwright/releases/download/{BINARY_VERSION}/checksums.txt"
|
|
|
|
responses.add(responses.GET, url_archive, body=archive_bytes, status=200)
|
|
responses.add(responses.GET, url_sums, body=f"{archive_sha} {asset}\n", status=200)
|
|
|
|
monkeypatch.setattr("sys.platform", "linux")
|
|
import platform
|
|
monkeypatch.setattr(platform, "machine", lambda: "x86_64")
|
|
|
|
with pytest.raises(RuntimeError, match="binary not found after extraction"):
|
|
ensure_binary()
|