package metrics import ( "context" "errors" "io" "net/http" "net/http/httptest" "strings" "sync/atomic" "testing" "github.com/prometheus/client_golang/prometheus" ) func TestHandlerServesMetricsAndIndependentProbes(t *testing.T) { t.Parallel() registry := prometheus.NewRegistry() gauge := prometheus.NewGauge(prometheus.GaugeOpts{Name: "proxy_pool_test_inventory"}) gauge.Set(7) registry.MustRegister(gauge) readiness := &recordingReadiness{} handler, err := NewHandler(Dependencies{Gatherer: registry, Readiness: readiness}) if err != nil { t.Fatalf("NewHandler() error = %v", err) } server := httptest.NewServer(handler) t.Cleanup(server.Close) assertProbe(t, http.MethodGet, server.URL+"/livez", http.StatusOK, "live") if readiness.calls.Load() != 0 { t.Fatalf("livez readiness calls = %d, want 0", readiness.calls.Load()) } assertProbe(t, http.MethodGet, server.URL+"/readyz", http.StatusOK, "ready") if readiness.calls.Load() != 1 { t.Fatalf("readyz readiness calls = %d, want 1", readiness.calls.Load()) } response, err := http.Get(server.URL + "/metrics") if err != nil { t.Fatalf("GET /metrics: %v", err) } defer response.Body.Close() body, err := io.ReadAll(response.Body) if err != nil { t.Fatalf("ReadAll(/metrics): %v", err) } if response.StatusCode != http.StatusOK || !strings.Contains(string(body), "proxy_pool_test_inventory 7") { t.Fatalf("GET /metrics = %d %q", response.StatusCode, body) } } func TestHandlerFailsReadyWithoutLeakingDependencyError(t *testing.T) { t.Parallel() const secret = "redis://user:secret@redis:6379" handler, err := NewHandler(Dependencies{ Gatherer: prometheus.NewRegistry(), Readiness: &recordingReadiness{err: errors.New(secret)}, }) if err != nil { t.Fatalf("NewHandler() error = %v", err) } recorder := httptest.NewRecorder() handler.ServeHTTP(recorder, httptest.NewRequest(http.MethodGet, "/readyz", nil)) if recorder.Code != http.StatusServiceUnavailable || strings.Contains(recorder.Body.String(), secret) { t.Fatalf("GET /readyz = %d %q", recorder.Code, recorder.Body.String()) } } func TestHandlerRejectsInvalidDependenciesMethodsAndPaths(t *testing.T) { t.Parallel() if _, err := NewHandler(Dependencies{}); !errors.Is(err, ErrInvalidDependencies) { t.Fatalf("NewHandler(empty) error = %v", err) } handler, err := NewHandler(Dependencies{ Gatherer: prometheus.NewRegistry(), Readiness: &recordingReadiness{}, }) if err != nil { t.Fatalf("NewHandler() error = %v", err) } recorder := httptest.NewRecorder() handler.ServeHTTP(recorder, httptest.NewRequest(http.MethodPost, "/livez", nil)) if recorder.Code != http.StatusMethodNotAllowed || recorder.Header().Get("Allow") != http.MethodGet { t.Fatalf("POST /livez = %d Allow=%q", recorder.Code, recorder.Header().Get("Allow")) } recorder = httptest.NewRecorder() handler.ServeHTTP(recorder, httptest.NewRequest(http.MethodGet, "/missing", nil)) if recorder.Code != http.StatusNotFound { t.Fatalf("GET /missing = %d", recorder.Code) } } type recordingReadiness struct { calls atomic.Int64 err error } func (readiness *recordingReadiness) Ready(context.Context) error { readiness.calls.Add(1) return readiness.err } func assertProbe(t *testing.T, method, target string, wantStatus int, wantBody string) { t.Helper() request, err := http.NewRequest(method, target, nil) if err != nil { t.Fatalf("NewRequest(): %v", err) } response, err := http.DefaultClient.Do(request) if err != nil { t.Fatalf("Do(%s): %v", target, err) } defer response.Body.Close() body, err := io.ReadAll(response.Body) if err != nil { t.Fatalf("ReadAll(%s): %v", target, err) } if response.StatusCode != wantStatus || strings.TrimSpace(string(body)) != wantBody { t.Fatalf("%s = %d %q", target, response.StatusCode, body) } }