| 1 | package host |
| 2 | |
| 3 | import ( |
| 4 | "context" |
| 5 | "encoding/json" |
| 6 | "testing" |
| 7 | "time" |
| 8 | |
| 9 | "github.com/carlosframework/platform/internal/analytics" |
| 10 | "github.com/carlosframework/platform/internal/release" |
| 11 | "github.com/carlosframework/platform/internal/usage" |
| 12 | ) |
| 13 | |
| 14 | func TestUsageFlushWritesAndReflushesMerge(t *testing.T) { |
| 15 | s := release.NewDirStore(t.TempDir()) |
| 16 | ctx := context.Background() |
| 17 | at := time.Date(2026, 8, 11, 9, 0, 0, 0, time.UTC) |
| 18 | rows := map[string]usage.EdgeRow{ |
| 19 | "a.example.com": {Account: "bab", App: "hello", Requests: 3, BytesOut: 300}, |
| 20 | } |
| 21 | if err := usageFlush(ctx, s, s, "box1", func() map[string]usage.EdgeRow { return rows }, nil, at); err != nil { |
| 22 | t.Fatal(err) |
| 23 | } |
| 24 | if err := usageFlush(ctx, s, s, "box1", func() map[string]usage.EdgeRow { return rows }, nil, at.Add(5*time.Minute)); err != nil { |
| 25 | t.Fatal(err) |
| 26 | } |
| 27 | raw, _, err := s.Get(ctx, usage.EdgeDayKey("box1", "2026-08-11")) |
| 28 | if err != nil { |
| 29 | t.Fatal(err) |
| 30 | } |
| 31 | var day usage.EdgeDay |
| 32 | if err := json.Unmarshal(raw, &day); err != nil { |
| 33 | t.Fatal(err) |
| 34 | } |
| 35 | if got := day.Hours["09"]["a.example.com"].Requests; got != 6 { |
| 36 | t.Fatalf("requests = %d, want 6", got) |
| 37 | } |
| 38 | } |
| 39 | |
| 40 | func TestUsageFlushEmptyDrainWritesNothing(t *testing.T) { |
| 41 | s := release.NewDirStore(t.TempDir()) |
| 42 | err := usageFlush(context.Background(), s, s, "box1", func() map[string]usage.EdgeRow { return nil }, nil, time.Now().UTC()) |
| 43 | if err != nil { |
| 44 | t.Fatal(err) |
| 45 | } |
| 46 | if keys, _ := s.List(context.Background(), "usage/"); len(keys) != 0 { |
| 47 | t.Fatalf("wrote %v", keys) |
| 48 | } |
| 49 | } |
| 50 | |
| 51 | // TestUsageFlushAlsoFlushesAnalytics: the same tick that flushes edge usage |
| 52 | // also drains the analytics meter into its app-day objects (spec §3.3). A |
| 53 | // batch that fails to write is re-queued and lands on the next flush. |
| 54 | func TestUsageFlushAlsoFlushesAnalytics(t *testing.T) { |
| 55 | ctx := context.Background() |
| 56 | s := release.NewDirStore(t.TempDir()) |
| 57 | now := time.Date(2026, 9, 7, 9, 30, 0, 0, time.UTC) |
| 58 | am := analytics.NewMeter(t.TempDir(), "box1", func() time.Time { return now }) |
| 59 | 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}) |
| 60 | if err := usageFlush(ctx, s, s, "box1", func() map[string]usage.EdgeRow { return nil }, am, now); err != nil { |
| 61 | t.Fatal(err) |
| 62 | } |
| 63 | sum, err := analytics.ReadRange(ctx, s, "bab", "web", now, now) |
| 64 | if err != nil || sum.PV != 1 { |
| 65 | t.Fatalf("flushed day: %+v %v", sum, err) |
| 66 | } |
| 67 | // A failed analytics flush re-queues; the next flush lands it. |
| 68 | 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}) |
| 69 | _ = usageFlush(ctx, s, failingStore{Store: s}, "box1", func() map[string]usage.EdgeRow { return nil }, am, now) |
| 70 | if err := usageFlush(ctx, s, s, "box1", func() map[string]usage.EdgeRow { return nil }, am, now); err != nil { |
| 71 | t.Fatal(err) |
| 72 | } |
| 73 | if sum, _ := analytics.ReadRange(ctx, s, "bab", "web", now, now); sum.PV != 2 { |
| 74 | t.Fatalf("after requeue: %d", sum.PV) |
| 75 | } |
| 76 | } |
| 77 | |