feat: timezone="auto" resolves from any egress + weekly geoip auto-update
Refine timezone="auto" so it ALWAYS resolves (drop the "host" sentinel): - ""/"auto" resolve from the proxy egress when a proxy is set, else from the host own public IP (direct lookup); an explicit zone is the only opt-out. - on failure: with a proxy raise; without a proxy fall back to the host TZ. GeoIP DB now auto-updates against daijro/geoip-all-in-one weekly rebuild: cache the latest, re-check after GEOIP_REFRESH_DAYS (7), prune old tags, reuse a stale cache offline; GEOIP_MMDB_VERSION is only the cold fallback. tests: test_geo.py (37) + test_geoip_update.py; full unit suite 429 green plus 8 live combinations (proxy / no-proxy / explicit / failing / freshness).
This commit is contained in:
+56
-31
@@ -136,6 +136,20 @@ def test_discover_egress_ip_all_fail_raises(monkeypatch):
|
||||
discover_egress_ip(SOCKS)
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_discover_egress_ip_no_proxy_is_direct(monkeypatch):
|
||||
# proxy=None → direct request, requests.get must get proxies=None.
|
||||
seen = {}
|
||||
|
||||
def fake_get(url, **kw):
|
||||
seen["proxies"] = kw.get("proxies", "MISSING")
|
||||
return _FakeResp("192.0.2.55")
|
||||
|
||||
monkeypatch.setattr(_geo.requests, "get", fake_get)
|
||||
assert discover_egress_ip(None) == "192.0.2.55"
|
||||
assert seen["proxies"] is None
|
||||
|
||||
|
||||
# ──────────────────────────────────────────────────────────────────────
|
||||
# ip_to_timezone — mocked mmdb reader
|
||||
# ──────────────────────────────────────────────────────────────────────
|
||||
@@ -194,8 +208,9 @@ def stub_egress(monkeypatch):
|
||||
"""Make egress resolution deterministic + offline; record if it ran."""
|
||||
state = {"called": False}
|
||||
|
||||
def fake_discover(proxy, **kw):
|
||||
def fake_discover(proxy=None, **kw):
|
||||
state["called"] = True
|
||||
state["proxy_arg"] = proxy
|
||||
return "203.0.113.7"
|
||||
|
||||
monkeypatch.setattr(_geo, "discover_egress_ip", fake_discover)
|
||||
@@ -208,56 +223,66 @@ def stub_egress(monkeypatch):
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.parametrize("sentinel", ["host", "local", "HOST", "Local"])
|
||||
def test_resolve_host_sentinel_forces_host_tz(sentinel, stub_egress):
|
||||
# Even with a proxy set, "host"/"local" force the host TZ and never resolve.
|
||||
assert resolve_session_timezone(sentinel, SOCKS) == ""
|
||||
assert stub_egress["called"] is False
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_resolve_explicit_iana_wins_over_proxy(stub_egress):
|
||||
def test_resolve_explicit_iana_wins(stub_egress):
|
||||
# An explicit zone wins and never triggers resolution (proxy or not).
|
||||
assert resolve_session_timezone("Asia/Tokyo", SOCKS) == "Asia/Tokyo"
|
||||
assert stub_egress["called"] is False # no resolution when explicit
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_resolve_empty_no_proxy_is_host(stub_egress):
|
||||
assert resolve_session_timezone("", None) == ""
|
||||
assert resolve_session_timezone("Asia/Tokyo", None) == "Asia/Tokyo"
|
||||
assert stub_egress["called"] is False
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_resolve_auto_no_proxy_is_host(stub_egress):
|
||||
assert resolve_session_timezone("auto", None) == ""
|
||||
assert stub_egress["called"] is False
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_resolve_empty_with_proxy_defaults_to_auto(stub_egress):
|
||||
# NEW default: a proxy with no timezone auto-resolves from the egress.
|
||||
def test_resolve_empty_with_proxy_resolves_from_proxy(stub_egress):
|
||||
assert resolve_session_timezone("", SOCKS) == "America/New_York"
|
||||
assert stub_egress["called"] is True
|
||||
assert stub_egress["proxy_arg"] == SOCKS # routed through the proxy
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_resolve_auto_with_proxy_resolves(stub_egress):
|
||||
def test_resolve_auto_with_proxy_resolves_from_proxy(stub_egress):
|
||||
assert resolve_session_timezone("auto", HTTP) == "America/New_York"
|
||||
assert stub_egress["proxy_arg"] == HTTP
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_resolve_empty_no_proxy_resolves_from_host(stub_egress):
|
||||
# auto ALWAYS resolves — without a proxy, from the host's own public IP.
|
||||
assert resolve_session_timezone("", None) == "America/New_York"
|
||||
assert stub_egress["called"] is True
|
||||
assert stub_egress["proxy_arg"] is None # direct request, no proxy
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_resolve_direct_proxy_treated_as_no_proxy(stub_egress):
|
||||
assert resolve_session_timezone("auto", {"server": "direct://"}) == ""
|
||||
assert stub_egress["called"] is False
|
||||
def test_resolve_auto_no_proxy_resolves_from_host(stub_egress):
|
||||
assert resolve_session_timezone("auto", None) == "America/New_York"
|
||||
assert stub_egress["proxy_arg"] is None
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_resolve_fail_early_propagates(monkeypatch):
|
||||
# With a proxy set, a discovery failure must raise — never silent host TZ.
|
||||
def boom(proxy, **kw):
|
||||
def test_resolve_direct_proxy_resolves_via_host(stub_egress):
|
||||
# direct:// counts as "no proxy" → resolve from the host IP, don't skip.
|
||||
assert resolve_session_timezone("auto", {"server": "direct://"}) == "America/New_York"
|
||||
assert stub_egress["proxy_arg"] is None
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_resolve_no_proxy_failure_falls_back_to_host(monkeypatch):
|
||||
# Without a proxy, a lookup failure must NOT break the launch → host TZ ("").
|
||||
def boom(proxy=None, **kw):
|
||||
raise GeoTimezoneError("offline")
|
||||
|
||||
monkeypatch.setattr(_geo, "discover_egress_ip", boom)
|
||||
assert resolve_session_timezone("auto", None) == ""
|
||||
assert resolve_session_timezone("", None) == ""
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_resolve_proxy_failure_raises(monkeypatch):
|
||||
# With a proxy set, a failure must raise — never a silent host-TZ fallback.
|
||||
def boom(proxy=None, **kw):
|
||||
raise GeoTimezoneError("no egress")
|
||||
|
||||
monkeypatch.setattr(_geo, "discover_egress_ip", boom)
|
||||
with pytest.raises(GeoTimezoneError):
|
||||
resolve_session_timezone("auto", SOCKS)
|
||||
with pytest.raises(GeoTimezoneError):
|
||||
resolve_session_timezone("", SOCKS)
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
"""Unit tests for the intelligent geoip mmdb auto-update in `download.py`.
|
||||
|
||||
daijro/geoip-all-in-one rebuilds weekly; `ensure_geoip_mmdb` keeps the cache
|
||||
fresh without a download (or API call) on every launch. These tests mock the
|
||||
cache root, the latest-tag API, and the per-tag download so nothing touches the
|
||||
network.
|
||||
"""
|
||||
import os
|
||||
import time
|
||||
|
||||
import pytest
|
||||
|
||||
import invisible_playwright.download as dl
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def cache(tmp_path, monkeypatch):
|
||||
"""Point the cache at tmp_path and clear the env override."""
|
||||
monkeypatch.setattr(dl, "cache_root", lambda: tmp_path)
|
||||
monkeypatch.delenv("STEALTHFOX_GEOIP_MMDB", raising=False)
|
||||
return tmp_path
|
||||
|
||||
|
||||
def _make_cached(root, tag, name=dl.GEOIP_MMDB_NAME):
|
||||
d = root / "geoip" / tag
|
||||
d.mkdir(parents=True, exist_ok=True)
|
||||
f = d / name
|
||||
f.write_bytes(b"FAKE-MMDB")
|
||||
return f
|
||||
|
||||
|
||||
def _set_marker_age(root, days):
|
||||
m = root / "geoip" / ".last_check"
|
||||
m.parent.mkdir(parents=True, exist_ok=True)
|
||||
m.touch()
|
||||
old = time.time() - days * 86400
|
||||
os.utime(m, (old, old))
|
||||
|
||||
|
||||
# ──────────────────────────────────────────────────────────────────────
|
||||
# env override
|
||||
# ──────────────────────────────────────────────────────────────────────
|
||||
@pytest.mark.unit
|
||||
def test_env_override_returns_file(tmp_path, monkeypatch):
|
||||
f = tmp_path / "mine.mmdb"
|
||||
f.write_bytes(b"X")
|
||||
monkeypatch.setenv("STEALTHFOX_GEOIP_MMDB", str(f))
|
||||
assert dl.ensure_geoip_mmdb() == f
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_env_override_missing_raises(tmp_path, monkeypatch):
|
||||
monkeypatch.setenv("STEALTHFOX_GEOIP_MMDB", str(tmp_path / "nope.mmdb"))
|
||||
with pytest.raises(RuntimeError):
|
||||
dl.ensure_geoip_mmdb()
|
||||
|
||||
|
||||
# ──────────────────────────────────────────────────────────────────────
|
||||
# freshness window
|
||||
# ──────────────────────────────────────────────────────────────────────
|
||||
@pytest.mark.unit
|
||||
def test_fresh_cache_no_network(cache, monkeypatch):
|
||||
f = _make_cached(cache, "2026.06.03")
|
||||
_set_marker_age(cache, 0) # just checked
|
||||
|
||||
def boom():
|
||||
raise AssertionError("latest-tag API must NOT be called within the window")
|
||||
|
||||
monkeypatch.setattr(dl, "_latest_geoip_tag", boom)
|
||||
assert dl.ensure_geoip_mmdb(max_age_days=7) == f
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_stale_same_tag_no_download(cache, monkeypatch):
|
||||
f = _make_cached(cache, "2026.06.03")
|
||||
_set_marker_age(cache, 30) # stale → will re-check
|
||||
monkeypatch.setattr(dl, "_latest_geoip_tag", lambda: "2026.06.03")
|
||||
# real _download_geoip_tag runs but target exists, so no actual download:
|
||||
monkeypatch.setattr(dl, "_download_file", lambda *a, **k: (_ for _ in ()).throw(
|
||||
AssertionError("must not download when tag already cached")))
|
||||
assert dl.ensure_geoip_mmdb(max_age_days=7) == f
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_stale_new_tag_downloads_and_prunes(cache, monkeypatch):
|
||||
old = _make_cached(cache, "2026.06.03")
|
||||
_set_marker_age(cache, 30)
|
||||
monkeypatch.setattr(dl, "_latest_geoip_tag", lambda: "2026.06.10")
|
||||
|
||||
def fake_download(tag):
|
||||
return _make_cached(cache, tag) # simulate fetch+extract of the new tag
|
||||
|
||||
monkeypatch.setattr(dl, "_download_geoip_tag", fake_download)
|
||||
got = dl.ensure_geoip_mmdb(max_age_days=7)
|
||||
assert got.parent.name == "2026.06.10"
|
||||
assert not old.parent.exists() # old tag pruned
|
||||
assert got.exists()
|
||||
|
||||
|
||||
# ──────────────────────────────────────────────────────────────────────
|
||||
# offline resilience
|
||||
# ──────────────────────────────────────────────────────────────────────
|
||||
@pytest.mark.unit
|
||||
def test_api_down_with_cache_uses_cache(cache, monkeypatch):
|
||||
f = _make_cached(cache, "2026.06.03")
|
||||
_set_marker_age(cache, 30)
|
||||
|
||||
def boom():
|
||||
raise OSError("offline")
|
||||
|
||||
monkeypatch.setattr(dl, "_latest_geoip_tag", boom)
|
||||
assert dl.ensure_geoip_mmdb(max_age_days=7) == f # stale cache reused, no raise
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_cold_cache_api_down_falls_back_to_pinned(cache, monkeypatch):
|
||||
# no cache at all + API unreachable → pinned GEOIP_MMDB_VERSION fallback.
|
||||
def boom():
|
||||
raise OSError("offline")
|
||||
|
||||
monkeypatch.setattr(dl, "_latest_geoip_tag", boom)
|
||||
captured = {}
|
||||
|
||||
def fake_download(tag):
|
||||
captured["tag"] = tag
|
||||
return _make_cached(cache, tag)
|
||||
|
||||
monkeypatch.setattr(dl, "_download_geoip_tag", fake_download)
|
||||
got = dl.ensure_geoip_mmdb(max_age_days=7)
|
||||
assert captured["tag"] == dl.GEOIP_MMDB_VERSION
|
||||
assert got.exists()
|
||||
Reference in New Issue
Block a user