Files
pad/internal/server/metrics_auth_test.go
T
xarmian 0a1dbc8c73 perf(test): wire remaining store helpers onto storetest fixture (TASK-1915) (#789)
newPadServer (internal/mcp), testStoreOAuth (internal/oauth), and
newMetricsTestServer (internal/server) were left on the slow
per-call migration path after IDEA-1914/#788 wired testServer and
store's white-box testStore onto storetest.NewSQLite. Switch all
three, and add minimal TestMains to internal/mcp and internal/oauth
to release storetest's process-wide template DB, matching
internal/server's existing TestMain.
2026-07-03 12:17:03 -04:00

104 lines
3.5 KiB
Go

package server
import (
"net/http"
"net/http/httptest"
"testing"
"github.com/PerpetualSoftware/pad/internal/metrics"
"github.com/PerpetualSoftware/pad/internal/store/storetest"
)
// newMetricsTestServer builds a Server with metrics enabled and the given
// static token, via storetest.NewSQLite (IDEA-1914) — see testServer in
// server_test.go for why this is fast (once-per-binary migration chain).
func newMetricsTestServer(t *testing.T, token string) *Server {
t.Helper()
s := storetest.NewSQLite(t)
srv := New(s)
srv.SetMetrics(metrics.New())
srv.SetMetricsToken(token)
// storetest.NewSQLite already registers t.Cleanup(store.Close); since
// t.Cleanup runs LIFO, registering Stop() here afterward still drains
// background goroutines BEFORE the store closes — see testServer in
// server_test.go for the BUG-842 race details.
t.Cleanup(func() {
srv.Stop()
})
return srv
}
func doMetricsRequest(srv *Server, remoteAddr, authHeader string) *httptest.ResponseRecorder {
req := httptest.NewRequest("GET", "/metrics", nil)
req.RemoteAddr = remoteAddr
if authHeader != "" {
req.Header.Set("Authorization", authHeader)
}
rr := httptest.NewRecorder()
srv.ServeHTTP(rr, req)
return rr
}
// TestMetricsAuth_TokenUnset_LoopbackOnly: no PAD_METRICS_TOKEN → loopback
// callers succeed, everyone else gets 403. Safe default for self-hosters
// running Prometheus on the same box.
func TestMetricsAuth_TokenUnset_LoopbackOnly(t *testing.T) {
srv := newMetricsTestServer(t, "")
rr := doMetricsRequest(srv, "127.0.0.1:12345", "")
if rr.Code != http.StatusOK {
t.Fatalf("loopback /metrics: expected 200, got %d: %s", rr.Code, rr.Body.String())
}
rr = doMetricsRequest(srv, "203.0.113.5:12345", "")
if rr.Code != http.StatusForbidden {
t.Fatalf("non-loopback /metrics: expected 403 (no token configured), got %d: %s", rr.Code, rr.Body.String())
}
}
// TestMetricsAuth_TokenSet_RequiresBearer: with PAD_METRICS_TOKEN set,
// every scrape — even loopback — must present the correct Bearer token.
func TestMetricsAuth_TokenSet_RequiresBearer(t *testing.T) {
srv := newMetricsTestServer(t, "super-secret-token")
// Missing header → 401
rr := doMetricsRequest(srv, "127.0.0.1:1", "")
if rr.Code != http.StatusUnauthorized {
t.Fatalf("no auth header: expected 401, got %d", rr.Code)
}
// Wrong token → 401
rr = doMetricsRequest(srv, "127.0.0.1:1", "Bearer wrong-token")
if rr.Code != http.StatusUnauthorized {
t.Fatalf("wrong token: expected 401, got %d", rr.Code)
}
// Non-Bearer scheme → 401
rr = doMetricsRequest(srv, "127.0.0.1:1", "Basic dXNlcjpwYXNz")
if rr.Code != http.StatusUnauthorized {
t.Fatalf("basic auth: expected 401, got %d", rr.Code)
}
// Correct token — from anywhere — succeeds.
rr = doMetricsRequest(srv, "203.0.113.5:1", "Bearer super-secret-token")
if rr.Code != http.StatusOK {
t.Fatalf("valid token: expected 200, got %d: %s", rr.Code, rr.Body.String())
}
// WWW-Authenticate header is set for failed attempts.
rr = doMetricsRequest(srv, "127.0.0.1:1", "Bearer wrong")
if got := rr.Header().Get("WWW-Authenticate"); got == "" {
t.Error("expected WWW-Authenticate header on 401")
}
}
// TestMetricsAuth_NoMetricsMeans404 sanity-checks that /metrics is
// simply not routed when metrics are disabled (SetMetrics never called).
func TestMetricsAuth_NoMetricsMeans404(t *testing.T) {
srv := testServer(t) // no SetMetrics
rr := doMetricsRequest(srv, "127.0.0.1:1", "")
if rr.Code != http.StatusNotFound {
t.Fatalf("metrics disabled: expected 404, got %d", rr.Code)
}
}