proxy-pool/internal/platform/metrics/handler_test.go
2026-07-30 12:30:42 +08:00

122 lines
3.7 KiB
Go

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