| 1 | package console |
| 2 | |
| 3 | import ( |
| 4 | "context" |
| 5 | "net" |
| 6 | "os" |
| 7 | "path/filepath" |
| 8 | "strconv" |
| 9 | "strings" |
| 10 | "testing" |
| 11 | "time" |
| 12 | |
| 13 | "github.com/carlosframework/platform/internal/geo" |
| 14 | "github.com/carlosframework/platform/internal/geoip" |
| 15 | ) |
| 16 | |
| 17 | func writeLocal(t *testing.T, dir, name string, body []byte, typ string, build time.Time) string { |
| 18 | t.Helper() |
| 19 | os.MkdirAll(dir, 0o755) |
| 20 | sha := name |
| 21 | if err := os.WriteFile(filepath.Join(dir, sha+".mmdb"), body, 0o644); err != nil { |
| 22 | t.Fatal(err) |
| 23 | } |
| 24 | cur := `{"sha256":"` + sha + `","database_type":"` + typ + `","build_epoch":` + itoa(build.Unix()) + `}` |
| 25 | if err := os.WriteFile(filepath.Join(dir, geoip.LocalCurrentFile), []byte(cur), 0o644); err != nil { |
| 26 | t.Fatal(err) |
| 27 | } |
| 28 | return sha |
| 29 | } |
| 30 | |
| 31 | func itoa(n int64) string { return strconv.FormatInt(n, 10) } |
| 32 | |
| 33 | func TestFollowerTracksTheEdgesGeneration(t *testing.T) { |
| 34 | dir := filepath.Join(t.TempDir(), "geoip") |
| 35 | sw := geo.NewSwappable() |
| 36 | f := NewGeoipFollower(dir, sw) |
| 37 | f.Tick() // nothing yet: the normal empty start |
| 38 | if _, ok := sw.Locate(net.ParseIP("81.2.69.142")); ok { |
| 39 | t.Fatal("located with nothing published") |
| 40 | } |
| 41 | shaA := strings.Repeat("a", 64) |
| 42 | writeLocal(t, dir, shaA, fixture(t, "GeoIP2-City-Test.mmdb"), "GeoIP2-City", time.Now()) |
| 43 | f.Tick() |
| 44 | if snap := sw.Lookup(net.ParseIP("81.2.69.142")); !snap.Found || snap.Source != "GeoIP2-City" { |
| 45 | t.Fatalf("after publish = %+v", snap) |
| 46 | } |
| 47 | // Off: the edge empties current. |
| 48 | os.WriteFile(filepath.Join(dir, geoip.LocalCurrentFile), []byte(`{}`), 0o644) |
| 49 | f.Tick() |
| 50 | if sw.Lookup(net.ParseIP("81.2.69.142")).Loaded { |
| 51 | t.Fatal("still loaded after off") |
| 52 | } |
| 53 | // current removed entirely: also off. |
| 54 | writeLocal(t, dir, shaA, fixture(t, "GeoIP2-City-Test.mmdb"), "GeoIP2-City", time.Now()) |
| 55 | f.Tick() |
| 56 | os.Remove(filepath.Join(dir, geoip.LocalCurrentFile)) |
| 57 | f.Tick() |
| 58 | if sw.Lookup(net.ParseIP("81.2.69.142")).Loaded { |
| 59 | t.Fatal("still loaded with current gone") |
| 60 | } |
| 61 | } |
| 62 | |
| 63 | func TestFollowerDropsAnExpiredGeoLite(t *testing.T) { |
| 64 | dir := filepath.Join(t.TempDir(), "geoip") |
| 65 | sw := geo.NewSwappable() |
| 66 | f := NewGeoipFollower(dir, sw) |
| 67 | sha := "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" |
| 68 | now := time.Now() |
| 69 | writeLocal(t, dir, sha, fixture(t, "GeoIP2-City-Test.mmdb"), "GeoLite2-City", now) |
| 70 | f.Tick() |
| 71 | if !sw.Lookup(net.ParseIP("81.2.69.142")).Loaded { |
| 72 | t.Fatal("not loaded") |
| 73 | } |
| 74 | f.now = func() time.Time { return now.Add(31 * 24 * time.Hour) } |
| 75 | f.Tick() |
| 76 | if sw.Lookup(net.ParseIP("81.2.69.142")).Loaded { |
| 77 | t.Fatal("an expired GeoLite2 is still in use") |
| 78 | } |
| 79 | } |
| 80 | |
| 81 | func TestFollowerKeepsTheDatabaseOnAnUnreadableState(t *testing.T) { |
| 82 | dir := filepath.Join(t.TempDir(), "geoip") |
| 83 | sw := geo.NewSwappable() |
| 84 | f := NewGeoipFollower(dir, sw) |
| 85 | sha := "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc" |
| 86 | writeLocal(t, dir, sha, fixture(t, "GeoIP2-City-Test.mmdb"), "GeoIP2-City", time.Now()) |
| 87 | f.Tick() |
| 88 | os.WriteFile(filepath.Join(dir, geoip.LocalCurrentFile), []byte(`not json`), 0o644) |
| 89 | f.Tick() |
| 90 | if !sw.Lookup(net.ParseIP("81.2.69.142")).Loaded { |
| 91 | t.Fatal("a garbled current dropped the database") |
| 92 | } |
| 93 | } |
| 94 | |
| 95 | // Code review R3-1: the loaded reader's own expiry is checked every tick, |
| 96 | // even when `current` is unreadable or names an unopenable generation. |
| 97 | func TestFollowerExpiresTheLoadedReaderWhateverCurrentSays(t *testing.T) { |
| 98 | for name, breakCurrent := range map[string]func(dir string){ |
| 99 | "unreadable current": func(dir string) { |
| 100 | os.WriteFile(filepath.Join(dir, geoip.LocalCurrentFile), []byte("garbled"), 0o644) |
| 101 | }, |
| 102 | "unopenable candidate": func(dir string) { |
| 103 | writeLocal(t, dir, strings.Repeat("e", 64), []byte("not a database"), "GeoIP2-City", time.Now()) |
| 104 | }, |
| 105 | } { |
| 106 | dir := filepath.Join(t.TempDir(), "geoip") |
| 107 | sw := geo.NewSwappable() |
| 108 | f := NewGeoipFollower(dir, sw) |
| 109 | writeLocal(t, dir, strings.Repeat("d", 64), fixture(t, "GeoIP2-City-Test.mmdb"), "GeoIP2-City", time.Now()) |
| 110 | f.Tick() |
| 111 | breakCurrent(dir) |
| 112 | followerExpired = func(string, time.Time, time.Time) bool { return true } |
| 113 | f.Tick() |
| 114 | followerExpired = geoip.Expired |
| 115 | if sw.Lookup(net.ParseIP("81.2.69.142")).Loaded { |
| 116 | t.Errorf("%s: an expired reader kept serving", name) |
| 117 | } |
| 118 | } |
| 119 | } |
| 120 | |
| 121 | // Code review R4-4: the guess's credit names the reader that answered. |
| 122 | func TestRecordingLocatorNamesTheAnsweringReader(t *testing.T) { |
| 123 | sw := geo.NewSwappable() |
| 124 | rec := &recordingLocator{sw: sw} |
| 125 | if _, ok := rec.Locate(net.ParseIP("81.2.69.142")); ok || rec.used { |
| 126 | t.Fatal("an empty reader answered") |
| 127 | } |
| 128 | db, _ := geo.OpenMMDB("../geo/testdata/GeoIP2-City-Test.mmdb") |
| 129 | sw.Swap(db) |
| 130 | if _, ok := rec.Locate(net.ParseIP("81.2.69.142")); !ok || !rec.used || rec.source != "GeoIP2-City" { |
| 131 | t.Fatalf("recorded = %+v", rec) |
| 132 | } |
| 133 | } |
| 134 | |
| 135 | // TestRealDatabaseVerifiesInTheChild runs only with GEOIP_REAL_MMDB set: |
| 136 | // a real database must fit the verifier child's memory cap. |
| 137 | func TestRealDatabaseVerifiesInTheChild(t *testing.T) { |
| 138 | p := os.Getenv("GEOIP_REAL_MMDB") |
| 139 | if p == "" { |
| 140 | t.Skip("set GEOIP_REAL_MMDB to a real .mmdb") |
| 141 | } |
| 142 | res, err := verifyInChild(context.Background(), p, []string{"8.8.8.8", "1.1.1.1"}) |
| 143 | if err != nil || res.Countries != 2 { |
| 144 | t.Fatalf("real database in the child = (%+v, %v)", res, err) |
| 145 | } |
| 146 | t.Logf("%s, %d nodes", res.Source, res.NodeCount) |
| 147 | } |
| 148 | |