912 lines
		
	
	
		
			22 KiB
		
	
	
	
		
			Go
		
	
	
	
			
		
		
	
	
			912 lines
		
	
	
		
			22 KiB
		
	
	
	
		
			Go
		
	
	
	
| /*
 | |
| 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)
 | |
| 			}
 | |
| 		})
 | |
| 	}
 | |
| }
 |