carlos / platform Public

Clone
git clone https://amadan.net/carlos/platform

Plain git — no account needed to clone.

Download

Download this file

1package geo
2
3import (
4 "bytes"
5 "errors"
6 "log"
7 "math"
8 "net"
9 "os"
10 "path/filepath"
11 "strings"
12 "testing"
13 "time"
14)
15
16func TestOpenMMDBRefusesNonDatabases(t *testing.T) {
17 dir := t.TempDir()
18 missing := filepath.Join(dir, "missing.mmdb")
19 junk := filepath.Join(dir, "junk.mmdb")
20 if err := os.WriteFile(junk, []byte("not a maxmind database"), 0o600); err != nil {
21 t.Fatal(err)
22 }
23 city, err := os.ReadFile("testdata/GeoIP2-City-Test.mmdb")
24 if err != nil {
25 t.Fatal(err)
26 }
27 truncated := filepath.Join(dir, "truncated.mmdb")
28 if err := os.WriteFile(truncated, city[:len(city)/3], 0o600); err != nil {
29 t.Fatal(err)
30 }
31 for _, p := range []string{missing, junk, truncated} {
32 if m, err := OpenMMDB(p); err == nil {
33 m.Close()
34 t.Errorf("OpenMMDB(%s) succeeded, want an error", filepath.Base(p))
35 }
36 }
37}
38
39func TestMMDBCityRecordGivesItsLocation(t *testing.T) {
40 m, err := OpenMMDB("testdata/GeoIP2-City-Test.mmdb")
41 if err != nil {
42 t.Fatal(err)
43 }
44 defer m.Close()
45 p, ok := m.Locate(net.ParseIP("81.2.69.142"))
46 if !ok || p.Lat < 51 || p.Lat > 52 {
47 t.Fatalf("81.2.69.142 = (%v, %v), want a London point", p, ok)
48 }
49 // Review Focus 2: an IPv4-mapped IPv6 address is the same client.
50 if mapped, ok := m.Locate(net.ParseIP("::ffff:81.2.69.142")); !ok || mapped != p {
51 t.Fatalf("::ffff:81.2.69.142 = (%v, %v), want %v", mapped, ok, p)
52 }
53}
54
55func TestMMDBCountryOnlyRecordGivesTheCountryPoint(t *testing.T) {
56 m, err := OpenMMDB("testdata/GeoIP2-Country-Test.mmdb")
57 if err != nil {
58 t.Fatal(err)
59 }
60 defer m.Close()
61 p, ok := m.Locate(net.ParseIP("81.2.69.142"))
62 gb, _ := CountryPoint("GB")
63 if !ok || p != gb {
64 t.Fatalf("country-only 81.2.69.142 = (%v, %v), want GB's point %v", p, ok, gb)
65 }
66}
67
68// Review Focus 2 and spec §6.5: a public IPv6 address looked up in an
69// IPv4-only database is a miss, and the reader's error — which contains
70// the address — never reaches the log.
71func TestMMDBLookupErrorsNeverLogTheAddress(t *testing.T) {
72 m, err := OpenMMDB("testdata/MaxMind-DB-test-ipv4-24.mmdb")
73 if err != nil {
74 t.Fatal(err)
75 }
76 defer m.Close()
77 var buf bytes.Buffer
78 prev := log.Writer()
79 log.SetOutput(&buf)
80 defer log.SetOutput(prev)
81 lookupErrLast = lookupErrLast.AddDate(-1, 0, 0) // un-throttle
82 if _, ok := m.Locate(net.ParseIP("2001:4860::1")); ok {
83 t.Fatal("an IPv6 address located in an IPv4-only database")
84 }
85 if bytes.Contains(buf.Bytes(), []byte("2001:4860")) {
86 t.Fatalf("the address leaked into the log: %s", buf.String())
87 }
88 if buf.Len() == 0 {
89 t.Fatal("a lookup failure must be logged (as a fixed category)")
90 }
91}
92
93// Final review: a structurally valid database can still carry a malformed
94// location. Nonfinite values and coordinates off the globe are a miss
95// (ok=false), never a point that ranks regions from nowhere, and never a
96// fall-through to the record's country either: a record that lies about
97// one field is not trusted on the next.
98func TestLocateRejectsMalformedCoordinates(t *testing.T) {
99 at := func(lat, lon float64) *MMDB {
100 return &MMDB{lookup: func(_ net.IP, result any) error {
101 rec := result.(*mmdbRecord)
102 rec.Location.Latitude, rec.Location.Longitude = &lat, &lon
103 rec.Country.ISOCode = "GB"
104 return nil
105 }}
106 }
107 bad := map[string][2]float64{
108 "NaN latitude": {math.NaN(), 0},
109 "NaN longitude": {0, math.NaN()},
110 "+Inf latitude": {math.Inf(1), 0},
111 "-Inf longitude": {0, math.Inf(-1)},
112 "latitude above 90": {90.0001, 0},
113 "latitude below -90": {-91, 0},
114 "longitude above 180": {0, 180.5},
115 "longitude below -180": {0, -181},
116 }
117 for name, c := range bad {
118 if p, ok := at(c[0], c[1]).Locate(net.ParseIP("81.2.69.142")); ok {
119 t.Errorf("%s: located %v, want a miss", name, p)
120 }
121 }
122 // The edges of the globe are on it.
123 for name, c := range map[string][2]float64{
124 "north pole": {90, 0}, "south pole": {-90, 0}, "antimeridian east": {0, 180}, "antimeridian west": {0, -180},
125 } {
126 if p, ok := at(c[0], c[1]).Locate(net.ParseIP("81.2.69.142")); !ok || p != (Point{c[0], c[1]}) {
127 t.Errorf("%s: (%v, %v), want %v", name, p, ok, Point{c[0], c[1]})
128 }
129 }
130}
131
132// Review Focus 2: non-public addresses never reach the reader — proved
133// through Locate itself, with a counting lookup in place of the reader.
134func TestLocateNeverLooksUpNonPublicAddresses(t *testing.T) {
135 calls := 0
136 m := &MMDB{lookup: func(net.IP, any) error { calls++; return errors.New("unused") }}
137 for _, s := range []string{
138 "127.0.0.1", "::1", "10.1.2.3", "192.168.0.1", "172.16.0.1",
139 "100.64.0.1", "169.254.1.1", "fe80::1", "fd00::1", "0.0.0.0", "::",
140 } {
141 if _, ok := m.Locate(net.ParseIP(s)); ok {
142 t.Errorf("Locate(%s) answered", s)
143 }
144 }
145 if calls != 0 {
146 t.Fatalf("the reader was asked %d time(s) about non-public addresses", calls)
147 }
148 m.Locate(net.ParseIP("81.2.69.142"))
149 if calls != 1 {
150 t.Fatalf("a public address must reach the reader, calls = %d", calls)
151 }
152 var nilM *MMDB
153 if _, ok := nilM.Locate(net.ParseIP("81.2.69.142")); ok {
154 t.Error("a nil *MMDB located an address")
155 }
156}
157
158func TestLookupCityGivesCountrySubdivisionAndPoint(t *testing.T) {
159 m, err := OpenMMDB("testdata/GeoIP2-City-Test.mmdb")
160 if err != nil {
161 t.Fatal(err)
162 }
163 defer m.Close()
164 cases := map[string]Result{
165 "81.2.69.142": {Country: "GB", Subdivision: "GB-ENG", HasPoint: true},
166 "216.160.83.56": {Country: "US", Subdivision: "US-WA", HasPoint: true},
167 "89.160.20.112": {Country: "SE", Subdivision: "SE-E", HasPoint: true},
168 "2001:218::1": {Country: "JP", HasPoint: true}, // no subdivision in the fixture
169 }
170 for ip, want := range cases {
171 got, ok := m.Lookup(net.ParseIP(ip))
172 if !ok || got.Country != want.Country || got.Subdivision != want.Subdivision || got.HasPoint != want.HasPoint {
173 t.Errorf("Lookup(%s) = (%+v, %v), want %+v", ip, got, ok, want)
174 }
175 }
176 if got, _ := m.Lookup(net.ParseIP("81.2.69.142")); got.Point.Lat < 51 || got.Point.Lat > 52 {
177 t.Errorf("London point = %v", got.Point)
178 }
179 if m.Source() != "GeoIP2-City" {
180 t.Errorf("Source = %q", m.Source())
181 }
182 if m.NodeCount() != 1547 {
183 t.Errorf("NodeCount = %d, want 1547 (FACTS.md)", m.NodeCount())
184 }
185 if m.BuildEpoch().Year() < 2020 {
186 t.Errorf("BuildEpoch = %v", m.BuildEpoch())
187 }
188}
189
190func TestLookupCountryDatabaseHasNoPoint(t *testing.T) {
191 m, err := OpenMMDB("testdata/GeoIP2-Country-Test.mmdb")
192 if err != nil {
193 t.Fatal(err)
194 }
195 defer m.Close()
196 got, ok := m.Lookup(net.ParseIP("81.2.69.142"))
197 if !ok || got.Country != "GB" || got.HasPoint || got.Subdivision != "" {
198 t.Fatalf("country-only = (%+v, %v), want GB with no point (never a centroid)", got, ok)
199 }
200}
201
202func TestLookupNoCountryDatabaseMisses(t *testing.T) {
203 m, err := OpenMMDB("testdata/MaxMind-DB-test-ipv4-24.mmdb")
204 if err != nil {
205 t.Fatal(err)
206 }
207 defer m.Close()
208 if got, ok := m.Lookup(net.ParseIP("81.2.69.142")); ok {
209 t.Fatalf("a database with no country or location answered %+v", got)
210 }
211}
212
213func TestLookupFallsBackToRegisteredCountryButNotItsSubdivision(t *testing.T) {
214 m := &MMDB{lookup: func(_ net.IP, result any) error {
215 rec := result.(*mmdbRecord)
216 rec.RegisteredCountry.ISOCode = "IE"
217 rec.Subdivisions = append(rec.Subdivisions, struct {
218 ISOCode string `maxminddb:"iso_code"`
219 }{"L"})
220 return nil
221 }}
222 got, ok := m.Lookup(net.ParseIP("81.2.69.142"))
223 if !ok || got.Country != "IE" || got.Subdivision != "" {
224 t.Fatalf("registered fallback = (%+v, %v), want IE with no subdivision", got, ok)
225 }
226}
227
228func TestLookupPointWithoutCountrySucceeds(t *testing.T) {
229 lat, lon := 10.0, 20.0
230 m := &MMDB{lookup: func(_ net.IP, result any) error {
231 rec := result.(*mmdbRecord)
232 rec.Location.Latitude, rec.Location.Longitude = &lat, &lon
233 return nil
234 }}
235 got, ok := m.Lookup(net.ParseIP("81.2.69.142"))
236 if !ok || got.Country != "" || !got.HasPoint || got.Point != (Point{10, 20}) {
237 t.Fatalf("point-only = (%+v, %v)", got, ok)
238 }
239 // Locate's existing rule: a valid point succeeds without a country.
240 if p, ok := m.Locate(net.ParseIP("81.2.69.142")); !ok || p != (Point{10, 20}) {
241 t.Fatalf("Locate point-only = (%v, %v)", p, ok)
242 }
243}
244
245func TestLocateRegisteredCountryFallbackUnchanged(t *testing.T) {
246 // Codex R1-9: a country with no representative point falls back to the
247 // registered country's point. "XK" has none in the table; "GB" does.
248 if _, ok := CountryPoint("XK"); ok {
249 t.Skip("XK gained a representative point; pick another code absent from the table")
250 }
251 m := &MMDB{lookup: func(_ net.IP, result any) error {
252 rec := result.(*mmdbRecord)
253 rec.Country.ISOCode, rec.RegisteredCountry.ISOCode = "XK", "GB"
254 return nil
255 }}
256 gb, _ := CountryPoint("GB")
257 if p, ok := m.Locate(net.ParseIP("81.2.69.142")); !ok || p != gb {
258 t.Fatalf("Locate = (%v, %v), want GB's point", p, ok)
259 }
260}
261
262func TestLookupRejectsMalformedCodes(t *testing.T) {
263 m := &MMDB{lookup: func(_ net.IP, result any) error {
264 rec := result.(*mmdbRecord)
265 rec.Country.ISOCode = "gb" // lower case is not ISO
266 rec.Subdivisions = append(rec.Subdivisions, struct {
267 ISOCode string `maxminddb:"iso_code"`
268 }{"TOOLONG"})
269 return nil
270 }}
271 if got, ok := m.Lookup(net.ParseIP("81.2.69.142")); ok {
272 t.Fatalf("malformed codes answered %+v", got)
273 }
274}
275
276func TestOpenMMDBTrustedOpensWithoutVerify(t *testing.T) {
277 m, err := OpenMMDBTrusted("testdata/GeoIP2-City-Test.mmdb")
278 if err != nil {
279 t.Fatal(err)
280 }
281 defer m.Close()
282 if r, ok := m.Lookup(net.ParseIP("81.2.69.142")); !ok || r.Country != "GB" {
283 t.Fatalf("trusted open lookup = (%+v, %v)", r, ok)
284 }
285 if _, err := OpenMMDBTrusted(filepath.Join(t.TempDir(), "missing.mmdb")); err == nil {
286 t.Fatal("a missing file opened")
287 }
288}
289
290func TestSourceIsSanitised(t *testing.T) {
291 m := &MMDB{source: sanitizeSource("GeoLite2-City\n<script>" + strings.Repeat("x", 100))}
292 if s := m.Source(); strings.ContainsAny(s, "\n<>") || len(s) > 64 {
293 t.Fatalf("Source = %q", s)
294 }
295 var nilM *MMDB
296 if nilM.Source() != "" || nilM.NodeCount() != 0 || !nilM.BuildEpoch().IsZero() {
297 t.Fatal("a nil MMDB must answer zero values")
298 }
299 _ = time.Now
300}
301
302func TestSourceNeverSanitisesToEmpty(t *testing.T) {
303 if got := sanitizeSource("\n<>!!"); got != "mmdb" {
304 t.Fatalf("sanitizeSource of only discarded characters = %q, want mmdb", got)
305 }
306 if got := sanitizeSource(""); got != "mmdb" {
307 t.Fatalf("empty type = %q, want mmdb", got)
308 }
309}
310