package main import ( "archive/zip" "fmt" "os" "path/filepath" "testing" "time" ) func writeZipFiles(t *testing.T, files map[string]string) string { t.Helper() path := filepath.Join(t.TempDir(), "fixture.zip") output, err := os.Create(path) if err != nil { t.Fatal(err) } archive := zip.NewWriter(output) for name, contents := range files { entry, err := archive.Create(name) if err != nil { t.Fatal(err) } if _, err := fmt.Fprint(entry, contents); err != nil { t.Fatal(err) } } if err := archive.Close(); err != nil { t.Fatal(err) } if err := output.Close(); err != nil { t.Fatal(err) } return path } func mustAddress(t *testing.T, value string) uint32 { t.Helper() address, ok := rangeFromPrefix(value + "/32") if !ok { t.Fatalf("invalid test address %q", value) } return address.start } func rangeContains(ranges []addressRange, address uint32) bool { for _, candidate := range ranges { if candidate.start <= address && candidate.end >= address { return true } } return false } func rangesEqual(left, right []addressRange) bool { left = mergeRanges(left) right = mergeRanges(right) if len(left) != len(right) { return false } for index := range left { if left[index] != right[index] { return false } } return true } func TestGeographySetsSeparateVPNExceptionCountries(t *testing.T) { path := writeZipFiles(t, map[string]string{ "ip-to-country.csv": "network,country_code,continent_code\n192.0.2.0/28,JP,AS\n192.0.2.16/28,KR,AS\n198.51.100.0/24,US,NA\n203.0.113.0/24,BR,SA\n", }) sets, err := ipLocateGeography(path) if err != nil { t.Fatal(err) } if !rangeContains(sets.admission, mustAddress(t, "198.51.100.1")) || rangeContains(sets.admission, mustAddress(t, "203.0.113.1")) { t.Fatalf("unexpected admission ranges: %#v", sets.admission) } if !rangeContains(sets.vpnException, mustAddress(t, "192.0.2.1")) || !rangeContains(sets.vpnException, mustAddress(t, "192.0.2.17")) || rangeContains(sets.vpnException, mustAddress(t, "198.51.100.1")) { t.Fatalf("unexpected VPN exception ranges: %#v", sets.vpnException) } } func TestMaxMindGeographySetsSeparateVPNExceptionCountries(t *testing.T) { path := writeZipFiles(t, map[string]string{ "GeoLite2-Country-Locations-en.csv": "geoname_id,locale_code,continent_code,country_iso_code\n1,en,AS,JP\n2,en,AS,KR\n3,en,NA,US\n4,en,SA,BR\n", "GeoLite2-Country-Blocks-IPv4.csv": "network,geoname_id,registered_country_geoname_id\n192.0.2.0/28,1,1\n192.0.2.16/28,2,2\n198.51.100.0/24,3,3\n203.0.113.0/24,4,4\n", }) sets, err := maxMindGeography(path) if err != nil { t.Fatal(err) } if !rangeContains(sets.admission, mustAddress(t, "198.51.100.1")) || rangeContains(sets.admission, mustAddress(t, "203.0.113.1")) { t.Fatalf("unexpected admission ranges: %#v", sets.admission) } if !rangeContains(sets.vpnException, mustAddress(t, "192.0.2.1")) || !rangeContains(sets.vpnException, mustAddress(t, "192.0.2.17")) || rangeContains(sets.vpnException, mustAddress(t, "198.51.100.1")) { t.Fatalf("unexpected VPN exception ranges: %#v", sets.vpnException) } } func TestIPLocateASNRangesSelectConfiguredOperators(t *testing.T) { path := writeZipFiles(t, map[string]string{ "ip-to-asn.csv": "network,asn,country_code,name,org,domain\n192.0.2.0/28,147049,AU,selected,selected,example.test\n192.0.2.16/28,64500,JP,other,other,example.test\n198.51.100.0/24,AS147049,AU,selected,selected,example.test\n", }) ranges, err := ipLocateASNRanges(path, map[uint32]struct{}{147049: {}}) if err != nil { t.Fatal(err) } if !rangeContains(ranges, mustAddress(t, "192.0.2.1")) || !rangeContains(ranges, mustAddress(t, "198.51.100.1")) || rangeContains(ranges, mustAddress(t, "192.0.2.17")) { t.Fatalf("unexpected ASN ranges: %#v", ranges) } } func TestVPNExceptionCompositionPreservesConsensusAndDenies(t *testing.T) { consensus := []addressRange{{start: 10, end: 20}} vpnASN := []addressRange{{start: 15, end: 30}} vpnGeography := []addressRange{{start: 18, end: 28}} denied := []addressRange{{start: 24, end: 25}} candidate := subtractRanges(intersectRanges(vpnASN, vpnGeography), consensus) exceptions := subtractRanges(candidate, denied) want := []addressRange{{start: 21, end: 23}, {start: 26, end: 28}} if !rangesEqual(exceptions, want) { t.Fatalf("exceptions=%#v want=%#v", exceptions, want) } } func TestParseASN(t *testing.T) { for _, value := range []string{"147049", "AS147049", "as147049"} { asn, err := parseASN(value) if err != nil || asn != 147049 { t.Fatalf("parseASN(%q)=%d,%v", value, asn, err) } } for _, value := range []string{"", "AS", "0", "-1", "4294967296"} { if _, err := parseASN(value); err == nil { t.Fatalf("parseASN(%q) succeeded", value) } } } func TestLoadIP2ProxyClassificationsOmitsStaleCache(t *testing.T) { now := time.Unix(2000000000, 0) path := filepath.Join(t.TempDir(), "ip2proxy.zip") if err := os.WriteFile(path, []byte("invalid archive"), 0o600); err != nil { t.Fatal(err) } modified := now.Add(-36 * 24 * time.Hour) if err := os.Chtimes(path, modified, modified); err != nil { t.Fatal(err) } result, state, age, err := loadIP2ProxyClassifications(path, now, 35*24*time.Hour) if err != nil { t.Fatal(err) } if state != "stale" || age != 36*24*time.Hour || len(result.all) != 0 { t.Fatalf("state=%s age=%s ranges=%d", state, age, len(result.all)) } } func TestLoadIP2ProxyClassificationsRequiresValidFreshCache(t *testing.T) { now := time.Unix(2000000000, 0) path := filepath.Join(t.TempDir(), "ip2proxy.zip") if err := os.WriteFile(path, []byte("invalid archive"), 0o600); err != nil { t.Fatal(err) } if err := os.Chtimes(path, now, now); err != nil { t.Fatal(err) } if _, _, _, err := loadIP2ProxyClassifications(path, now, 35*24*time.Hour); err == nil { t.Fatal("expected fresh invalid cache to fail") } } func TestLoadIP2ProxyClassificationsAllowsAbsentCache(t *testing.T) { result, state, age, err := loadIP2ProxyClassifications(filepath.Join(t.TempDir(), "missing.zip"), time.Now(), 35*24*time.Hour) if err != nil { t.Fatal(err) } if state != "absent" || age != 0 || len(result.all) != 0 { t.Fatalf("state=%s age=%s ranges=%d", state, age, len(result.all)) } }