package geo import ( "bytes" "errors" "log" "math" "net" "os" "path/filepath" "strings" "testing" "time" ) func TestOpenMMDBRefusesNonDatabases(t *testing.T) { dir := t.TempDir() missing := filepath.Join(dir, "missing.mmdb") junk := filepath.Join(dir, "junk.mmdb") if err := os.WriteFile(junk, []byte("not a maxmind database"), 0o600); err != nil { t.Fatal(err) } city, err := os.ReadFile("testdata/GeoIP2-City-Test.mmdb") if err != nil { t.Fatal(err) } truncated := filepath.Join(dir, "truncated.mmdb") if err := os.WriteFile(truncated, city[:len(city)/3], 0o600); err != nil { t.Fatal(err) } for _, p := range []string{missing, junk, truncated} { if m, err := OpenMMDB(p); err == nil { m.Close() t.Errorf("OpenMMDB(%s) succeeded, want an error", filepath.Base(p)) } } } func TestMMDBCityRecordGivesItsLocation(t *testing.T) { m, err := OpenMMDB("testdata/GeoIP2-City-Test.mmdb") if err != nil { t.Fatal(err) } defer m.Close() p, ok := m.Locate(net.ParseIP("81.2.69.142")) if !ok || p.Lat < 51 || p.Lat > 52 { t.Fatalf("81.2.69.142 = (%v, %v), want a London point", p, ok) } // Review Focus 2: an IPv4-mapped IPv6 address is the same client. if mapped, ok := m.Locate(net.ParseIP("::ffff:81.2.69.142")); !ok || mapped != p { t.Fatalf("::ffff:81.2.69.142 = (%v, %v), want %v", mapped, ok, p) } } func TestMMDBCountryOnlyRecordGivesTheCountryPoint(t *testing.T) { m, err := OpenMMDB("testdata/GeoIP2-Country-Test.mmdb") if err != nil { t.Fatal(err) } defer m.Close() p, ok := m.Locate(net.ParseIP("81.2.69.142")) gb, _ := CountryPoint("GB") if !ok || p != gb { t.Fatalf("country-only 81.2.69.142 = (%v, %v), want GB's point %v", p, ok, gb) } } // Review Focus 2 and spec ยง6.5: a public IPv6 address looked up in an // IPv4-only database is a miss, and the reader's error โ€” which contains // the address โ€” never reaches the log. func TestMMDBLookupErrorsNeverLogTheAddress(t *testing.T) { m, err := OpenMMDB("testdata/MaxMind-DB-test-ipv4-24.mmdb") if err != nil { t.Fatal(err) } defer m.Close() var buf bytes.Buffer prev := log.Writer() log.SetOutput(&buf) defer log.SetOutput(prev) lookupErrLast = lookupErrLast.AddDate(-1, 0, 0) // un-throttle if _, ok := m.Locate(net.ParseIP("2001:4860::1")); ok { t.Fatal("an IPv6 address located in an IPv4-only database") } if bytes.Contains(buf.Bytes(), []byte("2001:4860")) { t.Fatalf("the address leaked into the log: %s", buf.String()) } if buf.Len() == 0 { t.Fatal("a lookup failure must be logged (as a fixed category)") } } // Final review: a structurally valid database can still carry a malformed // location. Nonfinite values and coordinates off the globe are a miss // (ok=false), never a point that ranks regions from nowhere, and never a // fall-through to the record's country either: a record that lies about // one field is not trusted on the next. func TestLocateRejectsMalformedCoordinates(t *testing.T) { at := func(lat, lon float64) *MMDB { return &MMDB{lookup: func(_ net.IP, result any) error { rec := result.(*mmdbRecord) rec.Location.Latitude, rec.Location.Longitude = &lat, &lon rec.Country.ISOCode = "GB" return nil }} } bad := map[string][2]float64{ "NaN latitude": {math.NaN(), 0}, "NaN longitude": {0, math.NaN()}, "+Inf latitude": {math.Inf(1), 0}, "-Inf longitude": {0, math.Inf(-1)}, "latitude above 90": {90.0001, 0}, "latitude below -90": {-91, 0}, "longitude above 180": {0, 180.5}, "longitude below -180": {0, -181}, } for name, c := range bad { if p, ok := at(c[0], c[1]).Locate(net.ParseIP("81.2.69.142")); ok { t.Errorf("%s: located %v, want a miss", name, p) } } // The edges of the globe are on it. for name, c := range map[string][2]float64{ "north pole": {90, 0}, "south pole": {-90, 0}, "antimeridian east": {0, 180}, "antimeridian west": {0, -180}, } { if p, ok := at(c[0], c[1]).Locate(net.ParseIP("81.2.69.142")); !ok || p != (Point{c[0], c[1]}) { t.Errorf("%s: (%v, %v), want %v", name, p, ok, Point{c[0], c[1]}) } } } // Review Focus 2: non-public addresses never reach the reader โ€” proved // through Locate itself, with a counting lookup in place of the reader. func TestLocateNeverLooksUpNonPublicAddresses(t *testing.T) { calls := 0 m := &MMDB{lookup: func(net.IP, any) error { calls++; return errors.New("unused") }} for _, s := range []string{ "127.0.0.1", "::1", "10.1.2.3", "192.168.0.1", "172.16.0.1", "100.64.0.1", "169.254.1.1", "fe80::1", "fd00::1", "0.0.0.0", "::", } { if _, ok := m.Locate(net.ParseIP(s)); ok { t.Errorf("Locate(%s) answered", s) } } if calls != 0 { t.Fatalf("the reader was asked %d time(s) about non-public addresses", calls) } m.Locate(net.ParseIP("81.2.69.142")) if calls != 1 { t.Fatalf("a public address must reach the reader, calls = %d", calls) } var nilM *MMDB if _, ok := nilM.Locate(net.ParseIP("81.2.69.142")); ok { t.Error("a nil *MMDB located an address") } } func TestLookupCityGivesCountrySubdivisionAndPoint(t *testing.T) { m, err := OpenMMDB("testdata/GeoIP2-City-Test.mmdb") if err != nil { t.Fatal(err) } defer m.Close() cases := map[string]Result{ "81.2.69.142": {Country: "GB", Subdivision: "GB-ENG", HasPoint: true}, "216.160.83.56": {Country: "US", Subdivision: "US-WA", HasPoint: true}, "89.160.20.112": {Country: "SE", Subdivision: "SE-E", HasPoint: true}, "2001:218::1": {Country: "JP", HasPoint: true}, // no subdivision in the fixture } for ip, want := range cases { got, ok := m.Lookup(net.ParseIP(ip)) if !ok || got.Country != want.Country || got.Subdivision != want.Subdivision || got.HasPoint != want.HasPoint { t.Errorf("Lookup(%s) = (%+v, %v), want %+v", ip, got, ok, want) } } if got, _ := m.Lookup(net.ParseIP("81.2.69.142")); got.Point.Lat < 51 || got.Point.Lat > 52 { t.Errorf("London point = %v", got.Point) } if m.Source() != "GeoIP2-City" { t.Errorf("Source = %q", m.Source()) } if m.NodeCount() != 1547 { t.Errorf("NodeCount = %d, want 1547 (FACTS.md)", m.NodeCount()) } if m.BuildEpoch().Year() < 2020 { t.Errorf("BuildEpoch = %v", m.BuildEpoch()) } } func TestLookupCountryDatabaseHasNoPoint(t *testing.T) { m, err := OpenMMDB("testdata/GeoIP2-Country-Test.mmdb") if err != nil { t.Fatal(err) } defer m.Close() got, ok := m.Lookup(net.ParseIP("81.2.69.142")) if !ok || got.Country != "GB" || got.HasPoint || got.Subdivision != "" { t.Fatalf("country-only = (%+v, %v), want GB with no point (never a centroid)", got, ok) } } func TestLookupNoCountryDatabaseMisses(t *testing.T) { m, err := OpenMMDB("testdata/MaxMind-DB-test-ipv4-24.mmdb") if err != nil { t.Fatal(err) } defer m.Close() if got, ok := m.Lookup(net.ParseIP("81.2.69.142")); ok { t.Fatalf("a database with no country or location answered %+v", got) } } func TestLookupFallsBackToRegisteredCountryButNotItsSubdivision(t *testing.T) { m := &MMDB{lookup: func(_ net.IP, result any) error { rec := result.(*mmdbRecord) rec.RegisteredCountry.ISOCode = "IE" rec.Subdivisions = append(rec.Subdivisions, struct { ISOCode string `maxminddb:"iso_code"` }{"L"}) return nil }} got, ok := m.Lookup(net.ParseIP("81.2.69.142")) if !ok || got.Country != "IE" || got.Subdivision != "" { t.Fatalf("registered fallback = (%+v, %v), want IE with no subdivision", got, ok) } } func TestLookupPointWithoutCountrySucceeds(t *testing.T) { lat, lon := 10.0, 20.0 m := &MMDB{lookup: func(_ net.IP, result any) error { rec := result.(*mmdbRecord) rec.Location.Latitude, rec.Location.Longitude = &lat, &lon return nil }} got, ok := m.Lookup(net.ParseIP("81.2.69.142")) if !ok || got.Country != "" || !got.HasPoint || got.Point != (Point{10, 20}) { t.Fatalf("point-only = (%+v, %v)", got, ok) } // Locate's existing rule: a valid point succeeds without a country. if p, ok := m.Locate(net.ParseIP("81.2.69.142")); !ok || p != (Point{10, 20}) { t.Fatalf("Locate point-only = (%v, %v)", p, ok) } } func TestLocateRegisteredCountryFallbackUnchanged(t *testing.T) { // Codex R1-9: a country with no representative point falls back to the // registered country's point. "XK" has none in the table; "GB" does. if _, ok := CountryPoint("XK"); ok { t.Skip("XK gained a representative point; pick another code absent from the table") } m := &MMDB{lookup: func(_ net.IP, result any) error { rec := result.(*mmdbRecord) rec.Country.ISOCode, rec.RegisteredCountry.ISOCode = "XK", "GB" return nil }} gb, _ := CountryPoint("GB") if p, ok := m.Locate(net.ParseIP("81.2.69.142")); !ok || p != gb { t.Fatalf("Locate = (%v, %v), want GB's point", p, ok) } } func TestLookupRejectsMalformedCodes(t *testing.T) { m := &MMDB{lookup: func(_ net.IP, result any) error { rec := result.(*mmdbRecord) rec.Country.ISOCode = "gb" // lower case is not ISO rec.Subdivisions = append(rec.Subdivisions, struct { ISOCode string `maxminddb:"iso_code"` }{"TOOLONG"}) return nil }} if got, ok := m.Lookup(net.ParseIP("81.2.69.142")); ok { t.Fatalf("malformed codes answered %+v", got) } } func TestOpenMMDBTrustedOpensWithoutVerify(t *testing.T) { m, err := OpenMMDBTrusted("testdata/GeoIP2-City-Test.mmdb") if err != nil { t.Fatal(err) } defer m.Close() if r, ok := m.Lookup(net.ParseIP("81.2.69.142")); !ok || r.Country != "GB" { t.Fatalf("trusted open lookup = (%+v, %v)", r, ok) } if _, err := OpenMMDBTrusted(filepath.Join(t.TempDir(), "missing.mmdb")); err == nil { t.Fatal("a missing file opened") } } func TestSourceIsSanitised(t *testing.T) { m := &MMDB{source: sanitizeSource("GeoLite2-City\n