From 3aeb964166ac55ab8e17b7e2f8a09dd841b7b22f Mon Sep 17 00:00:00 2001 From: Alper Rifat Ulucinar Date: Wed, 12 Jun 2024 01:44:34 +0300 Subject: [PATCH] Add unit tests for traverser.maxItemsSync Signed-off-by: Alper Rifat Ulucinar --- pkg/config/resource_test.go | 187 +++++++++++++++++++++- pkg/config/schema_conversions_test.go | 5 +- pkg/schema/traverser/maxitemssync_test.go | 138 ++++++++++++++++ pkg/schema/traverser/traverse.go | 4 +- 4 files changed, 330 insertions(+), 4 deletions(-) create mode 100644 pkg/schema/traverser/maxitemssync_test.go diff --git a/pkg/config/resource_test.go b/pkg/config/resource_test.go index a94366c7..05197be8 100644 --- a/pkg/config/resource_test.go +++ b/pkg/config/resource_test.go @@ -24,7 +24,7 @@ const ( provider = "ACoolProvider" ) -func TestTagger_Initialize(t *testing.T) { +func TestTaggerInitialize(t *testing.T) { errBoom := errors.New("boom") type args struct { @@ -112,3 +112,188 @@ func TestSetExternalTagsWithPaved(t *testing.T) { }) } } + +func TestAddSingletonListConversion(t *testing.T) { + type args struct { + r func() *Resource + tfPath string + crdPath string + } + type want struct { + removed bool + r func() *Resource + } + cases := map[string]struct { + reason string + args + want + }{ + "AddNonWildcardTFPath": { + reason: "A non-wildcard TF path of a singleton list should successfully be configured to be converted into an embedded object.", + args: args{ + tfPath: "singleton_list", + crdPath: "singletonList", + r: func() *Resource { + r := DefaultResource("test_resource", nil, nil, nil) + r.AddSingletonListConversion("singleton_list", "singletonList") + return r + }, + }, + want: want{ + r: func() *Resource { + r := DefaultResource("test_resource", nil, nil, nil) + r.SchemaElementOptions = SchemaElementOptions{} + r.SchemaElementOptions["singleton_list"] = &SchemaElementOption{ + EmbeddedObject: true, + } + r.listConversionPaths["singleton_list"] = "singletonList" + return r + }, + }, + }, + "AddWildcardTFPath": { + reason: "A wildcard TF path of a singleton list should successfully be configured to be converted into an embedded object.", + args: args{ + tfPath: "parent[*].singleton_list", + crdPath: "parent[*].singletonList", + r: func() *Resource { + r := DefaultResource("test_resource", nil, nil, nil) + r.AddSingletonListConversion("parent[*].singleton_list", "parent[*].singletonList") + return r + }, + }, + want: want{ + r: func() *Resource { + r := DefaultResource("test_resource", nil, nil, nil) + r.SchemaElementOptions = SchemaElementOptions{} + r.SchemaElementOptions["parent.singleton_list"] = &SchemaElementOption{ + EmbeddedObject: true, + } + r.listConversionPaths["parent[*].singleton_list"] = "parent[*].singletonList" + return r + }, + }, + }, + "AddIndexedTFPath": { + reason: "An indexed TF path of a singleton list should successfully be configured to be converted into an embedded object.", + args: args{ + tfPath: "parent[0].singleton_list", + crdPath: "parent[0].singletonList", + r: func() *Resource { + r := DefaultResource("test_resource", nil, nil, nil) + r.AddSingletonListConversion("parent[0].singleton_list", "parent[0].singletonList") + return r + }, + }, + want: want{ + r: func() *Resource { + r := DefaultResource("test_resource", nil, nil, nil) + r.SchemaElementOptions = SchemaElementOptions{} + r.SchemaElementOptions["parent.singleton_list"] = &SchemaElementOption{ + EmbeddedObject: true, + } + r.listConversionPaths["parent[0].singleton_list"] = "parent[0].singletonList" + return r + }, + }, + }, + } + for n, tc := range cases { + t.Run(n, func(t *testing.T) { + r := tc.args.r() + r.AddSingletonListConversion(tc.args.tfPath, tc.args.crdPath) + wantR := tc.want.r() + if diff := cmp.Diff(wantR.listConversionPaths, r.listConversionPaths); diff != "" { + t.Errorf("%s\nAddSingletonListConversion(tfPath): -wantConversionPaths, +gotConversionPaths: \n%s", tc.reason, diff) + } + if diff := cmp.Diff(wantR.SchemaElementOptions, r.SchemaElementOptions); diff != "" { + t.Errorf("%s\nAddSingletonListConversion(tfPath): -wantSchemaElementOptions, +gotSchemaElementOptions: \n%s", tc.reason, diff) + } + }) + } +} + +func TestRemoveSingletonListConversion(t *testing.T) { + type args struct { + r func() *Resource + tfPath string + } + type want struct { + removed bool + r func() *Resource + } + cases := map[string]struct { + reason string + args + want + }{ + "RemoveWildcardListConversion": { + reason: "An existing wildcard list conversion can successfully be removed.", + args: args{ + tfPath: "parent[*].singleton_list", + r: func() *Resource { + r := DefaultResource("test_resource", nil, nil, nil) + r.AddSingletonListConversion("parent[*].singleton_list", "parent[*].singletonList") + return r + }, + }, + want: want{ + removed: true, + r: func() *Resource { + r := DefaultResource("test_resource", nil, nil, nil) + return r + }, + }, + }, + "RemoveIndexedListConversion": { + reason: "An existing indexed list conversion can successfully be removed.", + args: args{ + tfPath: "parent[0].singleton_list", + r: func() *Resource { + r := DefaultResource("test_resource", nil, nil, nil) + r.AddSingletonListConversion("parent[0].singleton_list", "parent[0].singletonList") + return r + }, + }, + want: want{ + removed: true, + r: func() *Resource { + r := DefaultResource("test_resource", nil, nil, nil) + return r + }, + }, + }, + "NonExistingListConversion": { + reason: "A list conversion path that does not exist cannot be removed.", + args: args{ + tfPath: "non-existent", + r: func() *Resource { + r := DefaultResource("test_resource", nil, nil, nil) + r.AddSingletonListConversion("parent[*].singleton_list", "parent[*].singletonList") + return r + }, + }, + want: want{ + removed: false, + r: func() *Resource { + r := DefaultResource("test_resource", nil, nil, nil) + r.AddSingletonListConversion("parent[*].singleton_list", "parent[*].singletonList") + return r + }, + }, + }, + } + for n, tc := range cases { + t.Run(n, func(t *testing.T) { + r := tc.args.r() + got := r.RemoveSingletonListConversion(tc.args.tfPath) + if diff := cmp.Diff(tc.want.removed, got); diff != "" { + t.Errorf("%s\nRemoveSingletonListConversion(tfPath): -wantRemoved, +gotRemoved: \n%s", tc.reason, diff) + } + + if diff := cmp.Diff(tc.want.r().listConversionPaths, r.listConversionPaths); diff != "" { + t.Errorf("%s\nRemoveSingletonListConversion(tfPath): -wantConversionPaths, +gotConversionPaths: \n%s", tc.reason, diff) + } + }) + } +} diff --git a/pkg/config/schema_conversions_test.go b/pkg/config/schema_conversions_test.go index ccfe6518..785fc6bd 100644 --- a/pkg/config/schema_conversions_test.go +++ b/pkg/config/schema_conversions_test.go @@ -155,7 +155,10 @@ func TestSingletonListEmbedder(t *testing.T) { t.Run(n, func(t *testing.T) { e := &SingletonListEmbedder{} r := DefaultResource(tt.args.name, tt.args.resource, nil, nil) - err := TraverseSchemas(tt.args.name, r, e) + s := ResourceSchema{ + tt.args.name: r, + } + err := s.TraverseTFSchemas(e) if diff := cmp.Diff(tt.want.err, err, test.EquateErrors()); diff != "" { t.Fatalf("\n%s\ntraverseSchemas(name, schema, ...): -wantErr, +gotErr:\n%s", tt.reason, diff) } diff --git a/pkg/schema/traverser/maxitemssync_test.go b/pkg/schema/traverser/maxitemssync_test.go new file mode 100644 index 00000000..d11d35b1 --- /dev/null +++ b/pkg/schema/traverser/maxitemssync_test.go @@ -0,0 +1,138 @@ +package traverser + +import ( + "testing" + + "github.com/crossplane/crossplane-runtime/pkg/test" + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +func TestMaxItemsSync(t *testing.T) { + type args struct { + srcSchema TFResourceSchema + targetSchema TFResourceSchema + } + type want struct { + targetSchema TFResourceSchema + err error + } + cases := map[string]struct { + reason string + args + want + }{ + "SyncMaxItemsConstraints": { + reason: `maxItemsSync traverser can successfully sync the "MaxItems = 1" constraints from the source schema to the target schema.`, + args: args{ + srcSchema: map[string]*schema.Resource{ + "test_resource": { + Schema: map[string]*schema.Schema{ + "argument": { + MaxItems: 1, + Type: schema.TypeList, + Elem: &schema.Resource{}, + }, + }, + }, + }, + targetSchema: map[string]*schema.Resource{ + "test_resource": { + Schema: map[string]*schema.Schema{ + "argument": { + MaxItems: 0, + Type: schema.TypeList, + Elem: &schema.Resource{}, + }, + }, + }, + }, + }, + want: want{ + targetSchema: map[string]*schema.Resource{ + "test_resource": { + Schema: map[string]*schema.Schema{ + "argument": { + MaxItems: 1, + Type: schema.TypeList, + Elem: &schema.Resource{}, + }, + }, + }, + }, + }, + }, + "NoSyncMaxItems": { + reason: "If the MaxItems constraint is greater than 1, then maxItemsSync should not sync the constraint.", + args: args{ + srcSchema: map[string]*schema.Resource{ + "test_resource": { + Schema: map[string]*schema.Schema{ + "argument": { + MaxItems: 2, + Type: schema.TypeList, + Elem: &schema.Resource{}, + }, + }, + }, + }, + targetSchema: map[string]*schema.Resource{ + "test_resource": { + Schema: map[string]*schema.Schema{ + "argument": { + MaxItems: 0, + Type: schema.TypeList, + Elem: &schema.Resource{}, + }, + }, + }, + }, + }, + want: want{ + targetSchema: map[string]*schema.Resource{ + "test_resource": { + Schema: map[string]*schema.Schema{ + "argument": { + MaxItems: 0, + Type: schema.TypeList, + Elem: &schema.Resource{}, + }, + }, + }, + }, + }, + }, + } + for n, tc := range cases { + t.Run(n, func(t *testing.T) { + copySrc := copySchema(tc.args.srcSchema) + got := tc.args.srcSchema.Traverse(NewMaxItemsSync(tc.args.targetSchema)) + if diff := cmp.Diff(tc.want.err, got, test.EquateErrors()); diff != "" { + t.Errorf("%s\nMaxItemsSync: -wantErr, +gotErr: \n%s", tc.reason, diff) + } + if diff := cmp.Diff(tc.want.err, got, test.EquateErrors()); diff != "" { + t.Errorf("%s\nMaxItemsSync: -wantErr, +gotErr: \n%s", tc.reason, diff) + } + if diff := cmp.Diff(copySrc, tc.srcSchema); diff != "" { + t.Errorf("%s\nMaxItemsSync: -wantSourceSchema, +gotSourceSchema: \n%s", tc.reason, diff) + } + if diff := cmp.Diff(tc.want.targetSchema, tc.args.targetSchema); diff != "" { + t.Errorf("%s\nMaxItemsSync: -wantTargetSchema, +gotTargetSchema: \n%s", tc.reason, diff) + } + }) + } +} + +func copySchema(s TFResourceSchema) TFResourceSchema { + result := make(TFResourceSchema) + for k, v := range s { + c := *v + for n, s := range v.Schema { + // not a deep copy but sufficient to check the schema constraints + s := *s + c.Schema[n] = &s + } + result[k] = &c + } + return result +} diff --git a/pkg/schema/traverser/traverse.go b/pkg/schema/traverser/traverse.go index 925a14ea..725bff2c 100644 --- a/pkg/schema/traverser/traverse.go +++ b/pkg/schema/traverser/traverse.go @@ -144,9 +144,9 @@ func (n NoopTraverser) VisitResource(*ResourceNode) error { // TFResourceSchema represents a provider's Terraform resource schema. type TFResourceSchema map[string]*schema.Resource -// TraverseTFSchemas traverses the receiver schema using the specified +// Traverse traverses the receiver schema using the specified // visitors. Reports any errors encountered by the visitors. -func (s TFResourceSchema) TraverseTFSchemas(visitors ...SchemaTraverser) error { +func (s TFResourceSchema) Traverse(visitors ...SchemaTraverser) error { for n, r := range s { if err := Traverse(n, r, visitors...); err != nil { return errors.Wrapf(err, "failed to traverse the schema of the Terraform resource with name %q", n)