diff --git a/internal/testing/shoot/matcher.go b/internal/testing/shoot/matcher.go new file mode 100644 index 00000000..53f97fc4 --- /dev/null +++ b/internal/testing/shoot/matcher.go @@ -0,0 +1,131 @@ +package shoot + +import ( + "fmt" + "reflect" + "strings" + + "github.com/gardener/gardener/pkg/apis/core/v1beta1" + "github.com/onsi/gomega" + "github.com/onsi/gomega/types" + "sigs.k8s.io/yaml" +) + +var ( + errInvalidType = fmt.Errorf("invalid type") +) + +type Matcher struct { + toMatch interface{} + fails []string +} + +func NewMatcher(i interface{}) types.GomegaMatcher { + return &Matcher{ + toMatch: i, + } +} + +func getShoot(i interface{}) (shoot v1beta1.Shoot, err error) { + if i == nil { + return v1beta1.Shoot{}, fmt.Errorf("invalid value nil") + } + + switch v := i.(type) { + case string: + err = yaml.Unmarshal([]byte(v), &shoot) + return shoot, err + + case v1beta1.Shoot: + return v, nil + + case *v1beta1.Shoot: + return *v, nil + + default: + return v1beta1.Shoot{}, fmt.Errorf(`%w: %s`, errInvalidType, reflect.TypeOf(v)) + } +} + +type matcher struct { + types.GomegaMatcher + path string + actual interface{} +} + +func (m *Matcher) Match(actual interface{}) (success bool, err error) { + aShoot, err := getShoot(actual) + if err != nil { + return false, err + } + + eShoot, err := getShoot(m.toMatch) + if err != nil { + return false, err + } + + for _, matcher := range []matcher{ + { + GomegaMatcher: gomega.Equal(eShoot.TypeMeta), + actual: aShoot.TypeMeta, + }, + { + GomegaMatcher: gomega.Equal(eShoot.Name), + actual: aShoot.Name, + path: "metadata/name", + }, + { + GomegaMatcher: gomega.Equal(eShoot.Namespace), + actual: aShoot.Namespace, + path: "metadata/namespace", + }, + { + GomegaMatcher: gomega.Equal(eShoot.Labels), + actual: aShoot.Labels, + path: "metadata/labels", + }, + { + GomegaMatcher: gomega.Equal(eShoot.Annotations), + actual: aShoot.Annotations, + path: "metadata/annotations", + }, + { + GomegaMatcher: gomega.Equal(eShoot.OwnerReferences), + actual: aShoot.OwnerReferences, + path: "metadata/ownerReferences", + }, + { + GomegaMatcher: gomega.Equal(eShoot.Finalizers), + actual: aShoot.Finalizers, + path: "metadata/finalizers", + }, + { + GomegaMatcher: gomega.Equal(eShoot.Spec), + actual: aShoot.Spec, + path: "spec", + }, + } { + ok, err := matcher.Match(matcher.actual) + if err != nil { + return false, err + } + + if !ok { + msg := matcher.FailureMessage(matcher.actual) + if matcher.path != "" { + msg = fmt.Sprintf("%s: %s", matcher.path, msg) + } + m.fails = append(m.fails, msg) + } + } + + return len(m.fails) == 0, nil +} + +func (m *Matcher) NegatedFailureMessage(_ interface{}) string { + return "expected should not equal actual" +} + +func (m *Matcher) FailureMessage(_ interface{}) string { + return strings.Join(m.fails, "\n") +} diff --git a/internal/testing/shoot/matcher_test.go b/internal/testing/shoot/matcher_test.go new file mode 100644 index 00000000..d52981db --- /dev/null +++ b/internal/testing/shoot/matcher_test.go @@ -0,0 +1,155 @@ +package shoot_test + +import ( + "github.com/gardener/gardener/pkg/apis/core/v1beta1" + "github.com/kyma-project/infrastructure-manager/internal/testing/shoot" + . "github.com/onsi/ginkgo/v2" //nolint:revive + . "github.com/onsi/gomega" //nolint:revive + corev1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +type deepCpOpts = func(*v1beta1.Shoot) + +func withName(name string) deepCpOpts { + return func(s *v1beta1.Shoot) { + s.Name = name + } +} + +func withNamespace(namespace string) deepCpOpts { + return func(s *v1beta1.Shoot) { + s.Namespace = namespace + } +} + +func withLabels(labels map[string]string) deepCpOpts { + return func(s *v1beta1.Shoot) { + s.Labels = labels + } +} + +func withFinalizers(finalizers []string) deepCpOpts { + return func(s *v1beta1.Shoot) { + s.Finalizers = finalizers + } +} + +func withOwnerReferences(ownerReferences []corev1.OwnerReference) deepCpOpts { + return func(s *v1beta1.Shoot) { + s.OwnerReferences = ownerReferences + } +} + +func withAnnotations(annotations map[string]string) deepCpOpts { + return func(s *v1beta1.Shoot) { + s.Annotations = annotations + } +} + +func withShootSpec(spec v1beta1.ShootSpec) deepCpOpts { + return func(s *v1beta1.Shoot) { + s.Spec = spec + } +} + +// nolint: unparam +func deepCp(s v1beta1.Shoot, opts ...deepCpOpts) v1beta1.Shoot { + for _, opt := range opts { + opt(&s) + } + + return s +} + +func testInvalidArgs(actual, expected interface{}) { + matcher := shoot.NewMatcher(expected) + _, err := matcher.Match(actual) + Expect(err).To(HaveOccurred()) +} + +func testResults(actual, expected interface{}, expectedMatch bool) { + matcher := shoot.NewMatcher(expected) + actualMatch, err := matcher.Match(actual) + Expect(err).ShouldNot(HaveOccurred()) + Expect(actualMatch).Should(Equal(expectedMatch), matcher.FailureMessage(actual)) +} + +var _ = Describe(":: shoot matcher :: ", func() { + var empty v1beta1.Shoot + + DescribeTable( + "checking invalid args :: ", + testInvalidArgs, + Entry("when actual is nil", nil, empty), + Entry("when expected is nil", "", nil), + ) + + DescribeTable( + "checking results :: ", + testResults, + Entry( + "should match empty and zero values", + "", + empty, + true, + ), + Entry( + "should match copies of the same instance", + deepCp(empty), + deepCp(empty), + true, + ), + Entry( + "should detect name difference", + deepCp(empty, withName("test1")), + deepCp(empty, withName("test2")), + false, + ), + Entry( + "should detect namespace difference", + deepCp(empty, withNamespace("test1")), + deepCp(empty, withNamespace("test2")), + false, + ), + Entry( + "should detect difference in labels", + deepCp(empty, withLabels(map[string]string{"test": "me"})), + deepCp(empty, withLabels(map[string]string{})), + false, + ), + Entry( + "should detect difference in annotations", + deepCp(empty, withAnnotations(map[string]string{"test": "me"})), + deepCp(empty, withAnnotations(map[string]string{"test": "it"})), + false, + ), + Entry( + "should detect differences in finalizers", + deepCp(empty, withFinalizers([]string{"test", "me"})), + deepCp(empty, withFinalizers([]string{"test", "me 2"})), + false, + ), + Entry( + "should detect differences in owner references", + deepCp(empty, withOwnerReferences([]corev1.OwnerReference{ + {Name: "test1", UID: "1"}, + {Name: "test2", UID: "2"}, + })), + deepCp(empty, withOwnerReferences([]corev1.OwnerReference{ + {Name: "test1", UID: "1"}, + {Name: "test3", UID: "3"}, + })), + false, + ), + Entry( + "should detect differences in spec", + deepCp(empty, withShootSpec(v1beta1.ShootSpec{ + Region: "test1", + })), + deepCp(empty, withShootSpec(v1beta1.ShootSpec{ + Region: "test2", + })), + false, + ), + ) +}) diff --git a/internal/testing/shoot/suite_test.go b/internal/testing/shoot/suite_test.go new file mode 100644 index 00000000..d1c79df5 --- /dev/null +++ b/internal/testing/shoot/suite_test.go @@ -0,0 +1,13 @@ +package shoot_test + +import ( + "testing" + + "github.com/onsi/ginkgo/v2" //nolint:revive + "github.com/onsi/gomega" //nolint:revive +) + +func TestMatcher(t *testing.T) { + gomega.RegisterFailHandler(ginkgo.Fail) + ginkgo.RunSpecs(t, "shoot matcher") +}