// Copyright 2020-2025 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 package cachecleaner import ( "context" "testing" "github.com/stretchr/testify/require" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apiserver/pkg/authentication/authenticator" authenticationv1alpha1 "go.pinniped.dev/generated/latest/apis/concierge/authentication/v1alpha1" conciergefake "go.pinniped.dev/generated/latest/client/concierge/clientset/versioned/fake" conciergeinformers "go.pinniped.dev/generated/latest/client/concierge/informers/externalversions" controllerAuthenticator "go.pinniped.dev/internal/controller/authenticator" "go.pinniped.dev/internal/controller/authenticator/authncache" "go.pinniped.dev/internal/controllerlib" "go.pinniped.dev/internal/plog" "go.pinniped.dev/internal/testutil" ) func TestController(t *testing.T) { t.Parallel() testWebhookKey1 := authncache.Key{ APIGroup: "authentication.concierge.pinniped.dev", Kind: "WebhookAuthenticator", Name: "test-webhook-name-one", } testWebhookKey2 := authncache.Key{ APIGroup: "authentication.concierge.pinniped.dev", Kind: "WebhookAuthenticator", Name: "test-webhook-name-two", } testJWTAuthenticatorKey1 := authncache.Key{ APIGroup: "authentication.concierge.pinniped.dev", Kind: "JWTAuthenticator", Name: "test-jwt-authenticator-name-one", } testJWTAuthenticatorKey2 := authncache.Key{ APIGroup: "authentication.concierge.pinniped.dev", Kind: "JWTAuthenticator", Name: "test-jwt-authenticator-name-two", } testKeyUnknownType := authncache.Key{ APIGroup: "authentication.concierge.pinniped.dev", Kind: "SomeOtherAuthenticator", Name: "test-name-one", } tests := []struct { name string objects []runtime.Object initialCache func(t *testing.T, cache *authncache.Cache) wantErr string wantLogs []string wantCacheKeys []authncache.Key }{ { name: "no change", initialCache: func(t *testing.T, cache *authncache.Cache) { cache.Store(testWebhookKey1, nil) cache.Store(testJWTAuthenticatorKey1, nil) }, objects: []runtime.Object{ &authenticationv1alpha1.WebhookAuthenticator{ ObjectMeta: metav1.ObjectMeta{ Name: testWebhookKey1.Name, }, }, &authenticationv1alpha1.JWTAuthenticator{ ObjectMeta: metav1.ObjectMeta{ Name: testJWTAuthenticatorKey1.Name, }, }, }, wantCacheKeys: []authncache.Key{testWebhookKey1, testJWTAuthenticatorKey1}, }, { name: "authenticators not yet added", objects: []runtime.Object{ &authenticationv1alpha1.WebhookAuthenticator{ ObjectMeta: metav1.ObjectMeta{ Name: testWebhookKey1.Name, }, }, &authenticationv1alpha1.WebhookAuthenticator{ ObjectMeta: metav1.ObjectMeta{ Name: testWebhookKey2.Name, }, }, &authenticationv1alpha1.JWTAuthenticator{ ObjectMeta: metav1.ObjectMeta{ Name: testJWTAuthenticatorKey1.Name, }, }, &authenticationv1alpha1.JWTAuthenticator{ ObjectMeta: metav1.ObjectMeta{ Name: testJWTAuthenticatorKey2.Name, }, }, }, wantCacheKeys: []authncache.Key{}, }, { name: "successful cleanup", initialCache: func(t *testing.T, cache *authncache.Cache) { cache.Store(testWebhookKey1, nil) cache.Store(testWebhookKey2, nil) cache.Store(testJWTAuthenticatorKey1, newClosableCacheValue(t, false)) cache.Store(testJWTAuthenticatorKey2, newClosableCacheValue(t, true)) cache.Store(testKeyUnknownType, nil) }, objects: []runtime.Object{ &authenticationv1alpha1.WebhookAuthenticator{ ObjectMeta: metav1.ObjectMeta{ Name: testWebhookKey1.Name, }, }, &authenticationv1alpha1.JWTAuthenticator{ ObjectMeta: metav1.ObjectMeta{ Name: testJWTAuthenticatorKey1.Name, }, }, }, wantLogs: []string{ `{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","logger":"cachecleaner-controller","caller":"cachecleaner/cachecleaner.go:$cachecleaner.(*controller).Sync","message":"deleting authenticator from cache","authenticator":{"name":"test-jwt-authenticator-name-two"},"kind":"JWTAuthenticator"}`, `{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","logger":"cachecleaner-controller","caller":"cachecleaner/cachecleaner.go:$cachecleaner.(*controller).Sync","message":"deleting authenticator from cache","authenticator":{"name":"test-webhook-name-two"},"kind":"WebhookAuthenticator"}`, }, wantCacheKeys: []authncache.Key{testWebhookKey1, testJWTAuthenticatorKey1, testKeyUnknownType}, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { // When we have t.Parallel() here, this test blocks pretty consistently...y tho? //nolint:staticcheck // our codegen does not yet generate a NewClientset() function fakeClient := conciergefake.NewSimpleClientset(tt.objects...) informers := conciergeinformers.NewSharedInformerFactory(fakeClient, 0) cache := authncache.New() if tt.initialCache != nil { tt.initialCache(t, cache) } webhooks := informers.Authentication().V1alpha1().WebhookAuthenticators() jwtAuthenticators := informers.Authentication().V1alpha1().JWTAuthenticators() logger, log := plog.TestLogger(t) controller := New(cache, webhooks, jwtAuthenticators, logger) ctx, cancel := context.WithCancel(context.Background()) defer cancel() informers.Start(ctx.Done()) controllerlib.TestRunSynchronously(t, controller) syncCtx := controllerlib.Context{ Context: ctx, Key: controllerlib.Key{ Name: "test-webhook-name-one", }, } if err := controllerlib.TestSync(t, controller, syncCtx); tt.wantErr != "" { require.EqualError(t, err, tt.wantErr) } else { require.NoError(t, err) } require.ElementsMatch(t, tt.wantLogs, testutil.SplitByNewline(log.String())) require.ElementsMatch(t, tt.wantCacheKeys, cache.Keys()) }) } } type mockValue struct { wasClosed bool } func (m *mockValue) Close() { m.wasClosed = true } func (m *mockValue) AuthenticateToken(_ context.Context, _ string) (*authenticator.Response, bool, error) { panic("implement me") } var _ authncache.Value = (*mockValue)(nil) var _ controllerAuthenticator.Closer = (*mockValue)(nil) func newClosableCacheValue(t *testing.T, wantClose bool) authncache.Value { t.Helper() mock := &mockValue{} t.Cleanup(func() { require.Equal(t, wantClose, mock.wasClosed) }) return mock }