From 58cb47cc06ed0651f32893d2a6034808d2b18ef3 Mon Sep 17 00:00:00 2001 From: Moritz Johner Date: Mon, 22 Jan 2024 09:35:09 +0100 Subject: [PATCH] chore: add tests for AWS/SM (#3057) Signed-off-by: Moritz Johner --- pkg/provider/aws/secretsmanager/fake/fake.go | 7 +- .../aws/secretsmanager/secretsmanager_test.go | 286 ++++++++++++++++++ 2 files changed, 291 insertions(+), 2 deletions(-) diff --git a/pkg/provider/aws/secretsmanager/fake/fake.go b/pkg/provider/aws/secretsmanager/fake/fake.go index 5bed4cdb4..21ca2135e 100644 --- a/pkg/provider/aws/secretsmanager/fake/fake.go +++ b/pkg/provider/aws/secretsmanager/fake/fake.go @@ -34,6 +34,7 @@ type Client struct { PutSecretValueWithContextFn PutSecretValueWithContextFn DescribeSecretWithContextFn DescribeSecretWithContextFn DeleteSecretWithContextFn DeleteSecretWithContextFn + ListSecretsFn ListSecretsFn } type CreateSecretWithContextFn func(aws.Context, *awssm.CreateSecretInput, ...request.Option) (*awssm.CreateSecretOutput, error) @@ -41,6 +42,7 @@ type GetSecretValueWithContextFn func(aws.Context, *awssm.GetSecretValueInput, . type PutSecretValueWithContextFn func(aws.Context, *awssm.PutSecretValueInput, ...request.Option) (*awssm.PutSecretValueOutput, error) type DescribeSecretWithContextFn func(aws.Context, *awssm.DescribeSecretInput, ...request.Option) (*awssm.DescribeSecretOutput, error) type DeleteSecretWithContextFn func(ctx aws.Context, input *awssm.DeleteSecretInput, opts ...request.Option) (*awssm.DeleteSecretOutput, error) +type ListSecretsFn func(ctx aws.Context, input *awssm.ListSecretsInput, opts ...request.Option) (*awssm.ListSecretsOutput, error) func (sm Client) CreateSecretWithContext(ctx aws.Context, input *awssm.CreateSecretInput, options ...request.Option) (*awssm.CreateSecretOutput, error) { return sm.CreateSecretWithContextFn(ctx, input, options...) @@ -60,6 +62,7 @@ func NewCreateSecretWithContextFn(output *awssm.CreateSecretOutput, err error, e return output, err } } + func (sm Client) DeleteSecretWithContext(ctx aws.Context, input *awssm.DeleteSecretInput, opts ...request.Option) (*awssm.DeleteSecretOutput, error) { return sm.DeleteSecretWithContextFn(ctx, input, opts...) } @@ -156,8 +159,8 @@ func (sm *Client) GetSecretValue(in *awssm.GetSecretValueInput) (*awssm.GetSecre return nil, fmt.Errorf("test case not found") } -func (sm *Client) ListSecrets(*awssm.ListSecretsInput) (*awssm.ListSecretsOutput, error) { - return nil, nil +func (sm *Client) ListSecrets(input *awssm.ListSecretsInput) (*awssm.ListSecretsOutput, error) { + return sm.ListSecretsFn(nil, input) } func (sm *Client) cacheKeyForInput(in *awssm.GetSecretValueInput) string { diff --git a/pkg/provider/aws/secretsmanager/secretsmanager_test.go b/pkg/provider/aws/secretsmanager/secretsmanager_test.go index fcf71a90a..fab097ac3 100644 --- a/pkg/provider/aws/secretsmanager/secretsmanager_test.go +++ b/pkg/provider/aws/secretsmanager/secretsmanager_test.go @@ -18,16 +18,22 @@ import ( "context" "errors" "fmt" + "reflect" "strings" "testing" "time" "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws/awserr" + "github.com/aws/aws-sdk-go/aws/credentials" + "github.com/aws/aws-sdk-go/aws/request" + "github.com/aws/aws-sdk-go/aws/session" awssm "github.com/aws/aws-sdk-go/service/secretsmanager" "github.com/google/go-cmp/cmp" + "github.com/stretchr/testify/assert" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/utils/ptr" esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1" fakesm "github.com/external-secrets/external-secrets/pkg/provider/aws/secretsmanager/fake" @@ -1025,3 +1031,283 @@ func getTagSlice() []*awssm.Tag { }, } } +func TestSecretsManagerGetAllSecrets(t *testing.T) { + ctx := context.Background() + + errBoom := errors.New("boom") + secretName := "my-secret" + secretVersion := "AWSCURRENT" + secretPath := "/path/to/secret" + secretValue := "secret value" + secretTags := map[string]string{ + "foo": "bar", + } + // Test cases + testCases := []struct { + name string + ref esv1beta1.ExternalSecretFind + + secretName string + secretVersion string + secretValue string + fetchError error + listSecretsFn func(ctx context.Context, input *awssm.ListSecretsInput, opts ...request.Option) (*awssm.ListSecretsOutput, error) + + expectedData map[string][]byte + expectedError string + }{ + { + name: "Matching secrets found", + ref: esv1beta1.ExternalSecretFind{ + Name: &esv1beta1.FindName{ + RegExp: secretName, + }, + Path: ptr.To(secretPath), + }, + secretName: secretName, + secretVersion: secretVersion, + secretValue: secretValue, + listSecretsFn: func(ctx context.Context, input *awssm.ListSecretsInput, opts ...request.Option) (*awssm.ListSecretsOutput, error) { + assert.Len(t, input.Filters, 1) + assert.Equal(t, "name", *input.Filters[0].Key) + assert.Equal(t, secretPath, *input.Filters[0].Values[0]) + return &awssm.ListSecretsOutput{ + SecretList: []*awssm.SecretListEntry{ + { + Name: ptr.To(secretName), + }, + }, + }, nil + }, + expectedData: map[string][]byte{ + secretName: []byte(secretValue), + }, + expectedError: "", + }, + { + name: "Error occurred while fetching secret value", + ref: esv1beta1.ExternalSecretFind{ + Name: &esv1beta1.FindName{ + RegExp: secretName, + }, + Path: ptr.To(secretPath), + }, + secretName: secretName, + secretVersion: secretVersion, + secretValue: secretValue, + fetchError: errBoom, + listSecretsFn: func(ctx context.Context, input *awssm.ListSecretsInput, opts ...request.Option) (*awssm.ListSecretsOutput, error) { + return &awssm.ListSecretsOutput{ + SecretList: []*awssm.SecretListEntry{ + { + Name: ptr.To(secretName), + }, + }, + }, nil + }, + expectedData: nil, + expectedError: errBoom.Error(), + }, + { + name: "regexp: error occurred while listing secrets", + ref: esv1beta1.ExternalSecretFind{ + Name: &esv1beta1.FindName{ + RegExp: secretName, + }, + }, + listSecretsFn: func(ctx context.Context, input *awssm.ListSecretsInput, opts ...request.Option) (*awssm.ListSecretsOutput, error) { + return nil, errBoom + }, + expectedData: nil, + expectedError: errBoom.Error(), + }, + { + name: "regep: no matching secrets found", + ref: esv1beta1.ExternalSecretFind{ + Name: &esv1beta1.FindName{ + RegExp: secretName, + }, + }, + listSecretsFn: func(ctx context.Context, input *awssm.ListSecretsInput, opts ...request.Option) (*awssm.ListSecretsOutput, error) { + return &awssm.ListSecretsOutput{ + SecretList: []*awssm.SecretListEntry{ + { + Name: ptr.To("other-secret"), + }, + }, + }, nil + }, + expectedData: make(map[string][]byte), + expectedError: "", + }, + { + name: "invalid regexp", + ref: esv1beta1.ExternalSecretFind{ + Name: &esv1beta1.FindName{ + RegExp: "[", + }, + }, + expectedData: nil, + expectedError: "could not compile find.name.regexp [[]: error parsing regexp: missing closing ]: `[`", + }, + + { + name: "tags: Matching secrets found", + ref: esv1beta1.ExternalSecretFind{ + Tags: secretTags, + }, + secretName: secretName, + secretVersion: secretVersion, + secretValue: secretValue, + listSecretsFn: func(ctx context.Context, input *awssm.ListSecretsInput, opts ...request.Option) (*awssm.ListSecretsOutput, error) { + assert.Len(t, input.Filters, 2) + assert.Equal(t, "tag-key", *input.Filters[0].Key) + assert.Equal(t, "foo", *input.Filters[0].Values[0]) + assert.Equal(t, "tag-value", *input.Filters[1].Key) + assert.Equal(t, "bar", *input.Filters[1].Values[0]) + return &awssm.ListSecretsOutput{ + SecretList: []*awssm.SecretListEntry{ + { + Name: ptr.To(secretName), + }, + }, + }, nil + }, + expectedData: map[string][]byte{ + secretName: []byte(secretValue), + }, + expectedError: "", + }, + { + name: "tags: error occurred while fetching secret value", + ref: esv1beta1.ExternalSecretFind{ + Tags: secretTags, + }, + secretName: secretName, + secretVersion: secretVersion, + secretValue: secretValue, + fetchError: errBoom, + listSecretsFn: func(ctx context.Context, input *awssm.ListSecretsInput, opts ...request.Option) (*awssm.ListSecretsOutput, error) { + return &awssm.ListSecretsOutput{ + SecretList: []*awssm.SecretListEntry{ + { + Name: ptr.To(secretName), + }, + }, + }, nil + }, + expectedData: nil, + expectedError: errBoom.Error(), + }, + { + name: "tags: error occurred while listing secrets", + ref: esv1beta1.ExternalSecretFind{ + Tags: secretTags, + }, + listSecretsFn: func(ctx context.Context, input *awssm.ListSecretsInput, opts ...request.Option) (*awssm.ListSecretsOutput, error) { + return nil, errBoom + }, + expectedData: nil, + expectedError: errBoom.Error(), + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + fc := fakesm.NewClient() + fc.ListSecretsFn = tc.listSecretsFn + fc.WithValue(&awssm.GetSecretValueInput{ + SecretId: ptr.To(tc.secretName), + VersionStage: ptr.To(tc.secretVersion), + }, &awssm.GetSecretValueOutput{ + Name: ptr.To(tc.secretName), + VersionStages: []*string{ptr.To(tc.secretVersion)}, + SecretBinary: []byte(tc.secretValue), + }, tc.fetchError) + sm := SecretsManager{ + client: fc, + cache: make(map[string]*awssm.GetSecretValueOutput), + } + data, err := sm.GetAllSecrets(ctx, tc.ref) + if err != nil && err.Error() != tc.expectedError { + t.Errorf("unexpected error: got %v, want %v", err, tc.expectedError) + } + if !reflect.DeepEqual(data, tc.expectedData) { + t.Errorf("unexpected data: got %v, want %v", data, tc.expectedData) + } + }) + } +} + +func TestSecretsManagerValidate(t *testing.T) { + type fields struct { + sess *session.Session + referentAuth bool + } + validSession, _ := session.NewSession(aws.NewConfig().WithCredentials(credentials.NewStaticCredentials("fake", "fake", "fake"))) + invalidSession, _ := session.NewSession(aws.NewConfig().WithCredentials(credentials.NewCredentials(&FakeCredProvider{ + retrieveFunc: func() (credentials.Value, error) { + return credentials.Value{}, errors.New("invalid credentials") + }, + }))) + tests := []struct { + name string + fields fields + want esv1beta1.ValidationResult + wantErr bool + }{ + { + name: "ReferentAuth should always return unknown", + fields: fields{ + referentAuth: true, + }, + want: esv1beta1.ValidationResultUnknown, + }, + { + name: "Valid credentials should return ready", + fields: fields{ + sess: validSession, + }, + want: esv1beta1.ValidationResultReady, + }, + { + name: "Invalid credentials should return error", + fields: fields{ + sess: invalidSession, + }, + want: esv1beta1.ValidationResultError, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + sm := &SecretsManager{ + sess: tt.fields.sess, + referentAuth: tt.fields.referentAuth, + } + got, err := sm.Validate() + if (err != nil) != tt.wantErr { + t.Errorf("SecretsManager.Validate() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("SecretsManager.Validate() = %v, want %v", got, tt.want) + } + }) + } +} + +// FakeCredProvider implements the AWS credentials.Provider interface +// It is used to inject an error into the AWS session to cause a +// validation error. +type FakeCredProvider struct { + retrieveFunc func() (credentials.Value, error) +} + +func (f *FakeCredProvider) Retrieve() (credentials.Value, error) { + return f.retrieveFunc() +} + +func (f *FakeCredProvider) IsExpired() bool { + return true +}