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" ) // 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), nil) require.NoError(t, err) code, _ := do(t, mux, "GET", "/pending", "Bearer aaa.bbb.ccc") assert.Equal(t, http.StatusServiceUnavailable, code) }