package handlers import ( "encoding/json" "net/http" "net/http/httptest" "testing" "time" "github.com/gin-gonic/gin" "switchboard-core/metrics" "switchboard-core/store" ) // mockHub implements metrics.ConnCounter for tests. type mockHub struct{ count int } func (m *mockHub) ConnCount() int { return m.count } // mockBus implements metrics.BusCounter for tests. type mockBus struct{ pub, del int64 } func (m *mockBus) PublishCount() int64 { return m.pub } func (m *mockBus) DeliverCount() int64 { return m.del } func TestMetrics_SQLiteShape(t *testing.T) { gin.SetMode(gin.TestMode) collector := metrics.NewCollector( "test-node", nil, // no DB &mockHub{count: 5}, &mockBus{pub: 10, del: 20}, store.Stores{}, // no cluster store func() (uint64, uint64, float64) { return 42, 3, 12.5 }, func() int64 { return 7 }, time.Now().Add(-10*time.Minute), ) h := NewMetricsHandler(collector) r := gin.New() r.GET("/api/v1/admin/metrics", h.GetMetrics) req := httptest.NewRequest(http.MethodGet, "/api/v1/admin/metrics", nil) w := httptest.NewRecorder() r.ServeHTTP(w, req) if w.Code != 200 { t.Fatalf("status = %d, want 200", w.Code) } var snap metrics.Snapshot if err := json.Unmarshal(w.Body.Bytes(), &snap); err != nil { t.Fatalf("unmarshal: %v", err) } // Node ID if snap.NodeID != "test-node" { t.Errorf("node_id = %q, want %q", snap.NodeID, "test-node") } // Runtime if snap.Runtime.WSClients != 5 { t.Errorf("ws_clients = %d, want 5", snap.Runtime.WSClients) } if snap.Runtime.UptimeSec < 600 { t.Errorf("uptime_sec = %f, want >= 600", snap.Runtime.UptimeSec) } // Cluster should be nil (SQLite) if snap.Cluster != nil { t.Errorf("cluster should be nil for SQLite, got %+v", snap.Cluster) } // Extensions if snap.Extensions.StarlarkExecTotal != 42 { t.Errorf("starlark_exec_total = %d, want 42", snap.Extensions.StarlarkExecTotal) } if snap.Extensions.StarlarkErrorsTotal != 3 { t.Errorf("starlark_errors_total = %d, want 3", snap.Extensions.StarlarkErrorsTotal) } if snap.Extensions.StarlarkAvgDuration != 12.5 { t.Errorf("starlark_avg_duration_ms = %f, want 12.5", snap.Extensions.StarlarkAvgDuration) } if snap.Extensions.TriggerFiresTotal != 7 { t.Errorf("trigger_fires_total = %d, want 7", snap.Extensions.TriggerFiresTotal) } if snap.Extensions.EventBusPublished != 10 { t.Errorf("event_bus_published = %d, want 10", snap.Extensions.EventBusPublished) } if snap.Extensions.EventBusDelivered != 20 { t.Errorf("event_bus_delivered = %d, want 20", snap.Extensions.EventBusDelivered) } } func TestMetrics_WithCluster(t *testing.T) { gin.SetMode(gin.TestMode) mock := &mockClusterStore{ nodes: []store.ClusterNode{ { NodeID: "node-1", Endpoint: "http://node-1:8080", Heartbeat: time.Now(), Stats: json.RawMessage(`{"uptime_sec":120,"ws_clients":3}`), }, }, } collector := metrics.NewCollector( "test-node", nil, &mockHub{count: 3}, &mockBus{}, store.Stores{Cluster: mock}, func() (uint64, uint64, float64) { return 0, 0, 0 }, func() int64 { return 0 }, time.Now(), ) h := NewMetricsHandler(collector) r := gin.New() r.GET("/api/v1/admin/metrics", h.GetMetrics) req := httptest.NewRequest(http.MethodGet, "/api/v1/admin/metrics", nil) w := httptest.NewRecorder() r.ServeHTTP(w, req) if w.Code != 200 { t.Fatalf("status = %d, want 200", w.Code) } var snap metrics.Snapshot if err := json.Unmarshal(w.Body.Bytes(), &snap); err != nil { t.Fatalf("unmarshal: %v", err) } if snap.Cluster == nil { t.Fatal("cluster should not be nil with cluster store") } if snap.Cluster.Size != 1 { t.Errorf("cluster.size = %d, want 1", snap.Cluster.Size) } if snap.Cluster.Nodes[0].NodeID != "node-1" { t.Errorf("node_id = %q, want %q", snap.Cluster.Nodes[0].NodeID, "node-1") } if snap.Cluster.Nodes[0].UptimeSec != 120 { t.Errorf("uptime_sec = %f, want 120", snap.Cluster.Nodes[0].UptimeSec) } } func TestMetrics_ExtensionCounters(t *testing.T) { gin.SetMode(gin.TestMode) collector := metrics.NewCollector( "test-node", nil, &mockHub{}, &mockBus{pub: 100, del: 500}, store.Stores{}, func() (uint64, uint64, float64) { return 1000, 50, 8.3 }, func() int64 { return 25 }, time.Now(), ) h := NewMetricsHandler(collector) r := gin.New() r.GET("/api/v1/admin/metrics", h.GetMetrics) req := httptest.NewRequest(http.MethodGet, "/api/v1/admin/metrics", nil) w := httptest.NewRecorder() r.ServeHTTP(w, req) var snap metrics.Snapshot _ = json.Unmarshal(w.Body.Bytes(), &snap) if snap.Extensions.StarlarkExecTotal != 1000 { t.Errorf("exec total = %d, want 1000", snap.Extensions.StarlarkExecTotal) } if snap.Extensions.EventBusPublished != 100 { t.Errorf("bus published = %d, want 100", snap.Extensions.EventBusPublished) } if snap.Extensions.EventBusDelivered != 500 { t.Errorf("bus delivered = %d, want 500", snap.Extensions.EventBusDelivered) } }