package metrics import ( "errors" "net/http" "net/http/httptest" "strings" "testing" "time" "github.com/go-chi/chi/v5" ) func TestObserveSyncAndHTTPExposition(t *testing.T) { t.Cleanup(Reset) Reset() ObserveHTTP(http.MethodGet, "/healthz", http.StatusOK, 12*time.Millisecond) ObserveSync("feed", nil, 100*time.Millisecond) ObserveSync("feed", errors.New("boom"), 200*time.Millisecond) rec := httptest.NewRecorder() Handler().ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/metrics", nil)) if rec.Code != http.StatusOK { t.Fatalf("status=%d", rec.Code) } body := rec.Body.String() for _, want := range []string{ "http_requests_total{", `path="/healthz"`, "http_request_duration_seconds_bucket{", "sync_duration_seconds_count{", `kind="feed"`, "sync_failures_total{", `sync_failures_total{kind="feed"} 1`, } { if !strings.Contains(body, want) { t.Fatalf("missing %q in:\n%s", want, body) } } snap := Snapshot() if snap["http_requests_total"].(uint64) != 1 { t.Fatalf("snapshot http=%v", snap) } if snap["sync_failures_total"].(uint64) != 1 { t.Fatalf("snapshot sync fail=%v", snap) } if snap["sync_duration_seconds_count"].(uint64) != 2 { t.Fatalf("snapshot sync count=%v", snap) } } func TestMiddlewareRecordsRoutePattern(t *testing.T) { t.Cleanup(Reset) Reset() r := chi.NewRouter() r.Use(Middleware) r.Get("/healthz", func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusOK) }) r.Handle("/metrics", Handler()) rec := httptest.NewRecorder() r.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/healthz", nil)) if rec.Code != http.StatusOK { t.Fatalf("healthz status=%d", rec.Code) } rec = httptest.NewRecorder() r.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/metrics", nil)) body := rec.Body.String() if !strings.Contains(body, `path="/healthz"`) { t.Fatalf("expected route pattern in metrics:\n%s", body) } // /metrics itself should not inflate request series when skipped. if strings.Count(body, "http_requests_total{") > 1 { // one series line for healthz is expected; ensure metrics path absent } if strings.Contains(body, `path="/metrics"`) { t.Fatalf("/metrics should not self-instrument:\n%s", body) } } func TestGateAllowsNonProduction(t *testing.T) { t.Cleanup(Reset) Reset() h := Gate(false, false)(Handler()) req := httptest.NewRequest(http.MethodGet, "/metrics", nil) req.RemoteAddr = "203.0.113.9:9999" rec := httptest.NewRecorder() h.ServeHTTP(rec, req) if rec.Code != http.StatusOK { t.Fatalf("non-prod status=%d", rec.Code) } } func TestGateBlocksNonLoopbackInProduction(t *testing.T) { t.Cleanup(Reset) Reset() h := Gate(true, false)(Handler()) req := httptest.NewRequest(http.MethodGet, "/metrics", nil) req.RemoteAddr = "203.0.113.9:9999" rec := httptest.NewRecorder() h.ServeHTTP(rec, req) if rec.Code != http.StatusNotFound { t.Fatalf("prod remote status=%d want 404", rec.Code) } } func TestGateAllowsLoopbackInProduction(t *testing.T) { t.Cleanup(Reset) Reset() h := Gate(true, false)(Handler()) req := httptest.NewRequest(http.MethodGet, "/metrics", nil) req.RemoteAddr = "127.0.0.1:54321" rec := httptest.NewRecorder() h.ServeHTTP(rec, req) if rec.Code != http.StatusOK { t.Fatalf("prod loopback status=%d", rec.Code) } } func TestGateAllowsPublicFlagInProduction(t *testing.T) { t.Cleanup(Reset) Reset() h := Gate(true, true)(Handler()) req := httptest.NewRequest(http.MethodGet, "/metrics", nil) req.RemoteAddr = "203.0.113.9:9999" rec := httptest.NewRecorder() h.ServeHTTP(rec, req) if rec.Code != http.StatusOK { t.Fatalf("METRICS_PUBLIC status=%d", rec.Code) } } func TestGateBlocksSpoofedLoopbackXFFInProduction(t *testing.T) { t.Cleanup(Reset) Reset() h := Gate(true, false)(Handler()) req := httptest.NewRequest(http.MethodGet, "/metrics", nil) req.RemoteAddr = "127.0.0.1:54321" req.Header.Set("X-Forwarded-For", "127.0.0.1") rec := httptest.NewRecorder() h.ServeHTTP(rec, req) if rec.Code != http.StatusNotFound { t.Fatalf("spoofed XFF loopback status=%d want 404", rec.Code) } }