package host import ( "context" "encoding/json" "testing" "time" "github.com/carlosframework/platform/internal/analytics" "github.com/carlosframework/platform/internal/release" "github.com/carlosframework/platform/internal/usage" ) func TestUsageFlushWritesAndReflushesMerge(t *testing.T) { s := release.NewDirStore(t.TempDir()) ctx := context.Background() at := time.Date(2026, 8, 11, 9, 0, 0, 0, time.UTC) rows := map[string]usage.EdgeRow{ "a.example.com": {Account: "bab", App: "hello", Requests: 3, BytesOut: 300}, } if err := usageFlush(ctx, s, s, "box1", func() map[string]usage.EdgeRow { return rows }, nil, at); err != nil { t.Fatal(err) } if err := usageFlush(ctx, s, s, "box1", func() map[string]usage.EdgeRow { return rows }, nil, at.Add(5*time.Minute)); err != nil { t.Fatal(err) } raw, _, err := s.Get(ctx, usage.EdgeDayKey("box1", "2026-08-11")) if err != nil { t.Fatal(err) } var day usage.EdgeDay if err := json.Unmarshal(raw, &day); err != nil { t.Fatal(err) } if got := day.Hours["09"]["a.example.com"].Requests; got != 6 { t.Fatalf("requests = %d, want 6", got) } } func TestUsageFlushEmptyDrainWritesNothing(t *testing.T) { s := release.NewDirStore(t.TempDir()) err := usageFlush(context.Background(), s, s, "box1", func() map[string]usage.EdgeRow { return nil }, nil, time.Now().UTC()) if err != nil { t.Fatal(err) } if keys, _ := s.List(context.Background(), "usage/"); len(keys) != 0 { t.Fatalf("wrote %v", keys) } } // TestUsageFlushAlsoFlushesAnalytics: the same tick that flushes edge usage // also drains the analytics meter into its app-day objects (spec ยง3.3). A // batch that fails to write is re-queued and lands on the next flush. func TestUsageFlushAlsoFlushesAnalytics(t *testing.T) { ctx := context.Background() s := release.NewDirStore(t.TempDir()) now := time.Date(2026, 9, 7, 9, 30, 0, 0, time.UTC) am := analytics.NewMeter(t.TempDir(), "box1", func() time.Time { return now }) am.Observe(analytics.Obs{Acct: "bab", App: "web", Host: "h", Method: "GET", Status: 200, ContentType: "text/html", Path: "/", UA: "Mozilla/5.0 Safari", ClientIP: "1.2.3.4", Bytes: 10, When: now}) if err := usageFlush(ctx, s, s, "box1", func() map[string]usage.EdgeRow { return nil }, am, now); err != nil { t.Fatal(err) } sum, err := analytics.ReadRange(ctx, s, "bab", "web", now, now) if err != nil || sum.PV != 1 { t.Fatalf("flushed day: %+v %v", sum, err) } // A failed analytics flush re-queues; the next flush lands it. am.Observe(analytics.Obs{Acct: "bab", App: "web", Host: "h", Method: "GET", Status: 200, ContentType: "text/html", Path: "/", UA: "Mozilla/5.0 Safari", ClientIP: "1.2.3.4", Bytes: 10, When: now}) _ = usageFlush(ctx, s, failingStore{Store: s}, "box1", func() map[string]usage.EdgeRow { return nil }, am, now) if err := usageFlush(ctx, s, s, "box1", func() map[string]usage.EdgeRow { return nil }, am, now); err != nil { t.Fatal(err) } if sum, _ := analytics.ReadRange(ctx, s, "bab", "web", now, now); sum.PV != 2 { t.Fatalf("after requeue: %d", sum.PV) } }