package console import ( "context" "net" "os" "path/filepath" "strconv" "strings" "testing" "time" "github.com/carlosframework/platform/internal/geo" "github.com/carlosframework/platform/internal/geoip" ) func writeLocal(t *testing.T, dir, name string, body []byte, typ string, build time.Time) string { t.Helper() os.MkdirAll(dir, 0o755) sha := name if err := os.WriteFile(filepath.Join(dir, sha+".mmdb"), body, 0o644); err != nil { t.Fatal(err) } cur := `{"sha256":"` + sha + `","database_type":"` + typ + `","build_epoch":` + itoa(build.Unix()) + `}` if err := os.WriteFile(filepath.Join(dir, geoip.LocalCurrentFile), []byte(cur), 0o644); err != nil { t.Fatal(err) } return sha } func itoa(n int64) string { return strconv.FormatInt(n, 10) } func TestFollowerTracksTheEdgesGeneration(t *testing.T) { dir := filepath.Join(t.TempDir(), "geoip") sw := geo.NewSwappable() f := NewGeoipFollower(dir, sw) f.Tick() // nothing yet: the normal empty start if _, ok := sw.Locate(net.ParseIP("81.2.69.142")); ok { t.Fatal("located with nothing published") } shaA := strings.Repeat("a", 64) writeLocal(t, dir, shaA, fixture(t, "GeoIP2-City-Test.mmdb"), "GeoIP2-City", time.Now()) f.Tick() if snap := sw.Lookup(net.ParseIP("81.2.69.142")); !snap.Found || snap.Source != "GeoIP2-City" { t.Fatalf("after publish = %+v", snap) } // Off: the edge empties current. os.WriteFile(filepath.Join(dir, geoip.LocalCurrentFile), []byte(`{}`), 0o644) f.Tick() if sw.Lookup(net.ParseIP("81.2.69.142")).Loaded { t.Fatal("still loaded after off") } // current removed entirely: also off. writeLocal(t, dir, shaA, fixture(t, "GeoIP2-City-Test.mmdb"), "GeoIP2-City", time.Now()) f.Tick() os.Remove(filepath.Join(dir, geoip.LocalCurrentFile)) f.Tick() if sw.Lookup(net.ParseIP("81.2.69.142")).Loaded { t.Fatal("still loaded with current gone") } } func TestFollowerDropsAnExpiredGeoLite(t *testing.T) { dir := filepath.Join(t.TempDir(), "geoip") sw := geo.NewSwappable() f := NewGeoipFollower(dir, sw) sha := "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" now := time.Now() writeLocal(t, dir, sha, fixture(t, "GeoIP2-City-Test.mmdb"), "GeoLite2-City", now) f.Tick() if !sw.Lookup(net.ParseIP("81.2.69.142")).Loaded { t.Fatal("not loaded") } f.now = func() time.Time { return now.Add(31 * 24 * time.Hour) } f.Tick() if sw.Lookup(net.ParseIP("81.2.69.142")).Loaded { t.Fatal("an expired GeoLite2 is still in use") } } func TestFollowerKeepsTheDatabaseOnAnUnreadableState(t *testing.T) { dir := filepath.Join(t.TempDir(), "geoip") sw := geo.NewSwappable() f := NewGeoipFollower(dir, sw) sha := "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc" writeLocal(t, dir, sha, fixture(t, "GeoIP2-City-Test.mmdb"), "GeoIP2-City", time.Now()) f.Tick() os.WriteFile(filepath.Join(dir, geoip.LocalCurrentFile), []byte(`not json`), 0o644) f.Tick() if !sw.Lookup(net.ParseIP("81.2.69.142")).Loaded { t.Fatal("a garbled current dropped the database") } } // Code review R3-1: the loaded reader's own expiry is checked every tick, // even when `current` is unreadable or names an unopenable generation. func TestFollowerExpiresTheLoadedReaderWhateverCurrentSays(t *testing.T) { for name, breakCurrent := range map[string]func(dir string){ "unreadable current": func(dir string) { os.WriteFile(filepath.Join(dir, geoip.LocalCurrentFile), []byte("garbled"), 0o644) }, "unopenable candidate": func(dir string) { writeLocal(t, dir, strings.Repeat("e", 64), []byte("not a database"), "GeoIP2-City", time.Now()) }, } { dir := filepath.Join(t.TempDir(), "geoip") sw := geo.NewSwappable() f := NewGeoipFollower(dir, sw) writeLocal(t, dir, strings.Repeat("d", 64), fixture(t, "GeoIP2-City-Test.mmdb"), "GeoIP2-City", time.Now()) f.Tick() breakCurrent(dir) followerExpired = func(string, time.Time, time.Time) bool { return true } f.Tick() followerExpired = geoip.Expired if sw.Lookup(net.ParseIP("81.2.69.142")).Loaded { t.Errorf("%s: an expired reader kept serving", name) } } } // Code review R4-4: the guess's credit names the reader that answered. func TestRecordingLocatorNamesTheAnsweringReader(t *testing.T) { sw := geo.NewSwappable() rec := &recordingLocator{sw: sw} if _, ok := rec.Locate(net.ParseIP("81.2.69.142")); ok || rec.used { t.Fatal("an empty reader answered") } db, _ := geo.OpenMMDB("../geo/testdata/GeoIP2-City-Test.mmdb") sw.Swap(db) if _, ok := rec.Locate(net.ParseIP("81.2.69.142")); !ok || !rec.used || rec.source != "GeoIP2-City" { t.Fatalf("recorded = %+v", rec) } } // TestRealDatabaseVerifiesInTheChild runs only with GEOIP_REAL_MMDB set: // a real database must fit the verifier child's memory cap. func TestRealDatabaseVerifiesInTheChild(t *testing.T) { p := os.Getenv("GEOIP_REAL_MMDB") if p == "" { t.Skip("set GEOIP_REAL_MMDB to a real .mmdb") } res, err := verifyInChild(context.Background(), p, []string{"8.8.8.8", "1.1.1.1"}) if err != nil || res.Countries != 2 { t.Fatalf("real database in the child = (%+v, %v)", res, err) } t.Logf("%s, %d nodes", res.Source, res.NodeCount) }