|
| 1 | +"""Regression test for the UA-cluster ↔ per-IP dedup fix. |
| 2 | +
|
| 3 | +An IP blocked by the per-IP pass must not also be written to the UA-cluster |
| 4 | +block file: both files are included in the same nginx geo block, and a CIDR |
| 5 | +present in two included files makes nginx log a "duplicate network" warning |
| 6 | +on every reload. |
| 7 | +
|
| 8 | +The UA-cluster pass must: |
| 9 | + 1. skip cluster member IPs already owned by the per-IP pass, and |
| 10 | + 2. drop stale entries already in its own file that the per-IP pass now owns |
| 11 | + (cleans duplicates accumulated before the fix). |
| 12 | +""" |
| 13 | +import textwrap |
| 14 | + |
| 15 | + |
| 16 | +def _make_cfg(tmp_path, ua_file, ips_file): |
| 17 | + return { |
| 18 | + "blocked_ua_cluster_conf": str(ua_file), |
| 19 | + "blocked_ips_conf": str(ips_file), |
| 20 | + "asn_db": str(tmp_path / "nonexistent-asn.tsv.gz"), |
| 21 | + "ua_cluster_threshold": 7, |
| 22 | + "ua_cluster_min_ips": 30, |
| 23 | + } |
| 24 | + |
| 25 | + |
| 26 | +def test_ua_cluster_skips_and_drops_per_ip_owned_ips(autoblock_mod, tmp_path, monkeypatch): |
| 27 | + ua_file = tmp_path / "blocked-ua-clusters.conf" |
| 28 | + ips_file = tmp_path / "blocked-ips.conf" |
| 29 | + |
| 30 | + # per-IP pass already owns 1.1.1.1 and 9.9.9.9 |
| 31 | + ips_file.write_text(textwrap.dedent(f"""\ |
| 32 | + {autoblock_mod.AUTO_BEGIN_MARKER} |
| 33 | + 1.1.1.1 1; # auto added=2026-05-19T00:00:00Z expires=2026-05-26T00:00:00Z reason=per-ip |
| 34 | + 9.9.9.9 1; # auto added=2026-05-19T00:00:00Z expires=2026-05-26T00:00:00Z reason=per-ip |
| 35 | + {autoblock_mod.AUTO_END_MARKER} |
| 36 | + """)) |
| 37 | + |
| 38 | + # UA-cluster file already has a STALE duplicate (1.1.1.1, now owned by |
| 39 | + # per-IP) plus a legitimate own entry (2.2.2.2) |
| 40 | + ua_file.write_text(textwrap.dedent(f"""\ |
| 41 | + {autoblock_mod.AUTO_BEGIN_MARKER} |
| 42 | + 1.1.1.1 1; # auto added=2026-05-18T00:00:00Z expires=2026-05-25T00:00:00Z reason=ua-cluster |
| 43 | + 2.2.2.2 1; # auto added=2026-05-18T00:00:00Z expires=2026-05-25T00:00:00Z reason=ua-cluster |
| 44 | + {autoblock_mod.AUTO_END_MARKER} |
| 45 | + """)) |
| 46 | + |
| 47 | + # One cluster whose members overlap per-IP (1.1.1.1, 9.9.9.9) and add new |
| 48 | + # IPs (2.2.2.2 extend, 3.3.3.3 new) |
| 49 | + cluster = { |
| 50 | + "ua": "Mozilla/5.0 Firefox/133.0", "score": 12, "ip_count": 200, |
| 51 | + "hosting_ratio": 0.95, "reasons": "noassets,noref,host95%", |
| 52 | + "ips": ["1.1.1.1", "9.9.9.9", "2.2.2.2", "3.3.3.3"], |
| 53 | + "added": "2026-05-19T10:00:00Z", "expires": "2026-05-26T10:00:00Z", |
| 54 | + } |
| 55 | + |
| 56 | + monkeypatch.setattr(autoblock_mod, "find_blockable_ua_clusters", |
| 57 | + lambda *a, **k: [cluster]) |
| 58 | + monkeypatch.setattr(autoblock_mod, "nginx_reload", lambda: True) |
| 59 | + |
| 60 | + cfg = _make_cfg(tmp_path, ua_file, ips_file) |
| 61 | + autoblock_mod.cmd_ua_cluster(cfg, dry_run=False) |
| 62 | + |
| 63 | + manual, auto = autoblock_mod.read_blocked(str(ua_file)) |
| 64 | + cidrs = {e["cidr"] for e in auto} |
| 65 | + |
| 66 | + # per-IP-owned IPs must NOT be in the UA-cluster file |
| 67 | + assert "1.1.1.1" not in cidrs, "stale per-IP-owned dup must be dropped" |
| 68 | + assert "9.9.9.9" not in cidrs, "per-IP-owned IP must be skipped, not added" |
| 69 | + # UA-cluster's own IPs stay |
| 70 | + assert "2.2.2.2" in cidrs, "UA-cluster's own IP must be kept" |
| 71 | + assert "3.3.3.3" in cidrs, "new non-overlapping cluster IP must be added" |
| 72 | + |
| 73 | + |
| 74 | +def test_ua_cluster_no_per_ip_file_is_safe(autoblock_mod, tmp_path, monkeypatch): |
| 75 | + """When blocked_ips_conf does not exist, dedup is a no-op — all cluster |
| 76 | + IPs are written normally.""" |
| 77 | + ua_file = tmp_path / "blocked-ua-clusters.conf" |
| 78 | + ips_file = tmp_path / "does-not-exist.conf" |
| 79 | + |
| 80 | + cluster = { |
| 81 | + "ua": "Mozilla/5.0 Firefox/133.0", "score": 12, "ip_count": 200, |
| 82 | + "hosting_ratio": 0.95, "reasons": "noassets,noref", |
| 83 | + "ips": ["4.4.4.4", "5.5.5.5"], |
| 84 | + "added": "2026-05-19T10:00:00Z", "expires": "2026-05-26T10:00:00Z", |
| 85 | + } |
| 86 | + monkeypatch.setattr(autoblock_mod, "find_blockable_ua_clusters", |
| 87 | + lambda *a, **k: [cluster]) |
| 88 | + monkeypatch.setattr(autoblock_mod, "nginx_reload", lambda: True) |
| 89 | + |
| 90 | + cfg = _make_cfg(tmp_path, ua_file, ips_file) |
| 91 | + autoblock_mod.cmd_ua_cluster(cfg, dry_run=False) |
| 92 | + |
| 93 | + _, auto = autoblock_mod.read_blocked(str(ua_file)) |
| 94 | + cidrs = {e["cidr"] for e in auto} |
| 95 | + assert cidrs == {"4.4.4.4", "5.5.5.5"} |
| 96 | + |
| 97 | + |
| 98 | +def test_ua_cluster_same_ip_two_clusters_not_duplicated(autoblock_mod, tmp_path, monkeypatch): |
| 99 | + """An IP appearing in two clusters in one run must be written once, not |
| 100 | + twice (the auto_by_cidr index must be updated as entries are appended).""" |
| 101 | + ua_file = tmp_path / "blocked-ua-clusters.conf" |
| 102 | + ips_file = tmp_path / "no-per-ip.conf" |
| 103 | + |
| 104 | + clusters = [ |
| 105 | + {"ua": "UA-A", "score": 10, "ip_count": 50, "hosting_ratio": 0.9, |
| 106 | + "reasons": "x", "ips": ["7.7.7.7"], |
| 107 | + "added": "2026-05-19T10:00:00Z", "expires": "2026-05-26T10:00:00Z"}, |
| 108 | + {"ua": "UA-B", "score": 10, "ip_count": 50, "hosting_ratio": 0.9, |
| 109 | + "reasons": "x", "ips": ["7.7.7.7"]}, # same IP, second cluster |
| 110 | + ] |
| 111 | + # second cluster reuses added/expires from first via .get fallback in code; |
| 112 | + # provide them to be safe |
| 113 | + clusters[1]["added"] = "2026-05-19T10:00:00Z" |
| 114 | + clusters[1]["expires"] = "2026-05-26T10:00:00Z" |
| 115 | + |
| 116 | + monkeypatch.setattr(autoblock_mod, "find_blockable_ua_clusters", |
| 117 | + lambda *a, **k: clusters) |
| 118 | + monkeypatch.setattr(autoblock_mod, "nginx_reload", lambda: True) |
| 119 | + |
| 120 | + cfg = _make_cfg(tmp_path, ua_file, ips_file) |
| 121 | + autoblock_mod.cmd_ua_cluster(cfg, dry_run=False) |
| 122 | + |
| 123 | + _, auto = autoblock_mod.read_blocked(str(ua_file)) |
| 124 | + cidrs = [e["cidr"] for e in auto] |
| 125 | + assert cidrs.count("7.7.7.7") == 1, "IP in two clusters must appear once" |
0 commit comments