// Copyright 2023 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 package tokenclient import ( "bytes" "errors" "testing" "time" "github.com/stretchr/testify/require" authenticationv1 "k8s.io/api/authentication/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/client-go/kubernetes" "k8s.io/utils/clock" clocktesting "k8s.io/utils/clock/testing" "go.pinniped.dev/internal/plog" ) func TestNew(t *testing.T) { mockWhatToDoWithTokenFunc := *new(WhatToDoWithTokenFunc) mockClient := new(kubernetes.Clientset) mockTime := time.Now() mockClock := clocktesting.NewFakeClock(mockTime) var log bytes.Buffer testLogger := plog.TestLogger(t, &log) type args struct { namespace string serviceAccountName string k8sClient *kubernetes.Clientset whatToDoWithToken WhatToDoWithTokenFunc logger plog.Logger opts []Opt } tests := []struct { name string args args expected *TokenClient }{ { name: "defaults", args: args{ namespace: "namespace", serviceAccountName: "serviceAccountName", k8sClient: mockClient, whatToDoWithToken: mockWhatToDoWithTokenFunc, logger: testLogger, }, expected: &TokenClient{ namespace: "namespace", serviceAccountName: "serviceAccountName", k8sClient: mockClient, whatToDoWithToken: mockWhatToDoWithTokenFunc, expirationSeconds: 600, clock: clock.RealClock{}, logger: testLogger, }, }, { name: "with all opts", args: args{ namespace: "custom-namespace", serviceAccountName: "custom-serviceAccountName", k8sClient: mockClient, whatToDoWithToken: mockWhatToDoWithTokenFunc, logger: testLogger, opts: []Opt{ WithExpirationSeconds(777), withClock(mockClock), }, }, expected: &TokenClient{ namespace: "custom-namespace", serviceAccountName: "custom-serviceAccountName", k8sClient: mockClient, whatToDoWithToken: mockWhatToDoWithTokenFunc, expirationSeconds: 777, clock: mockClock, logger: testLogger, }, }, } for _, tt := range tests { tt := tt t.Run(tt.name, func(t *testing.T) { actual := New( tt.args.namespace, tt.args.serviceAccountName, tt.args.k8sClient, tt.args.whatToDoWithToken, tt.args.logger, tt.args.opts..., ) require.Equal(t, tt.expected, actual) }) } } // withClock should only be used for testing. func withClock(clock clock.Clock) Opt { return func(client *TokenClient) { client.clock = clock } } func TestFetchToken(t *testing.T) { mockTime := metav1.Now() type expected struct { tokenRequestStatus authenticationv1.TokenRequestStatus ttl metav1.Duration errMessage string } tests := []struct { name string expirationSeconds int64 howToFetchTokenFromAPIServer howToFetchTokenFromAPIServer expected expected }{ { name: "happy path", expirationSeconds: 555, howToFetchTokenFromAPIServer: func(_ *authenticationv1.TokenRequest) (*authenticationv1.TokenRequest, error) { tokenRequest := authenticationv1.TokenRequest{ Status: authenticationv1.TokenRequestStatus{ Token: "token value", ExpirationTimestamp: metav1.NewTime(mockTime.Add(25 * time.Minute)), }, } return &tokenRequest, nil }, expected: expected{ tokenRequestStatus: authenticationv1.TokenRequestStatus{ Token: "token value", ExpirationTimestamp: metav1.NewTime(mockTime.Add(25 * time.Minute)), }, ttl: metav1.Duration{ Duration: 25 * time.Minute, }, }, }, { name: "returns errors from howToFetchTokenFromAPIServer", expirationSeconds: 444, howToFetchTokenFromAPIServer: func(_ *authenticationv1.TokenRequest) (*authenticationv1.TokenRequest, error) { return nil, errors.New("has an error") }, expected: expected{ errMessage: "error creating token: has an error", }, }, { name: "errors when howToFetchTokenFromAPIServer returns nil", expirationSeconds: 333, howToFetchTokenFromAPIServer: func(_ *authenticationv1.TokenRequest) (*authenticationv1.TokenRequest, error) { return nil, nil }, expected: expected{ errMessage: "tokenRequest is nil after request", }, }, } for _, tt := range tests { tt := tt t.Run(tt.name, func(t *testing.T) { wrappedFunc := func(tokenRequest *authenticationv1.TokenRequest) (*authenticationv1.TokenRequest, error) { require.NotNil(t, tokenRequest) require.Equal(t, tt.expirationSeconds, *tokenRequest.Spec.ExpirationSeconds) require.Empty(t, tokenRequest.Spec.Audiences) require.Empty(t, tokenRequest.Spec.BoundObjectRef) return tt.howToFetchTokenFromAPIServer(tokenRequest) } mockClock := clocktesting.NewFakeClock(mockTime.Time) var log bytes.Buffer tokenClient := TokenClient{ expirationSeconds: tt.expirationSeconds, clock: mockClock, logger: plog.TestLogger(t, &log), } tokenRequestStatus, ttl, err := tokenClient.fetchToken( wrappedFunc, ) if tt.expected.errMessage != "" { require.ErrorContains(t, err, tt.expected.errMessage) } else { require.Equal(t, tt.expected.tokenRequestStatus, tokenRequestStatus) require.Equal(t, tt.expected.ttl, ttl) } }) } }