/* Copyright 2019 The Crossplane 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 htcp://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 resource import ( "context" "strings" "testing" "github.com/google/go-cmp/cmp" corev1 "k8s.io/api/core/v1" kerrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/util/wait" "k8s.io/client-go/util/retry" "sigs.k8s.io/controller-runtime/pkg/client" xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1" "github.com/crossplane/crossplane-runtime/pkg/errors" "github.com/crossplane/crossplane-runtime/pkg/resource/fake" "github.com/crossplane/crossplane-runtime/pkg/test" ) const ( namespace = "coolns" name = "cool" uid = types.UID("definitely-a-uuid") testSteps = 3 ) var ( MockOwnerGVK = schema.GroupVersionKind{ Group: "cool", Version: "large", Kind: "MockOwner", } testBackoff = wait.Backoff{} errTest = errors.New("test-error") ) func TestLocalConnectionSecretFor(t *testing.T) { secretName := "coolsecret" type args struct { o LocalConnectionSecretOwner kind schema.GroupVersionKind } controller := true cases := map[string]struct { args args want *corev1.Secret }{ "Success": { args: args{ o: &fake.MockLocalConnectionSecretOwner{ ObjectMeta: metav1.ObjectMeta{ Namespace: namespace, Name: name, UID: uid, }, Ref: &xpv1.LocalSecretReference{Name: secretName}, }, kind: MockOwnerGVK, }, want: &corev1.Secret{ ObjectMeta: metav1.ObjectMeta{ Namespace: namespace, Name: secretName, OwnerReferences: []metav1.OwnerReference{{ APIVersion: MockOwnerGVK.GroupVersion().String(), Kind: MockOwnerGVK.Kind, Name: name, UID: uid, Controller: &controller, BlockOwnerDeletion: &controller, }}, }, Type: SecretTypeConnection, Data: map[string][]byte{}, }, }, } for name, tc := range cases { t.Run(name, func(t *testing.T) { got := LocalConnectionSecretFor(tc.args.o, tc.args.kind) if diff := cmp.Diff(tc.want, got); diff != "" { t.Errorf("LocalConnectionSecretFor(): -want, +got:\n%s", diff) } }) } } func TestConnectionSecretFor(t *testing.T) { secretName := "coolsecret" type args struct { o ConnectionSecretOwner kind schema.GroupVersionKind } controller := true cases := map[string]struct { args args want *corev1.Secret }{ "Success": { args: args{ o: &fake.MockConnectionSecretOwner{ ObjectMeta: metav1.ObjectMeta{ Namespace: namespace, Name: name, UID: uid, }, WriterTo: &xpv1.SecretReference{Namespace: namespace, Name: secretName}, }, kind: MockOwnerGVK, }, want: &corev1.Secret{ ObjectMeta: metav1.ObjectMeta{ Namespace: namespace, Name: secretName, OwnerReferences: []metav1.OwnerReference{{ APIVersion: MockOwnerGVK.GroupVersion().String(), Kind: MockOwnerGVK.Kind, Name: name, UID: uid, Controller: &controller, BlockOwnerDeletion: &controller, }}, }, Type: SecretTypeConnection, Data: map[string][]byte{}, }, }, } for name, tc := range cases { t.Run(name, func(t *testing.T) { got := ConnectionSecretFor(tc.args.o, tc.args.kind) if diff := cmp.Diff(tc.want, got); diff != "" { t.Errorf("ConnectionSecretFor(): -want, +got:\n%s", diff) } }) } } type MockTyper struct { GVKs []schema.GroupVersionKind Unversioned bool Error error } func (t MockTyper) ObjectKinds(_ runtime.Object) ([]schema.GroupVersionKind, bool, error) { return t.GVKs, t.Unversioned, t.Error } func (t MockTyper) Recognizes(_ schema.GroupVersionKind) bool { return true } func TestGetKind(t *testing.T) { type args struct { obj runtime.Object ot runtime.ObjectTyper } type want struct { kind schema.GroupVersionKind err error } errBoom := errors.New("boom") cases := map[string]struct { args args want want }{ "KindFound": { args: args{ ot: MockTyper{GVKs: []schema.GroupVersionKind{fake.GVK(&fake.Managed{})}}, }, want: want{ kind: fake.GVK(&fake.Managed{}), }, }, "KindError": { args: args{ ot: MockTyper{Error: errBoom}, }, want: want{ err: errors.Wrap(errBoom, "cannot get kind of supplied object"), }, }, "KindIsUnversioned": { args: args{ ot: MockTyper{Unversioned: true}, }, want: want{ err: errors.New("supplied object is unversioned"), }, }, "NotEnoughKinds": { args: args{ ot: MockTyper{}, }, want: want{ err: errors.New("supplied object does not have exactly one kind"), }, }, "TooManyKinds": { args: args{ ot: MockTyper{GVKs: []schema.GroupVersionKind{ fake.GVK(&fake.Object{}), fake.GVK(&fake.Managed{}), }}, }, want: want{ err: errors.New("supplied object does not have exactly one kind"), }, }, } for name, tc := range cases { t.Run(name, func(t *testing.T) { got, err := GetKind(tc.args.obj, tc.args.ot) if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" { t.Errorf("GetKind(...): -want error, +got error:\n%s", diff) } if diff := cmp.Diff(tc.want.kind, got); diff != "" { t.Errorf("GetKind(...): -want, +got:\n%s", diff) } }) } } func TestMustCreateObject(t *testing.T) { type args struct { kind schema.GroupVersionKind oc runtime.ObjectCreater } cases := map[string]struct { args args want runtime.Object }{ "KindRegistered": { args: args{ kind: fake.GVK(&fake.Managed{}), oc: fake.SchemeWith(&fake.Managed{}), }, want: &fake.Managed{}, }, } for name, tc := range cases { t.Run(name, func(t *testing.T) { got := MustCreateObject(tc.args.kind, tc.args.oc) if diff := cmp.Diff(tc.want, got); diff != "" { t.Errorf("MustCreateObject(...): -want, +got:\n%s", diff) } }) } } func TestIgnore(t *testing.T) { errBoom := errors.New("boom") type args struct { is ErrorIs err error } cases := map[string]struct { args args want error }{ "IgnoreError": { args: args{ is: func(_ error) bool { return true }, err: errBoom, }, want: nil, }, "PropagateError": { args: args{ is: func(_ error) bool { return false }, err: errBoom, }, want: errBoom, }, } for name, tc := range cases { t.Run(name, func(t *testing.T) { got := Ignore(tc.args.is, tc.args.err) if diff := cmp.Diff(tc.want, got, test.EquateErrors()); diff != "" { t.Errorf("Ignore(...): -want error, +got error:\n%s", diff) } }) } } func TestIgnoreAny(t *testing.T) { errBoom := errors.New("boom") type args struct { is []ErrorIs err error } cases := map[string]struct { args args want error }{ "IgnoreError": { args: args{ is: []ErrorIs{func(_ error) bool { return true }}, err: errBoom, }, want: nil, }, "IgnoreErrorArr": { args: args{ is: []ErrorIs{ func(_ error) bool { return true }, func(_ error) bool { return false }, }, err: errBoom, }, want: nil, }, "PropagateError": { args: args{ is: []ErrorIs{func(_ error) bool { return false }}, err: errBoom, }, want: errBoom, }, } for name, tc := range cases { t.Run(name, func(t *testing.T) { got := IgnoreAny(tc.args.err, tc.args.is...) if diff := cmp.Diff(tc.want, got, test.EquateErrors()); diff != "" { t.Errorf("Ignore(...): -want error, +got error:\n%s", diff) } }) } } func TestIsConditionTrue(t *testing.T) { cases := map[string]struct { c xpv1.Condition want bool }{ "IsTrue": { c: xpv1.Condition{Status: corev1.ConditionTrue}, want: true, }, "IsFalse": { c: xpv1.Condition{Status: corev1.ConditionFalse}, want: false, }, "IsUnknown": { c: xpv1.Condition{Status: corev1.ConditionUnknown}, want: false, }, "IsUnset": { c: xpv1.Condition{}, want: false, }, } for name, tc := range cases { t.Run(name, func(t *testing.T) { got := IsConditionTrue(tc.c) if diff := cmp.Diff(tc.want, got); diff != "" { t.Errorf("IsConditionTrue(...): -want, +got:\n%s", diff) } }) } } type object struct { runtime.Object metav1.ObjectMeta } func (o *object) DeepCopyObject() runtime.Object { return &object{ObjectMeta: *o.ObjectMeta.DeepCopy()} } func TestIsNotControllable(t *testing.T) { cases := map[string]struct { reason string err error want bool }{ "NilError": { reason: "A nil error does not indicate something is not controllable.", err: nil, want: false, }, "UnknownError": { reason: "An that doesn't have a 'NotControllable() bool' method does not indicate something is not controllable.", err: errors.New("boom"), want: false, }, "NotControllableError": { reason: "An that has a 'NotControllable() bool' method indicates something is not controllable.", err: errNotControllable{errors.New("boom")}, want: true, }, } for name, tc := range cases { t.Run(name, func(t *testing.T) { got := IsNotControllable(tc.err) if diff := cmp.Diff(tc.want, got); diff != "" { t.Errorf("\n%s\nIsNotControllable(...): -want, +got:\n%s\n", tc.reason, diff) } }) } } func TestMustBeControllableBy(t *testing.T) { uid := types.UID("very-unique-string") controller := true type args struct { ctx context.Context current runtime.Object desired runtime.Object } cases := map[string]struct { reason string u types.UID args args want error }{ "Adoptable": { reason: "A current object with no controller reference may be adopted and controlled", u: uid, args: args{ current: &object{}, }, }, "ControlledBySuppliedUID": { reason: "A current object that is already controlled by the supplied UID is controllable", u: uid, args: args{ current: &object{ObjectMeta: metav1.ObjectMeta{OwnerReferences: []metav1.OwnerReference{{ UID: uid, Controller: &controller, }}}}, }, }, "ControlledBySomeoneElse": { reason: "A current object that is already controlled by a different UID is not controllable", u: uid, args: args{ current: &object{ObjectMeta: metav1.ObjectMeta{OwnerReferences: []metav1.OwnerReference{{ UID: types.UID("some-other-uid"), Controller: &controller, }}}}, }, want: errNotControllable{errors.Errorf("existing object is not controlled by UID %q", uid)}, }, } for name, tc := range cases { t.Run(name, func(t *testing.T) { ao := MustBeControllableBy(tc.u) err := ao(tc.args.ctx, tc.args.current, tc.args.desired) if diff := cmp.Diff(tc.want, err, test.EquateErrors()); diff != "" { t.Errorf("\n%s\nMustBeControllableBy(...)(...): -want error, +got error\n%s\n", tc.reason, diff) } }) } } func TestConnectionSecretMustBeControllableBy(t *testing.T) { uid := types.UID("very-unique-string") controller := true type args struct { ctx context.Context current runtime.Object desired runtime.Object } cases := map[string]struct { reason string u types.UID args args want error }{ "Adoptable": { reason: "A Secret of SecretTypeConnection with no controller reference may be adopted and controlled", u: uid, args: args{ current: &corev1.Secret{Type: SecretTypeConnection}, }, }, "ControlledBySuppliedUID": { reason: "A Secret of any type that is already controlled by the supplied UID is controllable", u: uid, args: args{ current: &corev1.Secret{ ObjectMeta: metav1.ObjectMeta{OwnerReferences: []metav1.OwnerReference{{ UID: uid, Controller: &controller, }}}, Type: corev1.SecretTypeOpaque, }, }, }, "ControlledBySomeoneElse": { reason: "A Secret of any type that is already controlled by the another UID is not controllable", u: uid, args: args{ current: &corev1.Secret{ ObjectMeta: metav1.ObjectMeta{OwnerReferences: []metav1.OwnerReference{{ UID: types.UID("some-other-uid"), Controller: &controller, }}}, Type: SecretTypeConnection, }, }, want: errNotControllable{errors.Errorf("existing secret is not controlled by UID %q", uid)}, }, "UncontrolledOpaqueSecret": { reason: "A Secret of corev1.SecretTypeOpqaue with no controller is not controllable", u: uid, args: args{ current: &corev1.Secret{Type: corev1.SecretTypeOpaque}, }, want: errNotControllable{errors.Errorf("refusing to modify uncontrolled secret of type %q", corev1.SecretTypeOpaque)}, }, } for name, tc := range cases { t.Run(name, func(t *testing.T) { ao := ConnectionSecretMustBeControllableBy(tc.u) err := ao(tc.args.ctx, tc.args.current, tc.args.desired) if diff := cmp.Diff(tc.want, err, test.EquateErrors()); diff != "" { t.Errorf("\n%s\nConnectionSecretMustBeControllableBy(...)(...): -want error, +got error\n%s\n", tc.reason, diff) } }) } } func TestAllowUpdateIf(t *testing.T) { type args struct { ctx context.Context current runtime.Object desired runtime.Object } cases := map[string]struct { reason string fn func(current, desired runtime.Object) bool args args want error }{ "Allowed": { reason: "No error should be returned when the supplied function returns true", fn: func(_, _ runtime.Object) bool { return true }, args: args{ current: &object{}, }, }, "NotAllowed": { reason: "An error that satisfies IsNotAllowed should be returned when the supplied function returns false", fn: func(_, _ runtime.Object) bool { return false }, args: args{ current: &object{}, }, want: errNotAllowed{errors.New("update not allowed")}, }, } for name, tc := range cases { t.Run(name, func(t *testing.T) { ao := AllowUpdateIf(tc.fn) err := ao(tc.args.ctx, tc.args.current, tc.args.desired) if diff := cmp.Diff(tc.want, err, test.EquateErrors()); diff != "" { t.Errorf("\n%s\nAllowUpdateIf(...)(...): -want error, +got error\n%s\n", tc.reason, diff) } }) } } func TestGetExternalTags(t *testing.T) { provName := "prov" cases := map[string]struct { o Managed want map[string]string }{ "SuccessfulWithProviderConfig": { o: &fake.Managed{ ObjectMeta: metav1.ObjectMeta{ Name: name, }, ProviderConfigReferencer: fake.ProviderConfigReferencer{Ref: &xpv1.Reference{Name: provName}}, }, want: map[string]string{ ExternalResourceTagKeyKind: strings.ToLower((&fake.Managed{}).GetObjectKind().GroupVersionKind().GroupKind().String()), ExternalResourceTagKeyName: name, ExternalResourceTagKeyProvider: provName, }, }, } for name, tc := range cases { t.Run(name, func(t *testing.T) { got := GetExternalTags(tc.o) if diff := cmp.Diff(tc.want, got); diff != "" { t.Errorf("GetExternalTags(...): -want, +got:\n%s", diff) } }) } } // single test case => not using tables. func Test_errNotControllable_NotControllable(t *testing.T) { err := errNotControllable{ errors.New("test-error"), } if !err.NotControllable() { t.Errorf("NotControllable(): false") } } // single test case => not using tables. func Test_errNotAllowed_NotAllowed(t *testing.T) { err := errNotAllowed{ errors.New("test-error"), } if !err.NotAllowed() { t.Errorf("NotAllowed(): false") } } func TestIsAPIErrorWrapped(t *testing.T) { testCases := map[string]struct { err error want bool }{ "NoError": { want: false, }, "NotAPIError": { err: errors.New("test-error"), want: false, }, "APIError": { err: kerrors.NewNotFound(schema.GroupResource{}, "test-resource"), want: true, }, "WrappedAPIError": { err: errors.Wrap( kerrors.NewNotFound(schema.GroupResource{}, "test-resource"), "test-wrapper"), want: true, }, } for name, tc := range testCases { t.Run(name, func(t *testing.T) { if got := IsAPIErrorWrapped(tc.err); got != tc.want { t.Errorf("IsAPIErrorWrapped() = %v, want %v", got, tc.want) } }) } } func TestNewApplicatorWithRetry(t *testing.T) { type args struct { applicator Applicator shouldRetry shouldRetryFunc backoff *wait.Backoff } testCases := map[string]struct { args args want Applicator }{ "DefaultBackoff": { args: args{}, want: &ApplicatorWithRetry{ backoff: retry.DefaultRetry, }, }, "CustomBackoff": { args: args{ backoff: &testBackoff, }, want: &ApplicatorWithRetry{ backoff: testBackoff, }, }, } for name, tc := range testCases { t.Run(name, func(t *testing.T) { if diff := cmp.Diff(tc.want, NewApplicatorWithRetry(tc.args.applicator, tc.args.shouldRetry, tc.args.backoff), cmp.AllowUnexported(ApplicatorWithRetry{})); diff != "" { t.Errorf("NewApplicatorWithRetry(...): -want, +got:\n%s", diff) } }) } } type mockApplicator struct { returnError bool count uint } func (m *mockApplicator) Apply(_ context.Context, _ client.Object, _ ...ApplyOption) error { m.count++ if m.returnError { return errTest } return nil } func TestApplicatorWithRetry_Apply(t *testing.T) { type fields struct { applicator Applicator shouldRetry shouldRetryFunc backoff wait.Backoff } type args struct { ctx context.Context c client.Object opts []ApplyOption } testCases := map[string]struct { fields fields args args wantErr error wantCount uint }{ "NoRetry": { fields: fields{ applicator: &mockApplicator{returnError: true}, shouldRetry: func(_ error) bool { return false }, backoff: wait.Backoff{Steps: testSteps}, }, args: args{}, wantErr: errTest, wantCount: 1, }, "ShouldRetry": { fields: fields{ applicator: &mockApplicator{returnError: true}, shouldRetry: func(_ error) bool { return true }, backoff: wait.Backoff{Steps: testSteps}, }, args: args{}, wantErr: errTest, wantCount: testSteps, }, "NoError": { fields: fields{ applicator: &mockApplicator{}, shouldRetry: func(_ error) bool { return true }, backoff: wait.Backoff{Steps: testSteps}, }, args: args{}, wantErr: nil, wantCount: 1, }, } for name, tc := range testCases { t.Run(name, func(t *testing.T) { awr := &ApplicatorWithRetry{ Applicator: tc.fields.applicator, shouldRetry: tc.fields.shouldRetry, backoff: tc.fields.backoff, } if diff := cmp.Diff(tc.wantErr, awr.Apply(tc.args.ctx, tc.args.c, tc.args.opts...), test.EquateErrors()); diff != "" { t.Fatalf("ApplicatorWithRetry.Apply(...): -want, +got:\n%s", diff) } if diff := cmp.Diff(awr.Applicator.(*mockApplicator).count, tc.wantCount); diff != "" { t.Errorf("Retry count mismatch: -want, +got:\n%s", diff) } }) } } func TestUpdate(t *testing.T) { type args struct { fn func(current, desired runtime.Object) current runtime.Object desired runtime.Object } tests := map[string]struct { args args want runtime.Object }{ "Update": { args: args{ fn: func(current, desired runtime.Object) { c, d := current.(*corev1.Secret), desired.(*corev1.Secret) c.StringData = d.StringData }, current: &corev1.Secret{ ObjectMeta: metav1.ObjectMeta{ Name: "current", }, }, desired: &corev1.Secret{ ObjectMeta: metav1.ObjectMeta{ Name: "desired", }, StringData: map[string]string{ "key": "value", }, }, }, want: &corev1.Secret{ ObjectMeta: metav1.ObjectMeta{ Name: "current", }, StringData: map[string]string{ "key": "value", }, }, }, } for name, tt := range tests { t.Run(name, func(t *testing.T) { if err := UpdateFn(tt.args.fn)(nil, tt.args.current, tt.args.desired); err != nil { t.Fatalf("ApplyOption() = %v, want %v", err, nil) } if diff := cmp.Diff(tt.want, tt.args.current); diff != "" { t.Errorf("UpdateFn updated object mismatch: -want, +got: %s", diff) } }) } } func TestFirstNAndSomeMore(t *testing.T) { type args struct { n int names []string } tests := []struct { name string args args want string }{ {args: args{n: 3, names: []string{"a", "b", "c", "d", "e"}}, want: "a, b, c, and 2 more"}, {args: args{n: 3, names: []string{"a", "b", "c"}}, want: "a, b, and c"}, {args: args{n: 3, names: []string{"a", "b"}}, want: "a, b"}, {args: args{n: 3, names: []string{"a"}}, want: "a"}, {args: args{n: 3, names: []string{}}, want: ""}, {args: args{n: 3, names: []string{"a", "c", "e", "b", "d"}}, want: "a, c, e, and 2 more"}, {args: args{n: 3, names: []string{"a", "b", "b", "b", "d"}}, want: "a, b, b, and 2 more"}, //nolint:dupword // Intentional. {args: args{n: 2, names: []string{"a", "c", "e", "b", "d"}}, want: "a, c, and 3 more"}, {args: args{n: 0, names: []string{"a", "c", "e", "b", "d"}}, want: "5"}, {args: args{n: -7, names: []string{"a", "c", "e", "b", "d"}}, want: "5"}, {args: args{n: 1, names: []string{"a", "c", "e", "b", "d"}}, want: "a, and 4 more"}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if got := FirstNAndSomeMore(tt.args.n, tt.args.names); got != tt.want { t.Errorf("FirstNAndSomeMore() = %v, want %v", got, tt.want) } }) } }