// Copyright 2020-2021 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 package webhookcachefiller import ( "context" "encoding/base64" "fmt" "io/ioutil" "net/http" "os" "testing" "time" "github.com/stretchr/testify/require" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/client-go/tools/clientcmd" clientcmdapi "k8s.io/client-go/tools/clientcmd/api" auth1alpha1 "go.pinniped.dev/generated/1.20/apis/concierge/authentication/v1alpha1" pinnipedfake "go.pinniped.dev/generated/1.20/client/concierge/clientset/versioned/fake" pinnipedinformers "go.pinniped.dev/generated/1.20/client/concierge/informers/externalversions" "go.pinniped.dev/internal/controller/authenticator/authncache" "go.pinniped.dev/internal/controllerlib" "go.pinniped.dev/internal/testutil" "go.pinniped.dev/internal/testutil/testlogger" ) func TestController(t *testing.T) { t.Parallel() tests := []struct { name string syncKey controllerlib.Key webhooks []runtime.Object wantErr string wantLogs []string wantCacheEntries int }{ { name: "not found", syncKey: controllerlib.Key{Namespace: "test-namespace", Name: "test-name"}, wantLogs: []string{ `webhookcachefiller-controller "level"=0 "msg"="Sync() found that the WebhookAuthenticator does not exist yet or was deleted"`, }, }, { name: "invalid webhook", syncKey: controllerlib.Key{Namespace: "test-namespace", Name: "test-name"}, webhooks: []runtime.Object{ &auth1alpha1.WebhookAuthenticator{ ObjectMeta: metav1.ObjectMeta{ Namespace: "test-namespace", Name: "test-name", }, Spec: auth1alpha1.WebhookAuthenticatorSpec{ Endpoint: "invalid url", }, }, }, wantErr: `failed to build webhook config: parse "http://invalid url": invalid character " " in host name`, }, { name: "valid webhook", syncKey: controllerlib.Key{Namespace: "test-namespace", Name: "test-name"}, webhooks: []runtime.Object{ &auth1alpha1.WebhookAuthenticator{ ObjectMeta: metav1.ObjectMeta{ Namespace: "test-namespace", Name: "test-name", }, Spec: auth1alpha1.WebhookAuthenticatorSpec{ Endpoint: "https://example.com", TLS: &auth1alpha1.TLSSpec{CertificateAuthorityData: ""}, }, }, }, wantLogs: []string{ `webhookcachefiller-controller "level"=0 "msg"="added new webhook authenticator" "endpoint"="https://example.com" "webhook"={"name":"test-name","namespace":"test-namespace"}`, }, wantCacheEntries: 1, }, } for _, tt := range tests { tt := tt t.Run(tt.name, func(t *testing.T) { t.Parallel() fakeClient := pinnipedfake.NewSimpleClientset(tt.webhooks...) informers := pinnipedinformers.NewSharedInformerFactory(fakeClient, 0) cache := authncache.New("pinniped.dev") testLog := testlogger.New(t) controller := New(cache, informers.Authentication().V1alpha1().WebhookAuthenticators(), testLog) ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) defer cancel() informers.Start(ctx.Done()) controllerlib.TestRunSynchronously(t, controller) syncCtx := controllerlib.Context{Context: ctx, Key: tt.syncKey} if err := controllerlib.TestSync(t, controller, syncCtx); tt.wantErr != "" { require.EqualError(t, err, tt.wantErr) } else { require.NoError(t, err) } require.Equal(t, tt.wantLogs, testLog.Lines()) require.Equal(t, tt.wantCacheEntries, len(cache.Keys())) }) } } func TestNewWebhookAuthenticator(t *testing.T) { t.Run("temp file failure", func(t *testing.T) { brokenTempFile := func(_ string, _ string) (*os.File, error) { return nil, fmt.Errorf("some temp file error") } res, err := newWebhookAuthenticator(nil, brokenTempFile, clientcmd.WriteToFile) require.Nil(t, res) require.EqualError(t, err, "unable to create temporary file: some temp file error") }) t.Run("marshal failure", func(t *testing.T) { marshalError := func(_ clientcmdapi.Config, _ string) error { return fmt.Errorf("some marshal error") } res, err := newWebhookAuthenticator(&auth1alpha1.WebhookAuthenticatorSpec{}, ioutil.TempFile, marshalError) require.Nil(t, res) require.EqualError(t, err, "unable to marshal kubeconfig: some marshal error") }) t.Run("invalid base64", func(t *testing.T) { res, err := newWebhookAuthenticator(&auth1alpha1.WebhookAuthenticatorSpec{ Endpoint: "https://example.com", TLS: &auth1alpha1.TLSSpec{CertificateAuthorityData: "invalid-base64"}, }, ioutil.TempFile, clientcmd.WriteToFile) require.Nil(t, res) require.EqualError(t, err, "invalid TLS configuration: illegal base64 data at input byte 7") }) t.Run("valid config with no TLS spec", func(t *testing.T) { res, err := newWebhookAuthenticator(&auth1alpha1.WebhookAuthenticatorSpec{ Endpoint: "https://example.com", }, ioutil.TempFile, clientcmd.WriteToFile) require.NotNil(t, res) require.NoError(t, err) }) t.Run("success", func(t *testing.T) { caBundle, url := testutil.TLSTestServer(t, func(w http.ResponseWriter, r *http.Request) { body, err := ioutil.ReadAll(r.Body) require.NoError(t, err) require.Contains(t, string(body), "test-token") _, err = w.Write([]byte(`{}`)) require.NoError(t, err) }) spec := &auth1alpha1.WebhookAuthenticatorSpec{ Endpoint: url, TLS: &auth1alpha1.TLSSpec{ CertificateAuthorityData: base64.StdEncoding.EncodeToString([]byte(caBundle)), }, } res, err := newWebhookAuthenticator(spec, ioutil.TempFile, clientcmd.WriteToFile) require.NoError(t, err) require.NotNil(t, res) resp, authenticated, err := res.AuthenticateToken(context.Background(), "test-token") require.NoError(t, err) require.Nil(t, resp) require.False(t, authenticated) }) }