Files
hyperguild/ingestion/cmd/server/routes_observability_test.go
T
mathiasandClaude Opus 5.5 6d44e9dd43 fix(ingestion): build /mcp's bearer and the metrics wrap inside buildMux
/mcp's bearer was built in main.go and the buildMux tests used stubs, so
dropping it would have kept CI green. buildMux now takes the MCP handler
and wraps it with the same BearerMiddleware and credential as the REST
API. "/mcp" and "GET /metrics" left the public allowlist, so passing
either as a public route is refused. buildMux also registers GET
/metrics and returns the metrics-wrapped handler, so main passes its
result to ListenAndServe unchanged.

Flip-tested: registering /mcp unwrapped turns
TestBuildMuxRefusesMCPWithoutAValidToken red, and dropping the metrics
wrap turns the four metrics tests red.

Comments now claim only what is tested. The ListenAndServe line and the
symlink tripwire wiring in main (#154 comment) are still untested and
tracked in #182.

Closes #154

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 16:31:25 +02:00

89 lines
3.7 KiB
Go

package main
import (
"bytes"
"log/slog"
"net/http"
"net/http/httptest"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// wrongTokSentinel is a presented credential distinctive enough that any
// occurrence in a log line is the raw token, not a coincidence.
const wrongTokSentinel = "wrong-tok-sentinel-q7zx"
// 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, and the
// presented token appears nowhere in the log as a bare value (PR #181
// review: asserting on the "Bearer " header form could never fail, since
// the chassis logs fields, not headers).
func TestBuildMuxRejectionLogLineOmitsTheBarePresentedToken(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 "+wrongTokSentinel)
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(), wrongTokSentinel, "the rejected token is logged as a bare value")
assert.NotContains(t, buf.String(), routesTok, "the valid static token is logged")
}
// The `mcp auth accepted` line leaves out the valid static token.
func TestBuildMuxAcceptedLogLineOmitsTheBareStaticToken(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), "GET", "/pending", "Bearer "+routesTok)
require.Equal(t, http.StatusOK, code)
assert.Contains(t, buf.String(), "mcp auth accepted")
assert.NotContains(t, buf.String(), routesTok, "the accepted static token is logged as a bare value")
}
// 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) {
mux, reg := testMuxAndRegistry(t)
code, _ := do(t, mux, "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) {
mux, reg := testMuxAndRegistry(t)
code, _ := do(t, mux, "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) {
mux, reg := testMuxAndRegistry(t)
code, _ := do(t, mux, "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")
}