Files
descrybe/apps/api/internal/metrics/metrics_test.go
T
greeneclipse 8580c996c3 Initial commit of Descrybe v2 without local scratch artifacts.
Drop one-shot tmp/axe scripts and agent i18n scratch so the Gitea tree is deployable.
2026-08-09 22:47:43 +02:00

137 lines
3.6 KiB
Go

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)
}
}