invisible_playwright/tests/test_launcher_helpers.py
feder-cr e524695088 fix(webrtc): ship the validated proxy realness config + CI guards
Audit follow-up (2026-06-10), all validated before commit.

#2 WebRTC — the shipped baseline now MATCHES the manually-validated config
(behind a residential proxy: host=<uuid>.local, srflx=proxy egress, No-Leak,
gathering completes, indistinguishable from vanilla Firefox on BrowserLeaks +
CreepJS):
  - prefs baseline obfuscate_host_addresses False->True; add
    zoom.stealth.webrtc.disable_ipv6=True; drop the dead
    media.peerconnection.ice.disableIPv6 (no-op on FF150)
  - launcher auto-derives the proxy egress IP via _geo.prepare_session_geo
    (one round-trip shared with the timezone resolution) and feeds nICEr via
    STEALTHFOX_WEBRTC_PUBLIC_IP + STEALTHFOX_WEBRTC_DISABLE_IPV6 in _build_env
    (sync + async); an explicit caller env still wins. The C++ mechanisms were
    already in firefox-9 — this activates them, no rebuild.

#1 drop orphan prefs zoom.stealth.timezone + zoom.stealth.seed (read by no C++;
   the live ones are juggler.timezone.override + zoom.stealth.fpp.hw_seed).

#3 release title 'rev N' instead of 'rev firefox-N'.

CI guards (unit, leak-safe — no real proxy/creds, the kind that would have
caught this gap at zero cost):
  - shipped-baseline guard + no-orphan-prefs (test_webrtc_realness.py)
  - egress auto-derive in _build_env (test_launcher_helpers.py)
  - prepare_session_geo returns (tz, egress) (test_geo.py)
CI keeps faking 'behind a proxy' with an in-process TCP-only SOCKS5 + RFC 5737
TEST-NET IPs; real-proxy residential realness stays a LOCAL manual gate.

449 unit pass.
2026-06-10 14:30:16 +02:00

206 lines
7.6 KiB
Python

"""Unit tests for pure helpers in ``launcher.py``.
These cover code paths that are not exercised by the E2E launcher tests
(`test_e2e.py`) because they live in private helpers below the Playwright
boundary. The tests instantiate ``InvisiblePlaywright`` for the methods
that read ``self._profile`` but never enter ``__enter__``, so no Firefox
binary or virtual display is required.
"""
from __future__ import annotations
import pytest
from invisible_playwright import InvisiblePlaywright
from invisible_playwright.launcher import (
_CHROME_H,
_CHROME_W,
_IANA_TO_POSIX_TZ,
_TASKBAR_H,
_tz_env,
)
# ── _tz_env (IANA → POSIX) ────────────────────────────────────────────
@pytest.mark.unit
def test_tz_env_eastern_us_maps_to_posix_with_dst():
"""Eastern US zones share the same POSIX form; spot-check a few."""
assert _tz_env("America/New_York") == "EST5EDT"
assert _tz_env("America/Detroit") == "EST5EDT"
assert _tz_env("America/Indiana/Indianapolis") == "EST5EDT"
@pytest.mark.unit
def test_tz_env_central_mountain_pacific_map_to_posix_with_dst():
assert _tz_env("America/Chicago") == "CST6CDT"
assert _tz_env("America/Denver") == "MST7MDT"
assert _tz_env("America/Los_Angeles") == "PST8PDT"
@pytest.mark.unit
def test_tz_env_phoenix_strips_dst():
"""Arizona (outside Navajo Nation) does NOT observe DST. The POSIX
form must be ``MST7`` (no second segment) — using ``MST7MDT`` caused
FP Pro to deduce vpn_origin_timezone=America/Denver from a 60-minute
offset error in summer. Guard against regression of that mapping.
"""
assert _tz_env("America/Phoenix") == "MST7"
@pytest.mark.unit
def test_tz_env_honolulu_strips_dst():
"""Hawaii does not observe DST. POSIX form ``HST10`` (no DST segment)."""
assert _tz_env("Pacific/Honolulu") == "HST10"
@pytest.mark.unit
def test_tz_env_passthrough_for_unmapped_zone():
"""Zones outside the lookup table fall through to their IANA name —
glibc on Linux reads /usr/share/zoneinfo directly. Windows MSVCRT
won't understand them but that's accepted; the mapping covers the
common residential-proxy zones."""
assert _tz_env("Europe/Berlin") == "Europe/Berlin"
assert _tz_env("Asia/Tokyo") == "Asia/Tokyo"
@pytest.mark.unit
def test_tz_env_empty_string_passes_through():
"""Empty string is never set as ``TZ`` by the caller, but the helper
is still defensive — return it unchanged rather than raising."""
assert _tz_env("") == ""
@pytest.mark.unit
def test_iana_to_posix_phoenix_and_honolulu_present():
"""Sanity-check the no-DST entries are still in the mapping; deleting
them would silently revert the Phoenix DST bug."""
assert _IANA_TO_POSIX_TZ["America/Phoenix"] == "MST7"
assert _IANA_TO_POSIX_TZ["Pacific/Honolulu"] == "HST10"
# ── InvisiblePlaywright._humanize_max_seconds ─────────────────────────
@pytest.mark.unit
def test_humanize_true_defaults_to_one_and_a_half_seconds():
ip = InvisiblePlaywright(seed=42, humanize=True)
assert ip._humanize_max_seconds() == 1.5
@pytest.mark.unit
def test_humanize_float_passes_through_as_seconds():
ip = InvisiblePlaywright(seed=42, humanize=2.5)
assert ip._humanize_max_seconds() == 2.5
@pytest.mark.unit
def test_humanize_int_coerced_to_float():
"""``humanize=3`` is valid (truthy, not ``True``) → float coercion."""
ip = InvisiblePlaywright(seed=42, humanize=3)
out = ip._humanize_max_seconds()
assert out == 3.0
assert isinstance(out, float)
@pytest.mark.unit
def test_humanize_small_float_passes_through():
"""Below the default cap — the user's value wins."""
ip = InvisiblePlaywright(seed=42, humanize=0.4)
assert ip._humanize_max_seconds() == 0.4
# ── InvisiblePlaywright._default_context_kwargs ───────────────────────
@pytest.mark.unit
def test_default_context_viewport_subtracts_window_chrome():
"""Viewport must fit inside the spoofed screen with the headed
window chrome subtracted. Otherwise Playwright complains about the
viewport being larger than the screen."""
ip = InvisiblePlaywright(seed=42)
kw = ip._default_context_kwargs()
p = ip._profile
assert kw["viewport"]["width"] == p.screen.width - _CHROME_W
assert kw["viewport"]["height"] == p.screen.height - _TASKBAR_H - _CHROME_H
@pytest.mark.unit
def test_default_context_screen_matches_profile():
ip = InvisiblePlaywright(seed=42)
kw = ip._default_context_kwargs()
p = ip._profile
assert kw["screen"] == {"width": p.screen.width, "height": p.screen.height}
assert kw["device_scale_factor"] == p.screen.dpr
@pytest.mark.unit
def test_default_context_color_scheme_follows_dark_theme():
"""``color_scheme`` must match ``profile.dark_theme`` so the Playwright
realm tells matchMedia the same thing the prefs tell the chrome."""
ip_dark = InvisiblePlaywright(seed=42, pin={"dark_theme": True})
ip_light = InvisiblePlaywright(seed=42, pin={"dark_theme": False})
assert ip_dark._default_context_kwargs()["color_scheme"] == "dark"
assert ip_light._default_context_kwargs()["color_scheme"] == "light"
@pytest.mark.unit
def test_default_context_includes_timezone_when_set():
ip = InvisiblePlaywright(seed=42, timezone="America/New_York")
assert ip._default_context_kwargs()["timezone_id"] == "America/New_York"
@pytest.mark.unit
def test_default_context_omits_timezone_when_empty():
"""Default ``timezone=""`` means "let the host TZ leak through"
Playwright must not receive ``timezone_id`` at all in that case,
otherwise it overrides to the literal empty string."""
ip = InvisiblePlaywright(seed=42)
assert "timezone_id" not in ip._default_context_kwargs()
@pytest.mark.unit
def test_default_context_includes_locale_when_set():
ip = InvisiblePlaywright(seed=42, locale="de-DE")
assert ip._default_context_kwargs()["locale"] == "de-DE"
@pytest.mark.unit
def test_default_context_omits_locale_when_empty():
ip = InvisiblePlaywright(seed=42, locale="")
assert "locale" not in ip._default_context_kwargs()
# ── InvisiblePlaywright._build_env — WebRTC egress auto-derive ─────────
# Locks the 2026-06-10 fix: behind a proxy the launcher feeds the discovered
# egress IP to nICEr (srflx override) + drops IPv6. Without it, a proxied
# session's WebRTC silently fell back to leaking/blocking. Runs in tests.yml.
@pytest.mark.unit
def test_build_env_injects_webrtc_egress_when_discovered():
ip = InvisiblePlaywright(seed=42)
ip._webrtc_egress_ip = "203.0.113.9" # what __enter__ resolves behind a proxy
env = ip._build_env()
assert env["STEALTHFOX_WEBRTC_PUBLIC_IP"] == "203.0.113.9"
assert env["STEALTHFOX_WEBRTC_DISABLE_IPV6"] == "1"
@pytest.mark.unit
def test_build_env_no_webrtc_keys_without_proxy(monkeypatch):
monkeypatch.delenv("STEALTHFOX_WEBRTC_PUBLIC_IP", raising=False)
ip = InvisiblePlaywright(seed=42)
ip._webrtc_egress_ip = None # no proxy → real STUN already truthful
env = ip._build_env()
assert "STEALTHFOX_WEBRTC_PUBLIC_IP" not in env
assert "STEALTHFOX_WEBRTC_DISABLE_IPV6" not in env
@pytest.mark.unit
def test_build_env_caller_env_override_wins(monkeypatch):
monkeypatch.setenv("STEALTHFOX_WEBRTC_PUBLIC_IP", "198.51.100.5")
ip = InvisiblePlaywright(seed=42)
ip._webrtc_egress_ip = "203.0.113.9" # auto-discovered
env = ip._build_env()
assert env["STEALTHFOX_WEBRTC_PUBLIC_IP"] == "198.51.100.5" # caller wins
assert env["STEALTHFOX_WEBRTC_DISABLE_IPV6"] == "1"