# retoor import gzip from pathlib import Path import pytest from devplacepy.services.nodes import geo from devplacepy.services.nodes.geo import GeoDatabase, GeoError, build, pack ASN_ROWS = [ ("1.0.0.0", "1.0.0.255", 13335, "Cloudflare, Inc."), ("8.8.8.0", "8.8.8.255", 15169, "Google LLC"), ("20.0.0.0", "20.0.0.255", 64500, "Home Fiber BV"), ("2001:db8::", "2001:db8::ffff", 64501, "Mobile Wireless NV"), ] COUNTRY_ROWS = [ ("1.0.0.0", "1.0.0.255", "AU"), ("8.8.8.0", "8.8.8.255", "US"), ("20.0.0.0", "20.0.0.255", "NL"), ("2001:db8::", "2001:db8::ffff", "ZZ"), ] def lines(rows): return [",".join(f'"{part}"' if "," in str(part) else str(part) for part in row) for row in rows] def make(tmp_path, monkeypatch, checks=()): monkeypatch.setattr(geo, "SPEC", geo.GeoSpec(min_asn_rows=1, min_country_rows=1, checks=checks)) target = tmp_path / "geo.db" built = build(lines(ASN_ROWS), lines(COUNTRY_ROWS), target, "2026-10") geo.install(built, target) return GeoDatabase(target) def test_pack_normalises_v4_mapped_and_rejects_garbage(): assert pack("::ffff:8.8.8.8") == pack("8.8.8.8") assert pack("[2001:db8::1]")[0] == 6 with pytest.raises(ValueError): pack("nope") def test_lookup_hits_first_last_and_middle_of_a_range(tmp_path, monkeypatch): database = make(tmp_path, monkeypatch) for address in ("1.0.0.0", "1.0.0.77", "1.0.0.255"): found = database.lookup(address) assert (found.asn, found.country, found.asn_class) == (13335, "AU", "datacenter") def test_lookup_between_and_outside_ranges_is_unknown(tmp_path, monkeypatch): database = make(tmp_path, monkeypatch) for address in ("1.0.1.0", "0.255.255.255", "9.9.9.9", "255.255.255.255"): assert database.lookup(address).asn_class == "unknown" def test_ipv6_and_zz_country(tmp_path, monkeypatch): database = make(tmp_path, monkeypatch) found = database.lookup("2001:db8::1234") assert found.asn == 64501 and found.country == "" and found.asn_class == "mobile" assert database.lookup("2001:db9::1").asn == 0 def test_classification_of_a_residential_range(tmp_path, monkeypatch): assert make(tmp_path, monkeypatch).lookup("20.0.0.9").asn_class == "residential" def test_missing_file_and_bad_input_degrade_to_unknown(tmp_path): database = GeoDatabase(tmp_path / "absent.db") assert not database.available() assert database.lookup("8.8.8.8").asn_class == "unknown" assert database.status()["available"] is False assert GeoDatabase(tmp_path / "absent.db").lookup("").asn_class == "unknown" def test_a_too_small_dataset_is_refused_and_leaves_no_file(tmp_path, monkeypatch): monkeypatch.setattr(geo, "SPEC", geo.GeoSpec(min_asn_rows=10, min_country_rows=10, checks=())) target = tmp_path / "geo.db" with pytest.raises(GeoError): build(lines(ASN_ROWS), lines(COUNTRY_ROWS), target, "2026-10") assert not target.exists() and not target.with_suffix(".tmp").exists() def test_a_failed_sanity_check_refuses_and_keeps_the_current_database(tmp_path, monkeypatch): current = make(tmp_path, monkeypatch) before = current.path.read_bytes() monkeypatch.setattr(geo, "SPEC", geo.GeoSpec(min_asn_rows=1, min_country_rows=1, checks=(("8.8.8.8", 99999),))) with pytest.raises(GeoError): build(lines(ASN_ROWS), lines(COUNTRY_ROWS), tmp_path / "geo.db", "2026-11") assert current.path.read_bytes() == before def test_the_row_cap_is_enforced(tmp_path, monkeypatch): monkeypatch.setattr(geo, "SPEC", geo.GeoSpec(max_rows=2, min_asn_rows=1, min_country_rows=1, checks=())) with pytest.raises(GeoError): build(lines(ASN_ROWS), lines(COUNTRY_ROWS), tmp_path / "geo.db", "2026-10") def test_malformed_rows_are_skipped_not_fatal(tmp_path, monkeypatch): monkeypatch.setattr(geo, "SPEC", geo.GeoSpec(min_asn_rows=1, min_country_rows=1, checks=())) asn = lines(ASN_ROWS) + ["garbage", "a,b,c,d", "1.2.3.4,1.2.3.5,notanumber,x"] built = build(asn, lines(COUNTRY_ROWS), tmp_path / "geo.db", "2026-10") assert built["asn_rows"] == len(ASN_ROWS) def test_status_reports_metadata_and_attribution(tmp_path, monkeypatch): status = make(tmp_path, monkeypatch).status() assert status["available"] and status["month"] == "2026-10" and "DB-IP" in status["attribution"] def test_months_walk_back_over_a_year_boundary(): from datetime import datetime, timezone assert geo.months(datetime(2026, 1, 15, tzinfo=timezone.utc)) == ["2026-01", "2025-12", "2025-11"] def test_build_from_files_reads_gzip(tmp_path, monkeypatch): monkeypatch.setattr(geo, "SPEC", geo.GeoSpec(min_asn_rows=1, min_country_rows=1, checks=())) asn_file, country_file = tmp_path / "a.csv.gz", tmp_path / "c.csv.gz" with gzip.open(asn_file, "wt") as handle: handle.write("\n".join(lines(ASN_ROWS))) with gzip.open(country_file, "wt") as handle: handle.write("\n".join(lines(COUNTRY_ROWS))) built = geo.build_from_files(asn_file, country_file, tmp_path / "geo.db", "2026-10") geo.install(built, tmp_path / "geo.db") assert GeoDatabase(tmp_path / "geo.db").lookup("8.8.8.8").asn == 15169