mirror of
https://github.com/daijro/camoufox.git
synced 2026-10-04 00:00:26 +00:00
feat(geoip): make GeoIP AIO the default source, deprecate GeoLite2 (#820)
The default GeoIP source was MaxMind GeoLite2 via sapics/ip-location-db, whose URLs kept serving the 2026-06-17 build after that project moved to GitHub Releases (found in #815). GeoIP AIO (daijro/geoip-all-in-one) resolves timezones more accurately on real proxy IPs and is rebuilt weekly. - repos.yml: AIO is the default; GeoLite2 is `deprecated: true`, with the Releases URLs from #815 so it still works when picked by name. - A cache holding a deprecated source it was not explicitly given (`camoufox set --geoip` or the GUI) moves to the default and drops the old database. An explicit choice is kept, with a FutureWarning. - needs_update() reads the database's build date instead of the file age: refresh once the build is over 8 days old, re-checking at most daily, and warn when a fresh download is over 30 days old (a frozen source). - get_geolocation(geoip_db=...) now reads that source's own database rather than the active one's, and no longer makes it the active one. - tests/test_geoip_sources.py (from #815) downloads every non-deprecated source and fails when its build is stale; tests.yml installs the geoip extra so it runs, and so gates every release. - TypeScript twin updated to match; goldens answer in both layouts. Co-authored-by: lp177 <57773165+lp177@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
lp177
Claude Opus 5.5
parent
0db845fac0
commit
f36390a19e
@@ -0,0 +1,98 @@
|
||||
"""Which GeoIP source is active, and when its database is refreshed."""
|
||||
|
||||
import os
|
||||
import time
|
||||
import warnings
|
||||
|
||||
import pytest
|
||||
from yaml import safe_dump
|
||||
|
||||
from camoufox import geolocation
|
||||
|
||||
DEFAULT = "GeoIP AIO by daijro"
|
||||
DEPRECATED = "MaxMind GeoLite2"
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def cache(tmp_path, monkeypatch):
|
||||
monkeypatch.setattr(geolocation, "GEOIP_DIR", tmp_path)
|
||||
monkeypatch.setattr(geolocation, "MMDB_DIR", tmp_path / "mmdb")
|
||||
monkeypatch.setattr(geolocation, "GEOIP_CONFIG", tmp_path / "config.yml")
|
||||
return tmp_path
|
||||
|
||||
|
||||
def _saved(cache, **fields):
|
||||
(cache / "config.yml").write_text(safe_dump(fields))
|
||||
|
||||
|
||||
def test_default_is_aio():
|
||||
assert geolocation.load_geoip_config()["name"] == DEFAULT
|
||||
repos, _ = geolocation._load_geoip_repos()
|
||||
assert [r["name"] for r in repos if r.get("deprecated")] == [DEPRECATED]
|
||||
|
||||
|
||||
def test_implicit_deprecated_source_moves_to_default(cache):
|
||||
# What every cache written before the default changed holds
|
||||
_saved(cache, name=DEPRECATED)
|
||||
assert geolocation.load_geoip_config()["name"] == DEFAULT
|
||||
|
||||
|
||||
def test_explicit_deprecated_source_is_kept(cache):
|
||||
_saved(cache, name=DEPRECATED, explicit=True)
|
||||
assert geolocation.load_geoip_config()["name"] == DEPRECATED
|
||||
|
||||
|
||||
def test_save_records_explicit_choice(cache):
|
||||
config = geolocation._get_geoip_config_by_name(DEPRECATED)
|
||||
geolocation.save_geoip_config(config, explicit=True)
|
||||
assert geolocation.load_geoip_config()["name"] == DEPRECATED
|
||||
geolocation.save_geoip_config(config)
|
||||
assert geolocation.load_geoip_config()["name"] == DEFAULT
|
||||
|
||||
|
||||
def test_refresh_keeps_an_explicit_choice(cache, monkeypatch):
|
||||
# `camoufox fetch` and the weekly refresh download without naming a source
|
||||
_saved(cache, name=DEPRECATED, explicit=True)
|
||||
monkeypatch.setattr(geolocation, "webdl", lambda url, buffer, **kw: buffer.write(b"x"))
|
||||
monkeypatch.setattr(geolocation, "_build_age_days", lambda path: 1)
|
||||
geolocation.download_mmdb()
|
||||
assert geolocation.load_geoip_config()["name"] == DEPRECATED
|
||||
|
||||
|
||||
def test_deprecated_source_warns():
|
||||
with warnings.catch_warnings(record=True) as caught:
|
||||
warnings.simplefilter("always")
|
||||
geolocation.warn_if_deprecated(geolocation._get_geoip_config_by_name(DEPRECATED))
|
||||
geolocation.warn_if_deprecated(geolocation._get_geoip_config_by_name(DEFAULT))
|
||||
assert [w.category for w in caught] == [FutureWarning]
|
||||
assert DEFAULT in str(caught[0].message)
|
||||
|
||||
|
||||
def _database(cache, checked_days_ago):
|
||||
config = geolocation.load_geoip_config()
|
||||
path = geolocation.get_mmdb_path("ipv4", config)
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_bytes(b"")
|
||||
stamp = time.time() - checked_days_ago * 86400
|
||||
os.utime(path, (stamp, stamp))
|
||||
return config
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"checked, built, stale",
|
||||
[
|
||||
(0.5, 400, False), # checked today: wait, even for an old build
|
||||
(2, 3, False), # this week's build
|
||||
(2, 9, True), # a release has been missed
|
||||
(40, 3, False), # an old file holding a new build is not stale
|
||||
(2, None, True), # unreadable database
|
||||
],
|
||||
)
|
||||
def test_needs_update_reads_the_build_date(cache, monkeypatch, checked, built, stale):
|
||||
config = _database(cache, checked)
|
||||
monkeypatch.setattr(geolocation, "_build_age_days", lambda path: built)
|
||||
assert geolocation.needs_update(config) is stale
|
||||
|
||||
|
||||
def test_missing_database_needs_update():
|
||||
assert geolocation.needs_update() is True
|
||||
@@ -0,0 +1,43 @@
|
||||
"""Every GeoIP source camoufox offers must be serving a current build.
|
||||
|
||||
The launcher always downloads a source's newest build, so a source that stops
|
||||
publishing leaves every user on stale data with no error: GeoLite2's URLs
|
||||
served the 2026-06-17 build for months after sapics/ip-location-db moved to
|
||||
GitHub Releases (found in daijro/camoufox#815, which this test comes from).
|
||||
This suite runs in tests.yml, which gates every release.
|
||||
"""
|
||||
|
||||
import time
|
||||
|
||||
import pytest
|
||||
|
||||
from camoufox import geolocation
|
||||
|
||||
maxminddb = pytest.importorskip("maxminddb")
|
||||
|
||||
MAX_AGE_DAYS = 30
|
||||
|
||||
SOURCES = [repo["name"] for repo in geolocation._load_geoip_repos()[0] if not repo.get("deprecated")]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("name", SOURCES)
|
||||
def test_source_serves_a_current_build(name, tmp_path, monkeypatch):
|
||||
monkeypatch.setattr(geolocation, "GEOIP_DIR", tmp_path)
|
||||
monkeypatch.setattr(geolocation, "MMDB_DIR", tmp_path / "mmdb")
|
||||
monkeypatch.setattr(geolocation, "GEOIP_CONFIG", tmp_path / "config.yml")
|
||||
|
||||
geolocation.download_mmdb(name)
|
||||
|
||||
databases = sorted((tmp_path / "mmdb").glob("*.mmdb"))
|
||||
assert databases, f"{name} downloaded no database"
|
||||
for database in databases:
|
||||
with maxminddb.open_database(str(database)) as reader:
|
||||
age_days = (time.time() - reader.metadata().build_epoch) / 86400
|
||||
record = reader.get("8.8.8.8")
|
||||
assert age_days < MAX_AGE_DAYS, (
|
||||
f"{name}: {database.name} was built {age_days:.0f} days ago, its source is no longer updated"
|
||||
)
|
||||
config = geolocation._get_geoip_config_by_name(name)
|
||||
assert geolocation._find_in(record, config["paths"]["iso_code"]) == "US", (
|
||||
f"{name}: the configured paths do not read this database"
|
||||
)
|
||||
Reference in New Issue
Block a user