package main import ( "io" "net/http" "net/http/httptest" "strings" "testing" "time" "github.com/prometheus/client_golang/prometheus" dto "github.com/prometheus/client_model/go" ) func counterValue(t *testing.T, reg *prometheus.Registry, name string, labels map[string]string) float64 { t.Helper() metrics, err := reg.Gather() if err != nil { t.Fatalf("Gather() error: %v", err) } for _, mf := range metrics { if mf.GetName() != name { continue } for _, m := range mf.GetMetric() { if labelsMatch(m, labels) { return m.GetCounter().GetValue() } } } return 0 } func labelsMatch(m *dto.Metric, want map[string]string) bool { if len(m.GetLabel()) != len(want) { return false } for _, label := range m.GetLabel() { if want[label.GetName()] != label.GetValue() { return false } } return true } func TestHandleRouteRecordsRequestMetrics(t *testing.T) { reg := prometheus.NewRegistry() metrics := NewMetrics(reg) custom := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.Header().Set("Content-Type", "application/json") _, _ = w.Write([]byte(`{"status":"OK"}`)) })) defer custom.Close() cfg := Config{ Routes: []Route{{Prefixes: []string{"apple_bubbles"}, URL: custom.URL}}, RequestTimeout: time.Duration(defaultRequestTimeoutSec) * time.Second, } proxy := NewProxy(cfg, metrics) req := httptest.NewRequest(http.MethodPost, "/api/v1/send_push", strings.NewReader(`{"platform":"apple_bubbles","device_id":"tok","server_id":"s1"}`)) rec := httptest.NewRecorder() handleRoute(rec, req, proxy, cfg, "/api/v1/send_push") if got := counterValue(t, reg, metricRequestsTotalName, map[string]string{ "path": "send_push", "platform": "apple_bubbles", }); got != 1 { t.Fatalf("router_requests_total = %v, want 1", got) } if got := counterValue(t, reg, metricUpstreamRequestsTotalName, map[string]string{ "platform": "apple_bubbles", "outcome": "success", }); got != 1 { t.Fatalf("router_upstream_requests_total = %v, want 1", got) } } func TestHandleRouteRecordsUnknownPlatformMetric(t *testing.T) { reg := prometheus.NewRegistry() metrics := NewMetrics(reg) cfg := Config{ Routes: []Route{{Prefixes: []string{"apple_bubbles"}, URL: "http://localhost:1"}}, RequestTimeout: time.Duration(defaultRequestTimeoutSec) * time.Second, } proxy := NewProxy(cfg, metrics) req := httptest.NewRequest(http.MethodPost, "/api/v1/send_push", strings.NewReader(`{"platform":"unknown_app","device_id":"tok","server_id":"s1"}`)) rec := httptest.NewRecorder() handleRoute(rec, req, proxy, cfg, "/api/v1/send_push") if got := counterValue(t, reg, metricRoutingErrorsTotalName, map[string]string{ "reason": "unknown_platform", }); got != 1 { t.Fatalf("router_routing_errors_total = %v, want 1", got) } } func TestMetricsEndpoint(t *testing.T) { reg := prometheus.NewRegistry() metrics := NewMetrics(reg) metrics.incRequest("send_push", "apple_bubbles") metrics.incRoutingError("unknown_platform") metrics.observeUpstream("apple_bubbles", "success", time.Millisecond) mux := http.NewServeMux() mux.Handle("GET /metrics", metrics.Handler()) req := httptest.NewRequest(http.MethodGet, "/metrics", nil) rec := httptest.NewRecorder() mux.ServeHTTP(rec, req) if rec.Code != http.StatusOK { t.Fatalf("status = %d", rec.Code) } body, err := io.ReadAll(rec.Body) if err != nil { t.Fatal(err) } if !strings.Contains(string(body), metricRequestsTotalName) { t.Fatalf("metrics body missing %q", metricRequestsTotalName) } families, err := reg.Gather() if err != nil { t.Fatalf("Gather() error: %v", err) } got := make(map[string]struct{}, len(families)) for _, family := range families { got[family.GetName()] = struct{}{} } for _, name := range []string{ metricRequestsTotalName, metricRoutingErrorsTotalName, metricUpstreamRequestsTotalName, metricUpstreamDurationName, "go_goroutines", "process_cpu_seconds_total", } { if _, ok := got[name]; !ok { t.Fatalf("registry missing metric %q", name) } } } func TestUpstreamOutcome(t *testing.T) { tests := []struct { name string statusCode int body string want string }{ {"ok response", http.StatusOK, `{"status":"OK"}`, "success"}, {"fail response", http.StatusOK, `{"status":"FAIL","error":"nope"}`, "fail"}, {"http error", http.StatusBadGateway, `{}`, "fail"}, {"unparseable success", http.StatusOK, `not-json`, "success"}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if got := upstreamOutcome(tt.statusCode, []byte(tt.body)); got != tt.want { t.Fatalf("upstreamOutcome() = %q, want %q", got, tt.want) } }) } } func TestRoutePathLabel(t *testing.T) { if got := routePathLabel("/api/v1/ack"); got != "ack" { t.Fatalf("routePathLabel() = %q, want ack", got) } }