test: fortress coverage for download + constants + e2e
#15 shipped because unit tests only covered text-mode sha256sum output. This adds a comprehensive parser test matrix (binary mode `*` prefix, mixed, CRLF, BOM, indent, trailing whitespace, multiple stars, empty, comment-only, sha256sum -b coreutils format) plus the integration sentinel test_ensure_binary_accepts_binary_mode_checksums that reproduces #15 against the live wire format. Also covered for the first time: - _resolve_asset_url public/private branches, auth header propagation, asset-missing failure, HTTP 4xx propagation - _download_file 200/404/500, parent mkdir, auth on api.github.com only (not leaking to CDN URLs) - cache_root / cache_dir_for_version path shape and version isolation - _parse_owner_repo malformed inputs and dash/underscore/dot repo names ARCHIVE_NAME case-matrix (uppercase platform, lowercase machine), unsupported arch rejection (i386, ppc64le, arm64), unsupported platform rejection (darwin, freebsd), BINARY_ENTRY_REL <-> ARCHIVE_NAME invariant, RELEASE_URL_TEMPLATE shape (https, placeholders, owner pointer). New e2e tests (marker `e2e`, excluded by default): clean venv install, fetch against live release, binary launch, real-site Playwright sanity. This is the test suite that would have caught #15 end-to-end before publish. Stats: 275 -> 327 unit tests (+52), 0 -> 6 e2e tests. Controprova: rolling back the parser fix makes 9 of the new tests fail with the exact "no SHA256 for ..." error from #15.
This commit is contained in:
+148
-1
@@ -1,6 +1,13 @@
|
||||
import pytest
|
||||
|
||||
from invisible_playwright.constants import ARCHIVE_NAME, BINARY_BASENAME, BINARY_VERSION
|
||||
from invisible_playwright.constants import (
|
||||
ARCHIVE_NAME,
|
||||
BINARY_BASENAME,
|
||||
BINARY_ENTRY_REL,
|
||||
BINARY_VERSION,
|
||||
FIREFOX_UPSTREAM_VERSION,
|
||||
RELEASE_URL_TEMPLATE,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@@ -33,3 +40,143 @@ def test_archive_name_unsupported_raises():
|
||||
def test_binary_basename_format():
|
||||
assert "firefox" in BINARY_BASENAME.lower()
|
||||
assert "stealth" in BINARY_BASENAME.lower()
|
||||
|
||||
|
||||
# ---- Comprehensive ARCHIVE_NAME edge cases -------------------------------- #
|
||||
# Same risk shape as bug #15: a missed format assumption (sha256sum binary
|
||||
# mode) silently produced wrong output. Same class of bug here would be
|
||||
# uppercase platform string or odd machine value passing through to a
|
||||
# wrong-named asset on the CDN and 404-ing.
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.parametrize("platform_key,machine,expected_substring", [
|
||||
("win32", "AMD64", "win-x86_64.zip"), # Windows reports AMD64
|
||||
("win32", "amd64", "win-x86_64.zip"), # lowercase variant
|
||||
("win32", "x86_64", "win-x86_64.zip"), # mingw-style
|
||||
("linux", "x86_64", "linux-x86_64.tar.gz"), # standard Linux
|
||||
("linux", "AMD64", "linux-x86_64.tar.gz"), # odd but plausible
|
||||
("Linux", "x86_64", "linux-x86_64.tar.gz"), # case-insensitive platform
|
||||
("WIN32", "AMD64", "win-x86_64.zip"), # ALL CAPS platform
|
||||
])
|
||||
def test_archive_name_accepts_case_variations(platform_key, machine, expected_substring):
|
||||
"""sys.platform / platform.machine() return inconsistent casing across
|
||||
OS versions and Python versions. The asset filename must be stable
|
||||
regardless — otherwise the CDN 404s."""
|
||||
assert ARCHIVE_NAME(platform_key, machine).endswith(expected_substring)
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.parametrize("machine", ["i386", "i686", "ppc64le", "armv7l", "riscv64"])
|
||||
def test_archive_name_rejects_unsupported_arches(machine):
|
||||
"""Unsupported arches must raise NotImplementedError with the bad value
|
||||
in the message — silent fallback to a default arch would download the
|
||||
wrong binary, run, and fingerprint differently."""
|
||||
with pytest.raises(NotImplementedError, match=machine):
|
||||
ARCHIVE_NAME("linux", machine)
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.parametrize("machine", ["arm64", "aarch64"])
|
||||
def test_archive_name_arm64_not_yet_supported(machine):
|
||||
"""ARM64 is a frequent request (issue #6). Until binaries exist for it,
|
||||
ARCHIVE_NAME should hard-fail rather than silently degrade. If this test
|
||||
starts failing because someone shipped ARM64 builds, replace it with the
|
||||
positive case."""
|
||||
with pytest.raises(NotImplementedError):
|
||||
ARCHIVE_NAME("linux", machine)
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.parametrize("platform_key", ["darwin", "freebsd", "cygwin", "openbsd"])
|
||||
def test_archive_name_rejects_unsupported_platforms(platform_key):
|
||||
"""Same logic — non-Linux/non-Windows platforms must raise, not silently
|
||||
pick one of the two."""
|
||||
with pytest.raises(NotImplementedError, match=platform_key):
|
||||
ARCHIVE_NAME(platform_key, "x86_64")
|
||||
|
||||
|
||||
# ---- ARCHIVE_NAME ↔ BINARY_ENTRY_REL invariant ---------------------------- #
|
||||
# For every supported platform there MUST be an entry in BINARY_ENTRY_REL,
|
||||
# otherwise ensure_binary() will raise NotImplementedError AFTER having
|
||||
# already downloaded a 110 MB tarball — terrible UX.
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_binary_entry_rel_covers_every_supported_platform():
|
||||
"""If ARCHIVE_NAME accepts a platform key, BINARY_ENTRY_REL must declare
|
||||
where the executable lives inside the archive for it."""
|
||||
for plat in ["win32", "linux"]:
|
||||
ARCHIVE_NAME(plat, "x86_64") # must not raise
|
||||
assert plat in BINARY_ENTRY_REL, (
|
||||
f"ARCHIVE_NAME accepts {plat!r} but BINARY_ENTRY_REL has no entry "
|
||||
f"— ensure_binary() will fail late after a 110 MB download."
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_binary_entry_rel_extension_matches_platform():
|
||||
"""firefox.exe on Windows, plain `firefox` on Linux."""
|
||||
assert BINARY_ENTRY_REL["win32"].endswith(".exe")
|
||||
assert not BINARY_ENTRY_REL["linux"].endswith(".exe")
|
||||
assert BINARY_ENTRY_REL["linux"] == "firefox"
|
||||
|
||||
|
||||
# ---- RELEASE_URL_TEMPLATE shape ------------------------------------------- #
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_release_url_template_is_https():
|
||||
"""No http://. GitHub redirects http but we never accept the redirect."""
|
||||
assert RELEASE_URL_TEMPLATE.startswith("https://github.com/")
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_release_url_template_has_required_placeholders():
|
||||
"""{tag} and {asset} must both be present, otherwise _resolve_asset_url
|
||||
won't format a usable URL and downloads fail with confusing 404s."""
|
||||
assert "{tag}" in RELEASE_URL_TEMPLATE
|
||||
assert "{asset}" in RELEASE_URL_TEMPLATE
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_release_url_template_formats_cleanly():
|
||||
"""Confirm .format() actually substitutes — catches typos like {tags}."""
|
||||
url = RELEASE_URL_TEMPLATE.format(tag="firefox-99", asset="thing.zip")
|
||||
assert "{" not in url and "}" not in url
|
||||
assert "firefox-99" in url
|
||||
assert "thing.zip" in url
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_release_url_points_at_owned_repo():
|
||||
"""The template MUST point at an owner/repo the maintainer actually
|
||||
controls. A typo here would direct everyone's downloads at a stranger's
|
||||
GitHub account — silent supply-chain risk."""
|
||||
assert "/feder-cr/invisible_playwright/" in RELEASE_URL_TEMPLATE, (
|
||||
f"RELEASE_URL_TEMPLATE was changed to point elsewhere: "
|
||||
f"{RELEASE_URL_TEMPLATE!r}. Update this test only if the move is intentional."
|
||||
)
|
||||
|
||||
|
||||
# ---- Firefox upstream version sanity -------------------------------------- #
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_firefox_upstream_version_is_three_part_semver():
|
||||
parts = FIREFOX_UPSTREAM_VERSION.split(".")
|
||||
assert len(parts) >= 2, f"version too short: {FIREFOX_UPSTREAM_VERSION!r}"
|
||||
for p in parts:
|
||||
assert p.isdigit(), f"non-numeric segment in {FIREFOX_UPSTREAM_VERSION!r}"
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_binary_basename_includes_upstream_version():
|
||||
"""The basename references the upstream version, so the asset filename
|
||||
on the CDN encodes which Firefox was patched. Bumping FIREFOX_UPSTREAM_VERSION
|
||||
without rebuilding would leave stale binaries — this guards against
|
||||
accidentally desyncing the two."""
|
||||
assert FIREFOX_UPSTREAM_VERSION in BINARY_BASENAME
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.parametrize("plat", ["win32", "linux"])
|
||||
def test_archive_name_includes_upstream_version(plat):
|
||||
"""Same desync guard, from the other direction."""
|
||||
assert FIREFOX_UPSTREAM_VERSION in ARCHIVE_NAME(plat, "x86_64")
|
||||
|
||||
Reference in New Issue
Block a user