/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>
84 lines
3.0 KiB
Go
84 lines
3.0 KiB
Go
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"io"
|
|
"log/slog"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"sync/atomic"
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"github.com/mathiasbq/hyperguild/ingestion/internal/metrics"
|
|
)
|
|
|
|
// fakeIssuer answers OIDC discovery with 502 for the first fail requests, as
|
|
// ingress does while dex is not yet serving on a cold boot (#83), then with a
|
|
// real discovery document and an empty JWKS.
|
|
func fakeIssuer(t *testing.T, fail int32) (*httptest.Server, *atomic.Int32) {
|
|
t.Helper()
|
|
var discoveries atomic.Int32
|
|
mux := http.NewServeMux()
|
|
var srv *httptest.Server
|
|
mux.HandleFunc("/.well-known/openid-configuration", func(w http.ResponseWriter, _ *http.Request) {
|
|
if discoveries.Add(1) <= fail {
|
|
w.WriteHeader(http.StatusBadGateway)
|
|
return
|
|
}
|
|
w.Header().Set("Content-Type", "application/json")
|
|
_, _ = io.WriteString(w, `{"issuer":"`+srv.URL+`","jwks_uri":"`+srv.URL+`/keys"}`)
|
|
})
|
|
mux.HandleFunc("/keys", func(w http.ResponseWriter, _ *http.Request) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
_, _ = io.WriteString(w, `{"keys":[]}`)
|
|
})
|
|
srv = httptest.NewServer(mux)
|
|
t.Cleanup(srv.Close)
|
|
return srv, &discoveries
|
|
}
|
|
|
|
// #83 retried discovery at boot because chassis v0.2.0 failed construction on
|
|
// a 502. Since chassis v0.7 (mcp-chassis#4) the validator is built anyway and
|
|
// resolves the issuer lazily, so boot no longer waits or exits on it
|
|
// (hyperguild#155). An unresolved issuer is logged at boot.
|
|
func TestBuildJWTValidatorReturnsAValidatorWhileDiscovery502s(t *testing.T) {
|
|
srv, _ := fakeIssuer(t, 1000)
|
|
var buf bytes.Buffer
|
|
v, err := buildJWTValidator(context.Background(), slog.New(slog.NewTextHandler(&buf, nil)), srv.URL, "brain")
|
|
require.NoError(t, err)
|
|
assert.NotNil(t, v)
|
|
assert.Contains(t, buf.String(), "jwt issuer not resolved at boot")
|
|
}
|
|
|
|
func TestBuildJWTValidatorLogsNothingAgainstAHealthyIssuer(t *testing.T) {
|
|
srv, _ := fakeIssuer(t, 0)
|
|
var buf bytes.Buffer
|
|
v, err := buildJWTValidator(context.Background(), slog.New(slog.NewTextHandler(&buf, nil)), srv.URL, "brain")
|
|
require.NoError(t, err)
|
|
assert.NotNil(t, v)
|
|
assert.NotContains(t, buf.String(), "not resolved")
|
|
}
|
|
|
|
func TestBuildJWTValidatorIsNilWithoutAnIssuer(t *testing.T) {
|
|
v, err := buildJWTValidator(context.Background(), slog.New(slog.DiscardHandler), "", "brain")
|
|
require.NoError(t, err)
|
|
assert.Nil(t, v)
|
|
}
|
|
|
|
// With the issuer unresolved, a JWT on a REST route answers 503 (retry), not
|
|
// 401: the token may be fine, the server just cannot check it yet.
|
|
func TestBuildMuxAnswers503ToAJWTWhileTheIssuerIsUnresolved(t *testing.T) {
|
|
srv, _ := fakeIssuer(t, 1000)
|
|
v, err := buildJWTValidator(context.Background(), slog.New(slog.DiscardHandler), srv.URL, "brain")
|
|
require.NoError(t, err)
|
|
mux, err := buildMux(restAuth{token: routesTok, validator: v, realm: "brain"}, testHandler(t),
|
|
okHandler("mcp"), metrics.New(), nil)
|
|
require.NoError(t, err)
|
|
code, _ := do(t, mux, "GET", "/pending", "Bearer aaa.bbb.ccc")
|
|
assert.Equal(t, http.StatusServiceUnavailable, code)
|
|
}
|