package geoip import ( "archive/tar" "bytes" "compress/gzip" "errors" "io" "testing" "time" ) func gz(t *testing.T, b []byte) []byte { var buf bytes.Buffer w := gzip.NewWriter(&buf) w.Write(b) w.Close() return buf.Bytes() } func targz(t *testing.T, members map[string][]byte, order []string) []byte { var buf bytes.Buffer zw := gzip.NewWriter(&buf) tw := tar.NewWriter(zw) for _, name := range order { body := members[name] tw.WriteHeader(&tar.Header{Name: name, Mode: 0o644, Size: int64(len(body)), Typeflag: tar.TypeReg}) tw.Write(body) } tw.Close() zw.Close() return buf.Bytes() } func TestCappedReader(t *testing.T) { if b, err := io.ReadAll(CappedReader(bytes.NewReader(make([]byte, 100)), 100)); err != nil || len(b) != 100 { t.Fatalf("exactly max = %d %v", len(b), err) } if _, err := io.ReadAll(CappedReader(bytes.NewReader(make([]byte, 101)), 100)); !errors.Is(err, ErrTooLarge) { t.Fatalf("max+1 = %v", err) } } func TestGunzipStopsAtTheCap(t *testing.T) { r, err := GunzipReader(bytes.NewReader(gz(t, make([]byte, 10<<20))), 1<<20) if err != nil { t.Fatal(err) } n, err := io.Copy(io.Discard, r) if !errors.Is(err, ErrTooLarge) || n > 1<<20+1 { t.Fatalf("bomb read %d bytes, err %v", n, err) } } func TestTarGzMMDBCountsSkippedMembers(t *testing.T) { arc := targz(t, map[string][]byte{"GeoLite2-City_20261006/README.txt": make([]byte, 2<<20), "GeoLite2-City_20261006/GeoLite2-City.mmdb": []byte("the database")}, []string{"GeoLite2-City_20261006/README.txt", "GeoLite2-City_20261006/GeoLite2-City.mmdb"}) r, err := TarGzMMDB(bytes.NewReader(arc), 4<<20) if err != nil { t.Fatal(err) } if b, _ := io.ReadAll(r); string(b) != "the database" { t.Fatalf("member = %q", b) } if _, err := TarGzMMDB(bytes.NewReader(arc), 1<<20); !errors.Is(err, ErrTooLarge) { t.Fatalf("padding past the cap = %v, want ErrTooLarge", err) } none := targz(t, map[string][]byte{"x.txt": []byte("x")}, []string{"x.txt"}) if _, err := TarGzMMDB(bytes.NewReader(none), 1<<20); err == nil { t.Fatal("an archive with no .mmdb was accepted") } } func TestFeedSpecsAndURLs(t *testing.T) { s, ok := ParseFeed("dbip-city-lite") if !ok || s.Type != "DBIP-City-Lite" || s.Provider != "dbip" { t.Fatalf("spec = %+v %v", s, ok) } if got := DBIPURL("", s, time.Date(2026, 10, 6, 0, 0, 0, 0, time.UTC)); got != "https://download.db-ip.com/free/dbip-city-lite-2026-10.mmdb.gz" { t.Fatalf("url = %s", got) } c, _ := ParseFeed("dbip-country-lite") if got := DBIPURL("", c, time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC)); got != "https://download.db-ip.com/free/dbip-country-lite-2026-01.mmdb.gz" { t.Fatalf("country url = %s", got) } g, _ := ParseFeed("geolite2-city") if got := MaxMindURL("", g, "tar.gz.sha256"); got != "https://download.maxmind.com/geoip/databases/GeoLite2-City/download?suffix=tar.gz.sha256" { t.Fatalf("maxmind url = %s", got) } if _, ok := ParseFeed("ipinfo-lite"); ok { t.Fatal("unknown feed accepted") } } func TestFeedStatusFailing(t *testing.T) { s := FeedStatus{V: 1, ConsecutiveFailures: 6} if s.Failing() { t.Fatal("6 is not failing") } s.ConsecutiveFailures = 7 if !s.Failing() { t.Fatal("7 is failing") } if !(FeedStatus{ConsecutiveNotFound: 7}).Failing() { t.Fatal("7 not-found is failing") } raw := []byte(`{"v":1,"feed":"dbip-city-lite","box":"b","consecutive_failures":2}`) if got, err := ParseFeedStatus(raw); err != nil || got.Box != "b" { t.Fatalf("parse = %+v %v", got, err) } } // The paid MaxMind upgrade (Paul, 2026-10-07): same API, no D1 expiry. func TestPaidMaxMindFeeds(t *testing.T) { for name, typ := range map[string]string{"geoip2-city": "GeoIP2-City", "geoip2-country": "GeoIP2-Country"} { s, ok := ParseFeed(name) if !ok || s.Type != typ || s.Provider != "maxmind" { t.Fatalf("%s = %+v %v", name, s, ok) } if got := MaxMindURL("", s, "tar.gz"); got != "https://download.maxmind.com/geoip/databases/"+typ+"/download?suffix=tar.gz" { t.Fatalf("%s url = %s", name, got) } if Expired(typ, time.Unix(0, 0), time.Now()) { t.Fatalf("%s must never expire under D1", typ) } if _, ok := AttributionFor(typ); ok { t.Fatalf("%s has no platform-known attribution", typ) } } }