Closes #155
Reviewed by pr-gatekeeper: #181 (comment) (block, round 1) and re-review at e05a13a (approve).
Co-authored-by: Mathias <mthbqv@gmail.com>
81 lines
2.9 KiB
Go
81 lines
2.9 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"
|
|
)
|
|
|
|
// 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)
|
|
}
|