package main import ( "context" "io" "log/slog" "net/http" "net/http/httptest" "sync/atomic" "testing" "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/mathiasbq/hyperguild/ingestion/internal/startupretry" ) // 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 } var fastPolicy = startupretry.Policy{Attempts: 8, Base: time.Millisecond, Max: 5 * time.Millisecond} func TestBuildJWTValidatorRetriesDiscovery502sThenSucceeds(t *testing.T) { srv, discoveries := fakeIssuer(t, 3) ctx, cancel := context.WithCancel(context.Background()) defer cancel() v, err := buildJWTValidator(ctx, slog.New(slog.DiscardHandler), srv.URL, "brain", fastPolicy) require.NoError(t, err) assert.NotNil(t, v) assert.Equal(t, int32(4), discoveries.Load(), "three 502s, then the successful discovery") } func TestBuildJWTValidatorGivesUpAfterTheBudgetWithTheLastError(t *testing.T) { srv, discoveries := fakeIssuer(t, 1000) ctx, cancel := context.WithCancel(context.Background()) defer cancel() _, err := buildJWTValidator(ctx, slog.New(slog.DiscardHandler), srv.URL, "brain", fastPolicy) require.Error(t, err) assert.Contains(t, err.Error(), "status 502") assert.Equal(t, int32(8), discoveries.Load(), "bounded: exactly the attempt budget") } func TestBuildJWTValidatorStopsRetryingWhenTheContextIsCancelled(t *testing.T) { srv, _ := fakeIssuer(t, 1000) ctx, cancel := context.WithCancel(context.Background()) slow := startupretry.Policy{Attempts: 8, Base: time.Hour, Max: time.Hour} done := make(chan error, 1) go func() { _, err := buildJWTValidator(ctx, slog.New(slog.DiscardHandler), srv.URL, "brain", slow) done <- err }() time.Sleep(50 * time.Millisecond) cancel() select { case err := <-done: assert.ErrorIs(t, err, context.Canceled) case <-time.After(3 * time.Second): t.Fatal("did not return after the context was cancelled") } }