Files
hyperguild/ingestion/cmd/server/routes_observability_test.go
T
mathiasandClaude Opus 5.5 474473c99f
CI / Lint / Test / Vet (pull_request) Successful in 54s
CI / Mirror to GitHub (pull_request) Skipped
fix(ingestion): bump mcp-chassis to v0.7.1; label refused REST requests by route
1. Rejections are logged. chassis v0.2.0 refused without a log line, so
   since #149 an unknown REST caller got silent 401s. v0.7.1 writes
   `mcp auth rejected` (reason, remote, token fingerprint, path) and
   `mcp auth accepted` per authenticated request. Tested through
   buildMux.

2. Fallout: since chassis v0.7 (mcp-chassis#4) NewJWTValidator no longer
   fails on OIDC discovery; the issuer resolves lazily and a JWT gets
   503-and-retry until it does. #83's boot retry loop therefore never
   retried, and its tests failed. buildJWTValidator now builds once and
   logs an issuer still unresolved at boot. Behaviour change: a
   misconfigured issuer no longer exits the process; JWTs get 503, the
   static bearer keeps working. internal/startupretry is now unused.

3. Metrics: authenticated REST requests were already labelled by route
   (the REST mux sets Pattern on the same *http.Request the middleware
   reads). Requests refused by auth never reached the REST mux and were
   labelled path="/". labelRESTPattern now sets the REST route's pattern
   before auth; unmatched paths keep "/", so the label set stays bounded.

Closes #155

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 09:38:41 +02:00

70 lines
2.8 KiB
Go

package main
import (
"bytes"
"log/slog"
"net/http"
"net/http/httptest"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/mathiasbq/hyperguild/ingestion/internal/metrics"
)
// hyperguild#155: since #149 every REST route sits behind BearerMiddleware,
// and chassis v0.2.0 refused without logging, so an unknown caller got
// silent 401s. The chassis now writes an `mcp auth rejected` line.
func TestBuildMuxLogsARESTAuthRejection(t *testing.T) {
var buf bytes.Buffer
prev := slog.Default()
slog.SetDefault(slog.New(slog.NewTextHandler(&buf, nil)))
t.Cleanup(func() { slog.SetDefault(prev) })
code, _ := do(t, testMux(t), "POST", "/query", "Bearer wrong")
require.Equal(t, http.StatusUnauthorized, code)
assert.Contains(t, buf.String(), "mcp auth rejected")
assert.Contains(t, buf.String(), "path=/query")
assert.NotContains(t, buf.String(), "Bearer wrong", "the credential itself is never logged")
}
// An authenticated REST request is labelled with the REST mux's pattern:
// the inner mux sets it on the same *http.Request the outer middleware reads.
func TestMetricsLabelAnAuthenticatedRESTRequestWithItsRoutePattern(t *testing.T) {
reg := metrics.New()
code, _ := do(t, reg.Middleware(testMux(t)), "GET", "/pending", "Bearer "+routesTok)
require.Equal(t, http.StatusOK, code)
rec := httptest.NewRecorder()
reg.Handler().ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/metrics", nil))
assert.Contains(t, rec.Body.String(), `path="GET /pending"`)
assert.NotContains(t, rec.Body.String(), `path="/"`)
}
// hyperguild#155: a request refused by REST auth never reaches the REST mux,
// so it was labelled path="/" and a 4xx on /query could not be told from one
// on /write. It is now labelled with the REST pattern it was addressed to.
func TestMetricsLabelARejectedRESTRequestWithItsRoutePattern(t *testing.T) {
reg := metrics.New()
code, _ := do(t, reg.Middleware(testMux(t)), "POST", "/query", "")
require.Equal(t, http.StatusUnauthorized, code)
rec := httptest.NewRecorder()
reg.Handler().ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/metrics", nil))
assert.Contains(t, rec.Body.String(), `path="POST /query",status="4xx"`)
}
// A path no REST route matches keeps the catch-all label, so an attacker
// probing random URLs cannot grow the series set.
func TestMetricsLabelARejectedUnknownPathWithTheCatchAll(t *testing.T) {
reg := metrics.New()
code, _ := do(t, reg.Middleware(testMux(t)), "POST", "/no-such-route-zq", "")
require.Equal(t, http.StatusUnauthorized, code)
rec := httptest.NewRecorder()
reg.Handler().ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/metrics", nil))
assert.Contains(t, rec.Body.String(), `path="/",status="4xx"`)
assert.NotContains(t, rec.Body.String(), "no-such-route-zq")
}