/* Copyright © The ESO Authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at https://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package chef import ( "bytes" "context" "errors" "fmt" "strings" "testing" "time" "github.com/go-chef/chef" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" clientfake "sigs.k8s.io/controller-runtime/pkg/client/fake" esv1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1" v1 "github.com/external-secrets/external-secrets/apis/meta/v1" fake "github.com/external-secrets/external-secrets/providers/v1/chef/fake" "github.com/external-secrets/external-secrets/runtime/esutils" ) const ( name = "chef-demo-user" baseURL = "https://chef.cloudant.com/organizations/myorg/" noEndSlashInvalidBaseURL = "no end slash invalid base URL" baseInvalidURL = "invalid base URL/" authName = "chef-demo-auth-name" authKey = "chef-demo-auth-key" authNamespace = "chef-demo-auth-namespace" kind = "SecretStore" apiversion = "external-secrets.io/v1" databagName = "databag01" ) type chefTestCase struct { mockClient *fake.ChefMockClient databagName string databagItemName string property string ref *esv1.ExternalSecretDataRemoteRef apiErr error expectError string expectedData map[string][]byte expectedByte []byte } type ValidateStoreTestCase struct { store *esv1.SecretStore err error } // type storeModifier func(*esv1.SecretStore) *esv1.SecretStore func makeValidChefTestCase() *chefTestCase { smtc := chefTestCase{ mockClient: &fake.ChefMockClient{}, databagName: "databag01", databagItemName: "item01", property: "", apiErr: nil, expectError: "", expectedData: map[string][]byte{"item01": []byte(`"https://chef.com/organizations/dev/data/databag01/item01"`)}, expectedByte: []byte(`{"item01":"{\"id\":\"databag01-item01\",\"some_key\":\"fe7f29ede349519a1\",\"some_password\":\"dolphin_123zc\",\"some_username\":\"testuser\"}"}`), } smtc.ref = makeValidRef(smtc.databagName, smtc.databagItemName, smtc.property) smtc.mockClient.WithListItems(smtc.databagName, smtc.apiErr) smtc.mockClient.WithItem(smtc.databagName, smtc.databagItemName, smtc.apiErr) return &smtc } func makeInValidChefTestCase() *chefTestCase { smtc := chefTestCase{ mockClient: &fake.ChefMockClient{}, databagName: "databag01", databagItemName: "item03", property: "", apiErr: errors.New("unable to convert databagItem into JSON"), expectError: "unable to convert databagItem into JSON", expectedData: nil, expectedByte: nil, } smtc.ref = makeValidRef(smtc.databagName, smtc.databagItemName, smtc.property) smtc.mockClient.WithListItems(smtc.databagName, smtc.apiErr) smtc.mockClient.WithItem(smtc.databagName, smtc.databagItemName, smtc.apiErr) return &smtc } func makeValidRef(databag, dataitem, property string) *esv1.ExternalSecretDataRemoteRef { return &esv1.ExternalSecretDataRemoteRef{ Key: databag + "/" + dataitem, Property: property, } } func makeinValidRef() *esv1.ExternalSecretDataRemoteRef { return &esv1.ExternalSecretDataRemoteRef{ Key: "", } } func makeValidRefForGetSecretMap(databag string) *esv1.ExternalSecretDataRemoteRef { return &esv1.ExternalSecretDataRemoteRef{ Key: databag, } } func makeValidChefTestCaseCustom(tweaks ...func(smtc *chefTestCase)) *chefTestCase { smtc := makeValidChefTestCase() for _, fn := range tweaks { fn(smtc) } return smtc } func TestChefGetSecret(t *testing.T) { nilClient := func(smtc *chefTestCase) { smtc.mockClient = nil smtc.expectedByte = nil smtc.expectError = "chef provider is not initialized" } invalidDatabagName := func(smtc *chefTestCase) { smtc.databagName = "databag02" smtc.expectedByte = nil smtc.ref = makeinValidRef() smtc.expectError = "invalid key format in data section. Expected value 'databagName/databagItemName'" } invalidDatabagItemName := func(smtc *chefTestCase) { smtc.expectError = "data bag item item02 not found in data bag databag01" smtc.databagName = databagName smtc.databagItemName = "item02" smtc.expectedByte = nil smtc.ref = makeValidRef(smtc.databagName, smtc.databagItemName, "") } noProperty := func(smtc *chefTestCase) { smtc.expectError = "property findProperty not found in data bag item" smtc.databagName = databagName smtc.databagItemName = "item01" smtc.expectedByte = nil smtc.ref = makeValidRef(smtc.databagName, smtc.databagItemName, "findProperty") } withProperty := func(smtc *chefTestCase) { smtc.expectedByte = []byte("foundProperty") smtc.databagName = "databag03" smtc.databagItemName = "item03" smtc.ref = makeValidRef(smtc.databagName, smtc.databagItemName, "findProperty") } successCases := []*chefTestCase{ makeValidChefTestCase(), makeValidChefTestCaseCustom(nilClient), makeValidChefTestCaseCustom(invalidDatabagName), makeValidChefTestCaseCustom(invalidDatabagItemName), makeValidChefTestCaseCustom(noProperty), makeValidChefTestCaseCustom(withProperty), makeInValidChefTestCase(), } sm := Providerchef{ databagService: &chef.DataBagService{}, } ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() for k, v := range successCases { sm.databagService = v.mockClient out, err := sm.GetSecret(ctx, *v.ref) if err != nil && !esutils.ErrorContains(err, v.expectError) { t.Errorf("[case %d] expected error: %v, got: %v", k, v.expectError, err) } else if v.expectError != "" && err == nil { t.Errorf("[case %d] expected error: %v, got: nil", k, v.expectError) } if !bytes.Equal(out, v.expectedByte) { t.Errorf("[case %d] expected secret: %s, got: %s", k, v.expectedByte, out) } } } func TestChefGetSecretMap(t *testing.T) { nilClient := func(smtc *chefTestCase) { smtc.mockClient = nil smtc.expectedByte = nil smtc.expectError = "chef provider is not initialized" } databagHasSlash := func(smtc *chefTestCase) { smtc.expectedByte = nil smtc.ref = makeinValidRef() smtc.ref.Key = "data/Bag02" smtc.expectError = "invalid key format in dataForm section. Expected only 'databagName'" } withProperty := func(smtc *chefTestCase) { smtc.expectedByte = []byte(`{"item01":"{\"id\":\"databag01-item01\",\"some_key\":\"fe7f29ede349519a1\",\"some_password\":\"dolphin_123zc\",\"some_username\":\"testuser\"}"}`) smtc.databagName = databagName smtc.ref = makeValidRefForGetSecretMap(smtc.databagName) } withProperty2 := func(smtc *chefTestCase) { smtc.expectError = "unable to list items in data bag 123, may be given data bag doesn't exists or it is empty" smtc.expectedByte = nil smtc.databagName = "123" smtc.ref = makeValidRefForGetSecretMap(smtc.databagName) } successCases := []*chefTestCase{ makeValidChefTestCaseCustom(nilClient), makeValidChefTestCaseCustom(databagHasSlash), makeValidChefTestCaseCustom(withProperty), makeValidChefTestCaseCustom(withProperty2), } pc := Providerchef{ databagService: &chef.DataBagService{}, } ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() for k, v := range successCases { pc.databagService = v.mockClient out, err := pc.GetSecretMap(ctx, *v.ref) if err != nil && !esutils.ErrorContains(err, v.expectError) { t.Errorf("[case %d] expected error: %v, got: %v", k, v.expectError, err) } else if v.expectError != "" && err == nil { t.Errorf("[case %d] expected error: %v, got: nil", k, v.expectError) } if !bytes.Equal(out["item01"], v.expectedByte) { t.Errorf("[case %d] unexpected secret: expected %s, got %s", k, v.expectedByte, out) } } } func makeSecretStore(name, baseURL string, auth *esv1.ChefAuth) *esv1.SecretStore { store := &esv1.SecretStore{ Spec: esv1.SecretStoreSpec{ Provider: &esv1.SecretStoreProvider{ Chef: &esv1.ChefProvider{ UserName: name, ServerURL: baseURL, Auth: auth, }, }, }, } return store } func makeAuth(name, namespace, key string) *esv1.ChefAuth { return &esv1.ChefAuth{ SecretRef: esv1.ChefAuthSecretRef{ SecretKey: v1.SecretKeySelector{ Name: name, Key: key, Namespace: &namespace, }, }, } } func TestValidateStore(t *testing.T) { testCases := []ValidateStoreTestCase{ { store: makeSecretStore("", baseURL, makeAuth(authName, authNamespace, authKey)), err: errors.New("received invalid Chef SecretStore resource: missing username"), }, { store: makeSecretStore(name, "", makeAuth(authName, authNamespace, authKey)), err: errors.New("received invalid Chef SecretStore resource: missing serverurl"), }, { store: makeSecretStore(name, baseURL, nil), err: errors.New("received invalid Chef SecretStore resource: cannot initialize Chef Client: no valid authType was specified"), }, { store: makeSecretStore(name, baseInvalidURL, makeAuth(authName, authNamespace, authKey)), err: errors.New("received invalid Chef SecretStore resource: invalid serverurl: parse \"invalid base URL/\": invalid URI for request"), }, { store: makeSecretStore(name, noEndSlashInvalidBaseURL, makeAuth(authName, authNamespace, authKey)), err: errors.New("received invalid Chef SecretStore resource: serverurl does not end with slash(/)"), }, { store: makeSecretStore(name, baseURL, makeAuth(authName, authNamespace, "")), err: errors.New("received invalid Chef SecretStore resource: missing Secret Key"), }, { store: makeSecretStore(name, baseURL, makeAuth(authName, authNamespace, authKey)), err: errors.New("received invalid Chef SecretStore resource: namespace should either be empty or match the namespace of the SecretStore for a namespaced SecretStore"), }, { store: &esv1.SecretStore{ Spec: esv1.SecretStoreSpec{ Provider: nil, }, }, err: errors.New("received invalid Chef SecretStore resource: missing provider"), }, { store: &esv1.SecretStore{ Spec: esv1.SecretStoreSpec{ Provider: &esv1.SecretStoreProvider{ Chef: nil, }, }, }, err: errors.New("received invalid Chef SecretStore resource: missing chef provider"), }, } pc := Providerchef{} for _, tc := range testCases { _, err := pc.ValidateStore(tc.store) if tc.err != nil && err != nil && err.Error() != tc.err.Error() { t.Errorf("test failed! want: %v, got: %v", tc.err, err) } else if tc.err == nil && err != nil { t.Errorf("want: nil got: err %v", err) } else if tc.err != nil && err == nil { t.Errorf("want: err %v got: nil", tc.err) } } } func TestNewClient(t *testing.T) { store := &esv1.SecretStore{TypeMeta: metav1.TypeMeta{Kind: "ClusterSecretStore"}, Spec: esv1.SecretStoreSpec{ Provider: &esv1.SecretStoreProvider{ Chef: &esv1.ChefProvider{ Auth: makeAuth(authName, authNamespace, authKey), UserName: name, ServerURL: baseURL, }, }, }, } expected := fmt.Sprintf("could not fetch SecretKey Secret: secrets %q not found", authName) expectedMissingStore := "missing or invalid spec: missing store" ctx := context.TODO() kube := clientfake.NewClientBuilder().WithObjects(&corev1.Secret{ ObjectMeta: metav1.ObjectMeta{ Name: "creds", Namespace: "default", }, TypeMeta: metav1.TypeMeta{ Kind: kind, APIVersion: apiversion, }, }).Build() pc := Providerchef{databagService: &fake.ChefMockClient{}} _, errMissingStore := pc.NewClient(ctx, nil, kube, "default") if !ErrorContains(errMissingStore, expectedMissingStore) { t.Errorf("CheckNewClient unexpected error: %s, expected: '%s'", errMissingStore.Error(), expectedMissingStore) } _, err := pc.NewClient(ctx, store, kube, "default") if !ErrorContains(err, expected) { t.Errorf("CheckNewClient unexpected error: %s, expected: '%s'", err.Error(), expected) } } func ErrorContains(out error, want string) bool { if out == nil { return want == "" } if want == "" { return false } return strings.Contains(out.Error(), want) } func TestValidate(t *testing.T) { pc := Providerchef{} var mockClient *fake.ChefMockClient pc.userService = mockClient pc.clientName = "correctUser" _, err := pc.Validate() t.Log("Error: ", err) pc.clientName = "wrongUser" _, err = pc.Validate() t.Log("Error: ", err) } func TestCapabilities(t *testing.T) { pc := Providerchef{} capabilities := pc.Capabilities() t.Log(capabilities) } // Test Cases To be added when Close function is implemented. func TestClose(_ *testing.T) { pc := Providerchef{} pc.Close(context.Background()) } // Test Cases To be added when GetAllSecrets function is implemented. func TestGetAllSecrets(_ *testing.T) { pc := Providerchef{} pc.GetAllSecrets(context.Background(), esv1.ExternalSecretFind{}) } // Test Cases To be implemented when DeleteSecret function is implemented. func TestDeleteSecret(_ *testing.T) { pc := Providerchef{} pc.DeleteSecret(context.Background(), nil) } // Test Cases To be implemented when PushSecret function is implemented. func TestPushSecret(_ *testing.T) { pc := Providerchef{} pc.PushSecret(context.Background(), &corev1.Secret{}, nil) }