ContainerImage.Pinniped/test/integration/supervisor_oidc_client_test.go
Ryan Richard 34509e7430 Add more unit tests for dynamic clients and enhance token exchange
- Enhance the token exchange to check that the same client is used
  compared to the client used during the original authorization and
  token requests, and also check that the client has the token-exchange
  grant type allowed in its configuration.
- Reduce the minimum required bcrypt cost for OIDCClient secrets
  because 15 is too slow for real-life use, especially considering
  that every login and every refresh flow will require two client auths.
- In unit tests, use bcrypt hashes with a cost of 4, because bcrypt
  slows down by 13x when run with the race detector, and we run our
  tests with the race detector enabled, causing the tests to be
  unacceptably slow. The production code uses a higher minimum cost.
- Centralize all pre-computed bcrypt hashes used by unit tests to a
  single place. Also extract some other useful test helpers for
  unit tests related to OIDCClients.
- Add tons of unit tests for the token endpoint related to dynamic
  clients for authcode exchanges, token exchanges, and refreshes.
2022-07-20 13:55:56 -07:00

652 lines
24 KiB
Go

// Copyright 2022 the Pinniped contributors. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
package integration
import (
"context"
"fmt"
"sort"
"strings"
"testing"
"time"
"github.com/stretchr/testify/require"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
supervisorconfigv1alpha1 "go.pinniped.dev/generated/latest/apis/supervisor/config/v1alpha1"
"go.pinniped.dev/internal/oidcclientsecretstorage"
"go.pinniped.dev/internal/testutil"
"go.pinniped.dev/test/testlib"
)
func TestOIDCClientStaticValidation_Parallel(t *testing.T) {
env := testlib.IntegrationEnv(t)
adminClient := testlib.NewKubernetesClientset(t)
needsErrFix := testutil.KubeServerMinorVersionInBetweenInclusive(t, adminClient.Discovery(), 0, 23)
reallyOld := testutil.KubeServerMinorVersionInBetweenInclusive(t, adminClient.Discovery(), 0, 19)
noSets := testutil.KubeServerMinorVersionInBetweenInclusive(t, adminClient.Discovery(), 0, 17)
groupFix := strings.NewReplacer(".supervisor.pinniped.dev", ".supervisor."+env.APIGroupSuffix)
errFix := strings.NewReplacer(makeErrFix(reallyOld)...)
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
t.Cleanup(cancel)
namespaceClient := adminClient.CoreV1().Namespaces()
ns, err := namespaceClient.Create(ctx, &corev1.Namespace{
ObjectMeta: metav1.ObjectMeta{
GenerateName: "test-oidc-client-",
},
}, metav1.CreateOptions{})
require.NoError(t, err)
t.Cleanup(func() {
require.NoError(t, namespaceClient.Delete(ctx, ns.Name, metav1.DeleteOptions{}))
})
oidcClients := testlib.NewSupervisorClientset(t).ConfigV1alpha1().OIDCClients(ns.Name)
tests := []struct {
name string
client *supervisorconfigv1alpha1.OIDCClient
fixWant func(t *testing.T, err error, want string) string
wantErr string
skip bool
}{
{
name: "bad name",
client: &supervisorconfigv1alpha1.OIDCClient{
ObjectMeta: metav1.ObjectMeta{
Name: "panda",
},
Spec: supervisorconfigv1alpha1.OIDCClientSpec{
AllowedRedirectURIs: []supervisorconfigv1alpha1.RedirectURI{
"https://a",
},
AllowedGrantTypes: []supervisorconfigv1alpha1.GrantType{
"refresh_token",
},
AllowedScopes: []supervisorconfigv1alpha1.Scope{
"username",
},
},
},
wantErr: `OIDCClient.config.supervisor.pinniped.dev "panda" is invalid: metadata.name: Invalid value: "panda": metadata.name in body should match '^client\.oauth\.pinniped\.dev-'`,
},
{
name: "bad name but close",
client: &supervisorconfigv1alpha1.OIDCClient{
ObjectMeta: metav1.ObjectMeta{
Name: "client0oauth1pinniped2dev-regex",
},
Spec: supervisorconfigv1alpha1.OIDCClientSpec{
AllowedRedirectURIs: []supervisorconfigv1alpha1.RedirectURI{
"https://a",
},
AllowedGrantTypes: []supervisorconfigv1alpha1.GrantType{
"refresh_token",
},
AllowedScopes: []supervisorconfigv1alpha1.Scope{
"username",
},
},
},
wantErr: `OIDCClient.config.supervisor.pinniped.dev "client0oauth1pinniped2dev-regex" is invalid: metadata.name: Invalid value: "client0oauth1pinniped2dev-regex": metadata.name in body should match '^client\.oauth\.pinniped\.dev-'`,
},
{
name: "bad generate name",
client: &supervisorconfigv1alpha1.OIDCClient{
ObjectMeta: metav1.ObjectMeta{
GenerateName: "snorlax-",
},
Spec: supervisorconfigv1alpha1.OIDCClientSpec{
AllowedRedirectURIs: []supervisorconfigv1alpha1.RedirectURI{
"http://127.0.0.1/callback",
},
AllowedGrantTypes: []supervisorconfigv1alpha1.GrantType{
"refresh_token",
},
AllowedScopes: []supervisorconfigv1alpha1.Scope{
"username",
},
},
},
fixWant: func(t *testing.T, err error, want string) string {
require.Error(t, err)
gotErr := err.Error()
errPrefix := groupFix.Replace(`OIDCClient.config.supervisor.pinniped.dev "snorlax-`)
require.True(t, strings.HasPrefix(gotErr, errPrefix))
gotErr = strings.TrimPrefix(gotErr, errPrefix)
end := strings.Index(gotErr, `"`)
require.Equal(t, end, 5)
gotErr = gotErr[:end]
if reallyOld { // these servers do not show the actual invalid value
want = strings.Replace(want, `Invalid value: "snorlax-RAND"`, `Invalid value: ""`, 1)
}
return strings.Replace(want, "RAND", gotErr, 2)
},
wantErr: `OIDCClient.config.supervisor.pinniped.dev "snorlax-RAND" is invalid: metadata.name: Invalid value: "snorlax-RAND": metadata.name in body should match '^client\.oauth\.pinniped\.dev-'`,
},
{
name: "bad redirect uri",
client: &supervisorconfigv1alpha1.OIDCClient{
ObjectMeta: metav1.ObjectMeta{
Name: "client.oauth.pinniped.dev-hello",
},
Spec: supervisorconfigv1alpha1.OIDCClientSpec{
AllowedRedirectURIs: []supervisorconfigv1alpha1.RedirectURI{
"http://127.0.0.1/callback",
"oob",
"https://a",
},
AllowedGrantTypes: []supervisorconfigv1alpha1.GrantType{
"refresh_token",
},
AllowedScopes: []supervisorconfigv1alpha1.Scope{
"username",
},
},
},
wantErr: `OIDCClient.config.supervisor.pinniped.dev "client.oauth.pinniped.dev-hello" is invalid: spec.allowedRedirectURIs[1]: Invalid value: "oob": spec.allowedRedirectURIs[1] in body should match '^https://.+|^http://(127\.0\.0\.1|\[::1\])(:\d+)?/'`,
},
{
name: "bad grant type",
client: &supervisorconfigv1alpha1.OIDCClient{
ObjectMeta: metav1.ObjectMeta{
Name: "client.oauth.pinniped.dev-sky",
},
Spec: supervisorconfigv1alpha1.OIDCClientSpec{
AllowedRedirectURIs: []supervisorconfigv1alpha1.RedirectURI{
"http://127.0.0.1/callback",
},
AllowedGrantTypes: []supervisorconfigv1alpha1.GrantType{
"refresh_token",
"authorization_code",
"bird",
},
AllowedScopes: []supervisorconfigv1alpha1.Scope{
"username",
},
},
},
wantErr: `OIDCClient.config.supervisor.pinniped.dev "client.oauth.pinniped.dev-sky" is invalid: spec.allowedGrantTypes[2]: Unsupported value: "bird": supported values: "authorization_code", "refresh_token", "urn:ietf:params:oauth:grant-type:token-exchange"`,
},
{
name: "bad scope",
client: &supervisorconfigv1alpha1.OIDCClient{
ObjectMeta: metav1.ObjectMeta{
Name: "client.oauth.pinniped.dev-blue",
},
Spec: supervisorconfigv1alpha1.OIDCClientSpec{
AllowedRedirectURIs: []supervisorconfigv1alpha1.RedirectURI{
"http://127.0.0.1/callback",
},
AllowedGrantTypes: []supervisorconfigv1alpha1.GrantType{
"refresh_token",
},
AllowedScopes: []supervisorconfigv1alpha1.Scope{
"*",
"username",
},
},
},
wantErr: `OIDCClient.config.supervisor.pinniped.dev "client.oauth.pinniped.dev-blue" is invalid: spec.allowedScopes[0]: Unsupported value: "*": supported values: "openid", "offline_access", "username", "groups", "pinniped:request-audience"`,
},
{
name: "empty unset all",
client: &supervisorconfigv1alpha1.OIDCClient{},
wantErr: `OIDCClient.config.supervisor.pinniped.dev "" is invalid: [metadata.name: Required value: name or generateName is required, spec.allowedGrantTypes: Required value, spec.allowedRedirectURIs: Required value, spec.allowedScopes: Required value]`,
skip: reallyOld, // the error is both different and has unstable order on older servers
},
{
name: "empty uris",
client: &supervisorconfigv1alpha1.OIDCClient{
ObjectMeta: metav1.ObjectMeta{
Name: "client.oauth.pinniped.dev-green-1",
},
Spec: supervisorconfigv1alpha1.OIDCClientSpec{
AllowedRedirectURIs: []supervisorconfigv1alpha1.RedirectURI{},
AllowedGrantTypes: []supervisorconfigv1alpha1.GrantType{
"refresh_token",
},
AllowedScopes: []supervisorconfigv1alpha1.Scope{
"username",
},
},
},
wantErr: `OIDCClient.config.supervisor.pinniped.dev "client.oauth.pinniped.dev-green-1" is invalid: spec.allowedRedirectURIs: Invalid value: 0: spec.allowedRedirectURIs in body should have at least 1 items`,
},
{
name: "empty grants",
client: &supervisorconfigv1alpha1.OIDCClient{
ObjectMeta: metav1.ObjectMeta{
Name: "client.oauth.pinniped.dev-green-2",
},
Spec: supervisorconfigv1alpha1.OIDCClientSpec{
AllowedRedirectURIs: []supervisorconfigv1alpha1.RedirectURI{
"http://127.0.0.1/callback",
},
AllowedGrantTypes: []supervisorconfigv1alpha1.GrantType{},
AllowedScopes: []supervisorconfigv1alpha1.Scope{
"username",
},
},
},
wantErr: `OIDCClient.config.supervisor.pinniped.dev "client.oauth.pinniped.dev-green-2" is invalid: spec.allowedGrantTypes: Invalid value: 0: spec.allowedGrantTypes in body should have at least 1 items`,
},
{
name: "empty scopes",
client: &supervisorconfigv1alpha1.OIDCClient{
ObjectMeta: metav1.ObjectMeta{
Name: "client.oauth.pinniped.dev-green-3",
},
Spec: supervisorconfigv1alpha1.OIDCClientSpec{
AllowedRedirectURIs: []supervisorconfigv1alpha1.RedirectURI{
"http://127.0.0.1/callback",
},
AllowedGrantTypes: []supervisorconfigv1alpha1.GrantType{
"refresh_token",
},
AllowedScopes: []supervisorconfigv1alpha1.Scope{},
},
},
wantErr: `OIDCClient.config.supervisor.pinniped.dev "client.oauth.pinniped.dev-green-3" is invalid: spec.allowedScopes: Invalid value: 0: spec.allowedScopes in body should have at least 1 items`,
},
{
name: "duplicate uris",
client: &supervisorconfigv1alpha1.OIDCClient{
ObjectMeta: metav1.ObjectMeta{
Name: "client.oauth.pinniped.dev-red-1",
},
Spec: supervisorconfigv1alpha1.OIDCClientSpec{
AllowedRedirectURIs: []supervisorconfigv1alpha1.RedirectURI{
"http://127.0.0.1/callback",
"http://127.0.0.1/callback",
},
AllowedGrantTypes: []supervisorconfigv1alpha1.GrantType{
"refresh_token",
},
AllowedScopes: []supervisorconfigv1alpha1.Scope{
"username",
},
},
},
wantErr: `OIDCClient.config.supervisor.pinniped.dev "client.oauth.pinniped.dev-red-1" is invalid: spec.allowedRedirectURIs[1]: Duplicate value: "http://127.0.0.1/callback"`,
skip: noSets, // needs v1.18+ for x-kubernetes-list-type: set
},
{
name: "duplicate grants",
client: &supervisorconfigv1alpha1.OIDCClient{
ObjectMeta: metav1.ObjectMeta{
Name: "client.oauth.pinniped.dev-red-2",
},
Spec: supervisorconfigv1alpha1.OIDCClientSpec{
AllowedRedirectURIs: []supervisorconfigv1alpha1.RedirectURI{
"http://127.0.0.1/callback",
},
AllowedGrantTypes: []supervisorconfigv1alpha1.GrantType{
"refresh_token",
"refresh_token",
},
AllowedScopes: []supervisorconfigv1alpha1.Scope{
"username",
},
},
},
wantErr: `OIDCClient.config.supervisor.pinniped.dev "client.oauth.pinniped.dev-red-2" is invalid: spec.allowedGrantTypes[1]: Duplicate value: "refresh_token"`,
skip: noSets, // needs v1.18+ for x-kubernetes-list-type: set
},
{
name: "duplicate scopes",
client: &supervisorconfigv1alpha1.OIDCClient{
ObjectMeta: metav1.ObjectMeta{
Name: "client.oauth.pinniped.dev-red-3",
},
Spec: supervisorconfigv1alpha1.OIDCClientSpec{
AllowedRedirectURIs: []supervisorconfigv1alpha1.RedirectURI{
"http://127.0.0.1/callback",
},
AllowedGrantTypes: []supervisorconfigv1alpha1.GrantType{
"refresh_token",
},
AllowedScopes: []supervisorconfigv1alpha1.Scope{
"username",
"username",
},
},
},
wantErr: `OIDCClient.config.supervisor.pinniped.dev "client.oauth.pinniped.dev-red-3" is invalid: spec.allowedScopes[1]: Duplicate value: "username"`,
skip: noSets, // needs v1.18+ for x-kubernetes-list-type: set
},
{
name: "bad everything",
client: &supervisorconfigv1alpha1.OIDCClient{
ObjectMeta: metav1.ObjectMeta{
Name: "zone",
},
Spec: supervisorconfigv1alpha1.OIDCClientSpec{
AllowedRedirectURIs: []supervisorconfigv1alpha1.RedirectURI{
"of",
},
AllowedGrantTypes: []supervisorconfigv1alpha1.GrantType{
"the",
},
AllowedScopes: []supervisorconfigv1alpha1.Scope{
"enders",
},
},
},
fixWant: func(t *testing.T, err error, want string) string {
// sort the error causes and use that to rebuild a sorted error message
statusErr := &errors.StatusError{}
require.ErrorAs(t, err, &statusErr)
require.Len(t, statusErr.ErrStatus.Details.Causes, 4)
out := make([]string, 0, len(statusErr.ErrStatus.Details.Causes))
for _, cause := range statusErr.ErrStatus.Details.Causes {
cause := cause
out = append(out, fmt.Sprintf("%s: %s", cause.Field, cause.Message))
}
sort.Strings(out)
errPrefix := groupFix.Replace(`OIDCClient.config.supervisor.pinniped.dev "zone" is invalid: [`)
require.True(t, strings.HasPrefix(err.Error(), errPrefix))
require.Equal(t, err.Error(), statusErr.ErrStatus.Message)
statusErr.ErrStatus.Message = errPrefix + strings.Join(out, ", ") + "]"
return want // leave the wanted error unchanged
},
wantErr: `OIDCClient.config.supervisor.pinniped.dev "zone" is invalid: [metadata.name: Invalid value: "zone": metadata.name in body should match '^client\.oauth\.pinniped\.dev-', spec.allowedGrantTypes[0]: Unsupported value: "the": supported values: "authorization_code", "refresh_token", "urn:ietf:params:oauth:grant-type:token-exchange", spec.allowedRedirectURIs[0]: Invalid value: "of": spec.allowedRedirectURIs[0] in body should match '^https://.+|^http://(127\.0\.0\.1|\[::1\])(:\d+)?/', spec.allowedScopes[0]: Unsupported value: "enders": supported values: "openid", "offline_access", "username", "groups", "pinniped:request-audience"]`,
},
{
name: "everything valid",
client: &supervisorconfigv1alpha1.OIDCClient{
ObjectMeta: metav1.ObjectMeta{
Name: "client.oauth.pinniped.dev-lava",
},
Spec: supervisorconfigv1alpha1.OIDCClientSpec{
AllowedRedirectURIs: []supervisorconfigv1alpha1.RedirectURI{
"https://example.com",
"http://127.0.0.1/yoyo",
},
AllowedGrantTypes: []supervisorconfigv1alpha1.GrantType{
"authorization_code",
"refresh_token",
"urn:ietf:params:oauth:grant-type:token-exchange",
},
AllowedScopes: []supervisorconfigv1alpha1.Scope{
"openid",
"offline_access",
"username",
"groups",
"pinniped:request-audience",
},
},
},
wantErr: "",
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
if tt.skip {
t.Skip()
}
t.Parallel()
client, err := oidcClients.Create(ctx, tt.client, metav1.CreateOptions{})
want := tt.wantErr
if len(want) == 0 {
require.NoError(t, err)
// unset server generated fields
client.Namespace = ""
client.UID = ""
client.ResourceVersion = ""
client.ManagedFields = nil
client.CreationTimestamp = metav1.Time{}
client.Generation = 0
client.SelfLink = "" // nolint: staticcheck // old API servers still set this field
require.Equal(t, tt.client, client)
return
}
if tt.fixWant != nil {
want = tt.fixWant(t, err, want)
}
want = groupFix.Replace(want)
// old API servers have slightly different error messages
if needsErrFix && !strings.Contains(want, "Duplicate value:") {
want = errFix.Replace(want)
}
require.EqualError(t, err, want)
})
}
}
func makeErrFix(reallyOld bool) []string {
const total = 10 // should be enough indexes
out := make([]string, 0, total*6) // good enough allocation
// these servers do not show the actual index of where the error occurred
for i := 0; i < total; i++ {
idx := fmt.Sprintf("[%d]", i)
out = append(out, idx+":", ":")
out = append(out, idx+" ", " ")
}
if reallyOld {
// these servers display empty values differently
out = append(out, "0:", `"":`)
// these servers do not show the actual invalid value
for _, s := range []string{
"of",
"oob",
"zone",
"panda",
"client0oauth1pinniped2dev-regex",
} {
out = append(out,
fmt.Sprintf(`Invalid value: "%s"`, s),
`Invalid value: ""`)
}
}
return out
}
func TestOIDCClientControllerValidations_Parallel(t *testing.T) {
env := testlib.IntegrationEnv(t)
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
t.Cleanup(cancel)
secrets := testlib.NewKubernetesClientset(t).CoreV1().Secrets(env.SupervisorNamespace)
oidcClients := testlib.NewSupervisorClientset(t).ConfigV1alpha1().OIDCClients(env.SupervisorNamespace)
tests := []struct {
name string
client *supervisorconfigv1alpha1.OIDCClient
secret *corev1.Secret
wantPhase string
wantConditions []supervisorconfigv1alpha1.Condition
}{
{
name: "invalid AllowedGrantTypes and AllowedScopes (missing minimum required values), with no Secret",
client: &supervisorconfigv1alpha1.OIDCClient{
ObjectMeta: metav1.ObjectMeta{
GenerateName: "client.oauth.pinniped.dev-",
},
Spec: supervisorconfigv1alpha1.OIDCClientSpec{
AllowedRedirectURIs: []supervisorconfigv1alpha1.RedirectURI{"https://some-redirect-url.test.pinniped.dev/some/path"},
AllowedGrantTypes: []supervisorconfigv1alpha1.GrantType{"refresh_token"}, // needs to have authorization_code
AllowedScopes: []supervisorconfigv1alpha1.Scope{"username"}, // needs to have openid
},
},
wantPhase: "Error",
wantConditions: []supervisorconfigv1alpha1.Condition{
{
Type: "AllowedGrantTypesValid",
Status: "False",
Reason: "MissingRequiredValue",
Message: `"authorization_code" must always be included in "allowedGrantTypes"`,
},
{
Type: "AllowedScopesValid",
Status: "False",
Reason: "MissingRequiredValue",
Message: `"openid" must always be included in "allowedScopes"; "offline_access" must be included in "allowedScopes" when "refresh_token" is included in "allowedGrantTypes"`,
},
{
Type: "ClientSecretExists",
Status: "False",
Reason: "NoClientSecretFound",
Message: `no client secret found (no Secret storage found)`,
},
},
},
{
name: "minimal valid AllowedGrantTypes and AllowedScopes, with Secret that contains empty list of client secrets",
client: &supervisorconfigv1alpha1.OIDCClient{
ObjectMeta: metav1.ObjectMeta{
GenerateName: "client.oauth.pinniped.dev-",
},
Spec: supervisorconfigv1alpha1.OIDCClientSpec{
AllowedRedirectURIs: []supervisorconfigv1alpha1.RedirectURI{"https://some-redirect-url.test.pinniped.dev/some/path"},
AllowedGrantTypes: []supervisorconfigv1alpha1.GrantType{"authorization_code"},
AllowedScopes: []supervisorconfigv1alpha1.Scope{"openid"},
},
},
secret: testutil.OIDCClientSecretStorageSecretWithoutName(t, env.SupervisorNamespace, []string{}),
wantPhase: "Error",
wantConditions: []supervisorconfigv1alpha1.Condition{
{
Type: "AllowedGrantTypesValid",
Status: "True",
Reason: "Success",
Message: `"allowedGrantTypes" is valid`,
},
{
Type: "AllowedScopesValid",
Status: "True",
Reason: "Success",
Message: `"allowedScopes" is valid`,
},
{
Type: "ClientSecretExists",
Status: "False",
Reason: "NoClientSecretFound",
Message: `no client secret found (empty list in storage)`,
},
},
},
{
name: "happy path example with one client secret stored and all possible AllowedGrantTypes and AllowedScopes",
client: &supervisorconfigv1alpha1.OIDCClient{
ObjectMeta: metav1.ObjectMeta{
GenerateName: "client.oauth.pinniped.dev-",
},
Spec: supervisorconfigv1alpha1.OIDCClientSpec{
AllowedRedirectURIs: []supervisorconfigv1alpha1.RedirectURI{"https://some-redirect-url.test.pinniped.dev/some/path"},
AllowedGrantTypes: []supervisorconfigv1alpha1.GrantType{"authorization_code", "urn:ietf:params:oauth:grant-type:token-exchange", "refresh_token"},
AllowedScopes: []supervisorconfigv1alpha1.Scope{"openid", "offline_access", "pinniped:request-audience", "username", "groups"},
},
},
secret: testutil.OIDCClientSecretStorageSecretWithoutName(t, env.SupervisorNamespace, []string{testutil.HashedPassword1AtSupervisorMinCost}),
wantPhase: "Ready",
wantConditions: []supervisorconfigv1alpha1.Condition{
{
Type: "AllowedGrantTypesValid",
Status: "True",
Reason: "Success",
Message: `"allowedGrantTypes" is valid`,
},
{
Type: "AllowedScopesValid",
Status: "True",
Reason: "Success",
Message: `"allowedScopes" is valid`,
},
{
Type: "ClientSecretExists",
Status: "True",
Reason: "Success",
Message: `1 client secret(s) found`,
},
},
},
// Note: there are many more possible combinations of these settings, but they are covered by the controller's
// unit tests. This test ensures that everything is wired up correctly in regard to this controller, enough to
// allow the controller to work correctly.
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
client, err := oidcClients.Create(ctx, tt.client, metav1.CreateOptions{})
require.NoError(t, err)
t.Cleanup(func() {
t.Logf("cleaning up test OIDCClient %s/%s", client.Namespace, client.Name)
err := oidcClients.Delete(ctx, client.Name, metav1.DeleteOptions{})
require.NoError(t, err)
})
if tt.secret != nil {
// Force the Secret's name to match the client created above.
tt.secret.Name = oidcclientsecretstorage.New(nil, nil).GetName(client.UID)
secret, err := secrets.Create(ctx, tt.secret, metav1.CreateOptions{})
require.NoError(t, err)
t.Cleanup(func() {
t.Logf("cleaning up test Secret %s/%s", secret.Namespace, secret.Name)
err := secrets.Delete(ctx, secret.Name, metav1.DeleteOptions{})
require.NoError(t, err)
})
}
// Wait for the OIDCClient to enter the expected phase (or time out).
testlib.RequireEventuallyf(t, func(requireEventually *require.Assertions) {
var err error
updatedClient, err := oidcClients.Get(ctx, client.Name, metav1.GetOptions{})
requireEventually.NoErrorf(err, "error while getting OIDCClient %s/%s", client.Namespace, client.Name)
requireEventually.Equalf(supervisorconfigv1alpha1.OIDCClientPhase(tt.wantPhase), updatedClient.Status.Phase,
"OIDCClient is not in phase %s: %v", tt.wantPhase, testlib.Sdump(updatedClient))
}, 1*time.Minute, 2*time.Second, "expected the OIDCClient to go into phase %s", tt.wantPhase)
// Wait for the controller to converge to the expected Conditions list. It may take several passes of the
// controller running, since the Secret is created after the OIDCClient is created, potentially causing
// the controller to Sync at least twice.
testlib.RequireEventuallyf(t, func(requireEventually *require.Assertions) {
var err error
updatedClient, err := oidcClients.Get(ctx, client.Name, metav1.GetOptions{})
requireEventually.NoErrorf(err, "error while getting OIDCClient %s/%s", client.Namespace, client.Name)
// Note that the controller sorts the conditions by type name,
// so we can assume that ordering in the test expectations for this test.
requireEventually.Len(updatedClient.Status.Conditions, len(tt.wantConditions))
for i, want := range tt.wantConditions {
actual := updatedClient.Status.Conditions[i]
requireEventually.Equal(want.Type, actual.Type)
requireEventually.Equal(want.Status, actual.Status)
requireEventually.Equal(want.Reason, actual.Reason)
requireEventually.Equal(want.Message, actual.Message)
requireEventually.Equal(updatedClient.Generation, actual.ObservedGeneration)
requireEventually.NotEmpty(actual.LastTransitionTime)
}
}, 1*time.Minute, 2*time.Second, "expected the OIDCClient to to have conditions %v", tt.wantConditions)
})
}
}