package analytics import ( "context" "encoding/json" "fmt" "testing" "time" "github.com/carlosframework/platform/internal/release" ) func pv(country, src, state string) Obs { return Obs{Acct: "a", App: "x", Host: "x.test", Method: "GET", Status: 200, ContentType: "text/html", Path: "/", UA: "Mozilla/5.0", ClientIP: "203.0.113.1", When: time.Date(2026, 10, 6, 9, 0, 0, 0, time.UTC), Country: country, GeoSource: src, GeoState: state} } func TestMeterCountsCountriesByState(t *testing.T) { now := time.Date(2026, 10, 6, 9, 0, 0, 0, time.UTC) m := NewMeter(t.TempDir(), "box1", func() time.Time { return now }) m.Observe(pv("GB", "DBIP-City-Lite", "hit")) m.Observe(pv("", "DBIP-City-Lite", "hit")) // coordinate-only (codex R3-3) m.Observe(pv("", "DBIP-City-Lite", "miss")) m.Observe(pv("", "", "off")) m.Observe(pv("", "", "")) // an edge from before GeoIP nonPV := pv("FR", "DBIP-City-Lite", "hit") nonPV.Status = 404 m.Observe(nonPV) d := m.Drain()[AppKey{Acct: "a", App: "x"}][0] if d.Countries["GB"] != 1 || d.Countries["_unknown"] != 2 || len(d.Countries) != 2 { t.Fatalf("countries = %v", d.Countries) } if d.GeoSources["DBIP-City-Lite"] != 3 || len(d.GeoSources) != 1 { t.Fatalf("sources = %v", d.GeoSources) } } func TestReadRangeMergesCountriesAcrossBoxesDaysAndOldDays(t *testing.T) { ctx := context.Background() s := release.NewDirStore(t.TempDir()) put := func(box, date string, d Day) { raw, _ := json.Marshal(d) if _, err := s.Put(ctx, must(t)(DayKey("bab", "x", box, date)), raw, ""); err != nil { t.Fatal(err) } } // An old day, from before GeoIP: no geo_sources, empty countries. put("b1", "2026-10-04", Day{V: 1, Date: "2026-10-04", Countries: map[string]int64{}}) put("b1", "2026-10-05", Day{V: 1, Date: "2026-10-05", Countries: map[string]int64{"GB": 3}, GeoSources: map[string]int64{"DBIP-City-Lite": 3}}) put("b2", "2026-10-05", Day{V: 1, Date: "2026-10-05", Countries: map[string]int64{"GB": 1, "IE": 2}, GeoSources: map[string]int64{"GeoLite2-City": 3}}) sum, err := ReadRange(ctx, s, "bab", "x", time.Date(2026, 10, 4, 0, 0, 0, 0, time.UTC), time.Date(2026, 10, 5, 0, 0, 0, 0, time.UTC)) if err != nil { t.Fatal(err) } if sum.Countries["GB"] != 4 || sum.Countries["IE"] != 2 { t.Fatalf("countries = %v", sum.Countries) } if sum.GeoSources["DBIP-City-Lite"] != 3 || sum.GeoSources["GeoLite2-City"] != 3 { t.Fatalf("both sources must be credited (spec ยง9): %v", sum.GeoSources) } } func TestDayMergeAddsCountries(t *testing.T) { var d Day d.Merge(Delta{Box: "b", Seq: 1, Countries: map[string]int64{"GB": 1}, GeoSources: map[string]int64{"s": 1}}) d.Merge(Delta{Box: "b", Seq: 2, Countries: map[string]int64{"GB": 2, "IE": 1}, GeoSources: map[string]int64{"s": 3}}) if d.Countries["GB"] != 3 || d.Countries["IE"] != 1 || d.GeoSources["s"] != 4 { t.Fatalf("day = %v %v", d.Countries, d.GeoSources) } } func TestBoundedAddFoldsIntoOther(t *testing.T) { m := map[string]int64{} for i := 0; i < 12; i++ { boundedAdd(m, fmt.Sprintf("K%02d", i), 1, 8, nil) } if len(m) != 9 || m["_other"] != 4 { t.Fatalf("bounded = %v", m) } } // Codex R7-3: an attributed source is never folded, however many other // types crowd the map, across flushes and days. func TestAttributedSourcesSurviveOverflow(t *testing.T) { var d Day junk := map[string]int64{} for i := 0; i < 20; i++ { junk[fmt.Sprintf("Custom-%02d", i)] = 1 } d.Merge(Delta{Box: "b", Seq: 1, GeoSources: junk}) d.Merge(Delta{Box: "b", Seq: 2, GeoSources: map[string]int64{"DBIP-City-Lite": 5, "GeoLite2-Country": 2}}) if d.GeoSources["DBIP-City-Lite"] != 5 || d.GeoSources["GeoLite2-Country"] != 2 { t.Fatalf("attributed sources folded: %v", d.GeoSources) } if d.GeoSources["_other"] == 0 { t.Fatalf("unattributed overflow must still fold: %v", d.GeoSources) } }