github.com/crossplane/upjet@v1.3.0/pkg/types/builder_test.go (about)

     1  // SPDX-FileCopyrightText: 2023 The Crossplane Authors <https://crossplane.io>
     2  //
     3  // SPDX-License-Identifier: Apache-2.0
     4  
     5  package types
     6  
     7  import (
     8  	"fmt"
     9  	"go/token"
    10  	"go/types"
    11  	"testing"
    12  
    13  	"github.com/crossplane/crossplane-runtime/pkg/test"
    14  	"github.com/google/go-cmp/cmp"
    15  	"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
    16  	"github.com/pkg/errors"
    17  
    18  	"github.com/crossplane/upjet/pkg/config"
    19  )
    20  
    21  func TestBuilder_generateTypeName(t *testing.T) {
    22  	type args struct {
    23  		existing []string
    24  		suffix   string
    25  		names    []string
    26  
    27  		overrideFieldNames map[string]string
    28  	}
    29  	type want struct {
    30  		out string
    31  		err error
    32  	}
    33  	cases := map[string]struct {
    34  		args
    35  		want
    36  	}{
    37  		"NoExisting": {
    38  			args: args{
    39  				existing: []string{
    40  					"SomeOtherType",
    41  				},
    42  				suffix: "Parameters",
    43  				names: []string{
    44  					"Subnetwork",
    45  				},
    46  			},
    47  			want: want{
    48  				out: "SubnetworkParameters",
    49  				err: nil,
    50  			},
    51  		},
    52  		"NoExistingMultipleIndexes": {
    53  			args: args{
    54  				existing: []string{
    55  					"SomeOtherType",
    56  				},
    57  				suffix: "Parameters",
    58  				names: []string{
    59  					"RouterNat",
    60  					"Subnetwork",
    61  				},
    62  			},
    63  			want: want{
    64  				out: "SubnetworkParameters",
    65  				err: nil,
    66  			},
    67  		},
    68  		"NoIndexExists": {
    69  			args: args{
    70  				existing: []string{
    71  					"SubnetworkParameters",
    72  				},
    73  				suffix: "Parameters",
    74  				names: []string{
    75  					"Subnetwork",
    76  				},
    77  			},
    78  			want: want{
    79  				out: "SubnetworkParameters_2",
    80  				err: nil,
    81  			},
    82  		},
    83  		"MultipleIndexesExist": {
    84  			args: args{
    85  				existing: []string{
    86  					"SubnetworkParameters",
    87  					"SubnetworkParameters_2",
    88  					"SubnetworkParameters_3",
    89  					"SubnetworkParameters_4",
    90  				},
    91  				suffix: "Parameters",
    92  				names: []string{
    93  					"Subnetwork",
    94  				},
    95  			},
    96  			want: want{
    97  				out: "SubnetworkParameters_5",
    98  				err: nil,
    99  			},
   100  		},
   101  		"ErrIfAllIndexesExist": {
   102  			args: args{
   103  				existing: []string{
   104  					"SubnetworkParameters",
   105  					"SubnetworkParameters_2",
   106  					"SubnetworkParameters_3",
   107  					"SubnetworkParameters_4",
   108  					"SubnetworkParameters_5",
   109  					"SubnetworkParameters_6",
   110  					"SubnetworkParameters_7",
   111  					"SubnetworkParameters_8",
   112  					"SubnetworkParameters_9",
   113  				},
   114  				suffix: "Parameters",
   115  				names: []string{
   116  					"Subnetwork",
   117  				},
   118  			},
   119  			want: want{
   120  				err: errors.Errorf("could not generate a unique name for %s", "SubnetworkParameters"),
   121  			},
   122  		},
   123  		"MultipleNamesPrependsBeforeIndexing": {
   124  			args: args{
   125  				existing: []string{
   126  					"SubnetworkParameters",
   127  				},
   128  				suffix: "Parameters",
   129  				names: []string{
   130  					"RouterNat",
   131  					"Subnetwork",
   132  				},
   133  			},
   134  			want: want{
   135  				out: "RouterNatSubnetworkParameters",
   136  				err: nil,
   137  			},
   138  		},
   139  		"MultipleNamesUsesIndexingIfNeeded": {
   140  			args: args{
   141  				existing: []string{
   142  					"SubnetworkParameters",
   143  					"RouterNatSubnetworkParameters",
   144  				},
   145  				suffix: "Parameters",
   146  				names: []string{
   147  					"RouterNat",
   148  					"Subnetwork",
   149  				},
   150  			},
   151  			want: want{
   152  				out: "RouterNatSubnetworkParameters_2",
   153  				err: nil,
   154  			},
   155  		},
   156  		"AnySuffixWouldWorkSame": {
   157  			args: args{
   158  				existing: []string{
   159  					"SubnetworkObservation",
   160  					"SubnetworkObservation_2",
   161  					"SubnetworkObservation_3",
   162  					"SubnetworkObservation_4",
   163  				},
   164  				suffix: "Observation",
   165  				names: []string{
   166  					"Subnetwork",
   167  				},
   168  			},
   169  			want: want{
   170  				out: "SubnetworkObservation_5",
   171  				err: nil,
   172  			},
   173  		},
   174  		"OverrideFieldNames": {
   175  			args: args{
   176  				suffix: "Parameters",
   177  				names: []string{
   178  					"Cluster",
   179  					"Tag",
   180  				},
   181  				overrideFieldNames: map[string]string{
   182  					"TagParameters": "ClusterTagParameters",
   183  				},
   184  			},
   185  			want: want{
   186  				out: "ClusterTagParameters",
   187  				err: nil,
   188  			},
   189  		},
   190  	}
   191  	for n, tc := range cases {
   192  		t.Run(n, func(t *testing.T) {
   193  			p := types.NewPackage("path/to/test", "test")
   194  			for _, s := range tc.existing {
   195  				p.Scope().Insert(types.NewTypeName(token.NoPos, p, s, &types.Struct{}))
   196  			}
   197  
   198  			g := &Builder{
   199  				Package: p,
   200  			}
   201  			got, gotErr := generateTypeName(tc.args.suffix, g.Package, tc.args.overrideFieldNames, tc.args.names...)
   202  			if diff := cmp.Diff(tc.want.err, gotErr, test.EquateErrors()); diff != "" {
   203  				t.Fatalf("generateTypeName(...): -want error, +got error: %s", diff)
   204  			}
   205  			if diff := cmp.Diff(tc.want.out, got); diff != "" {
   206  				t.Errorf("generateTypeName(...) out = %v, want %v", got, tc.want.out)
   207  			}
   208  		})
   209  	}
   210  }
   211  
   212  func TestBuild(t *testing.T) {
   213  	type args struct {
   214  		cfg *config.Resource
   215  	}
   216  	type want struct {
   217  		forProvider     string
   218  		atProvider      string
   219  		validationRules string
   220  		err             error
   221  	}
   222  	cases := map[string]struct {
   223  		args
   224  		want
   225  	}{
   226  		"Base_Types": {
   227  			args: args{
   228  				cfg: &config.Resource{
   229  					TerraformResource: &schema.Resource{
   230  						Schema: map[string]*schema.Schema{
   231  							"name": {
   232  								Type:     schema.TypeString,
   233  								Required: true,
   234  							},
   235  							"id": {
   236  								Type:     schema.TypeInt,
   237  								Required: true,
   238  							},
   239  							"enable": {
   240  								Type:     schema.TypeBool,
   241  								Optional: true,
   242  								Computed: true,
   243  							},
   244  							"value": {
   245  								Type:     schema.TypeFloat,
   246  								Optional: false,
   247  								Computed: true,
   248  							},
   249  							"config": {
   250  								Type:     schema.TypeString,
   251  								Optional: false,
   252  								Computed: true,
   253  							},
   254  						},
   255  					},
   256  				},
   257  			},
   258  			want: want{
   259  				forProvider: `type example.Parameters struct{Enable *bool "json:\"enable,omitempty\" tf:\"enable,omitempty\""; ID *int64 "json:\"id,omitempty\" tf:\"id,omitempty\""; Name *string "json:\"name,omitempty\" tf:\"name,omitempty\""}`,
   260  				atProvider:  `type example.Observation struct{Config *string "json:\"config,omitempty\" tf:\"config,omitempty\""; Enable *bool "json:\"enable,omitempty\" tf:\"enable,omitempty\""; ID *int64 "json:\"id,omitempty\" tf:\"id,omitempty\""; Name *string "json:\"name,omitempty\" tf:\"name,omitempty\""; Value *float64 "json:\"value,omitempty\" tf:\"value,omitempty\""}`,
   261  				validationRules: `
   262  // +kubebuilder:validation:XValidation:rule="!('*' in self.managementPolicies || 'Create' in self.managementPolicies || 'Update' in self.managementPolicies) || has(self.forProvider.id) || (has(self.initProvider) && has(self.initProvider.id))",message="spec.forProvider.id is a required parameter"
   263  // +kubebuilder:validation:XValidation:rule="!('*' in self.managementPolicies || 'Create' in self.managementPolicies || 'Update' in self.managementPolicies) || has(self.forProvider.name) || (has(self.initProvider) && has(self.initProvider.name))",message="spec.forProvider.name is a required parameter"`,
   264  			},
   265  		},
   266  		"Resource_Types": {
   267  			args: args{
   268  				cfg: &config.Resource{
   269  					TerraformResource: &schema.Resource{
   270  						Schema: map[string]*schema.Schema{
   271  							"list": {
   272  								Type:     schema.TypeList,
   273  								Required: true,
   274  								Elem: &schema.Schema{
   275  									Type:     schema.TypeString,
   276  									Required: true,
   277  								},
   278  							},
   279  							"resource_in": {
   280  								Type:     schema.TypeMap,
   281  								Required: true,
   282  								Elem:     &schema.Resource{},
   283  							},
   284  							"resource_out": {
   285  								Type:     schema.TypeMap,
   286  								Optional: false,
   287  								Computed: true,
   288  								Elem:     &schema.Resource{},
   289  							},
   290  						},
   291  					},
   292  				},
   293  			},
   294  			want: want{
   295  				forProvider: `type example.Parameters struct{List []*string "json:\"list,omitempty\" tf:\"list,omitempty\""; ResourceIn map[string]example.ResourceInParameters "json:\"resourceIn,omitempty\" tf:\"resource_in,omitempty\""}`,
   296  				atProvider:  `type example.Observation struct{List []*string "json:\"list,omitempty\" tf:\"list,omitempty\""; ResourceIn map[string]example.ResourceInParameters "json:\"resourceIn,omitempty\" tf:\"resource_in,omitempty\""; ResourceOut map[string]example.ResourceOutObservation "json:\"resourceOut,omitempty\" tf:\"resource_out,omitempty\""}`,
   297  				validationRules: `
   298  // +kubebuilder:validation:XValidation:rule="!('*' in self.managementPolicies || 'Create' in self.managementPolicies || 'Update' in self.managementPolicies) || has(self.forProvider.list) || (has(self.initProvider) && has(self.initProvider.list))",message="spec.forProvider.list is a required parameter"
   299  // +kubebuilder:validation:XValidation:rule="!('*' in self.managementPolicies || 'Create' in self.managementPolicies || 'Update' in self.managementPolicies) || has(self.forProvider.resourceIn) || (has(self.initProvider) && has(self.initProvider.resourceIn))",message="spec.forProvider.resourceIn is a required parameter"`,
   300  			},
   301  		},
   302  		"Sensitive_Fields": {
   303  			args: args{
   304  				cfg: &config.Resource{
   305  					TerraformResource: &schema.Resource{
   306  						Schema: map[string]*schema.Schema{
   307  							"key_1": {
   308  								Type:      schema.TypeString,
   309  								Optional:  true,
   310  								Sensitive: true,
   311  							},
   312  							"key_2": {
   313  								Type:      schema.TypeString,
   314  								Sensitive: true,
   315  							},
   316  							"key_3": {
   317  								Type:      schema.TypeList,
   318  								Sensitive: true,
   319  							},
   320  						},
   321  					},
   322  				},
   323  			},
   324  			want: want{
   325  				forProvider: `type example.Parameters struct{Key1SecretRef *github.com/crossplane/crossplane-runtime/apis/common/v1.SecretKeySelector "json:\"key1SecretRef,omitempty\" tf:\"-\""; Key2SecretRef github.com/crossplane/crossplane-runtime/apis/common/v1.SecretKeySelector "json:\"key2SecretRef\" tf:\"-\""; Key3SecretRef []github.com/crossplane/crossplane-runtime/apis/common/v1.SecretKeySelector "json:\"key3SecretRef\" tf:\"-\""}`,
   326  				atProvider:  `type example.Observation struct{}`,
   327  				validationRules: `
   328  // +kubebuilder:validation:XValidation:rule="!('*' in self.managementPolicies || 'Create' in self.managementPolicies || 'Update' in self.managementPolicies) || has(self.forProvider.key2SecretRef)",message="spec.forProvider.key2SecretRef is a required parameter"
   329  // +kubebuilder:validation:XValidation:rule="!('*' in self.managementPolicies || 'Create' in self.managementPolicies || 'Update' in self.managementPolicies) || has(self.forProvider.key3SecretRef)",message="spec.forProvider.key3SecretRef is a required parameter"`,
   330  			},
   331  		},
   332  		"Invalid_Sensitive_Fields": {
   333  			args: args{
   334  				cfg: &config.Resource{
   335  					Name: "test_resource",
   336  					TerraformResource: &schema.Resource{
   337  						Schema: map[string]*schema.Schema{
   338  							"key_1": {
   339  								Type:      schema.TypeFloat,
   340  								Sensitive: true,
   341  							},
   342  						},
   343  					},
   344  				},
   345  			},
   346  			want: want{
   347  				err: errors.Wrapf(fmt.Errorf(`got type %q for field %q, only types "string", "*string", []string, []*string, "map[string]string" and "map[string]*string" supported as sensitive`, "*float64", "Key1"), `cannot build the Types for resource "test_resource"`),
   348  			},
   349  		},
   350  		"References": {
   351  			args: args{
   352  				cfg: &config.Resource{
   353  					TerraformResource: &schema.Resource{
   354  						Schema: map[string]*schema.Schema{
   355  							"name": {
   356  								Type:     schema.TypeString,
   357  								Required: true,
   358  							},
   359  							"reference_id": {
   360  								Type:     schema.TypeString,
   361  								Required: true,
   362  							},
   363  						},
   364  					},
   365  					References: map[string]config.Reference{
   366  						"reference_id": {
   367  							Type:         "string",
   368  							RefFieldName: "ExternalResourceID",
   369  						},
   370  					},
   371  				},
   372  			},
   373  			want: want{
   374  				forProvider: `type example.Parameters struct{Name *string "json:\"name,omitempty\" tf:\"name,omitempty\""; ReferenceID *string "json:\"referenceId,omitempty\" tf:\"reference_id,omitempty\""; ExternalResourceID *github.com/crossplane/crossplane-runtime/apis/common/v1.Reference "json:\"externalResourceId,omitempty\" tf:\"-\""; ReferenceIDSelector *github.com/crossplane/crossplane-runtime/apis/common/v1.Selector "json:\"referenceIdSelector,omitempty\" tf:\"-\""}`,
   375  				atProvider:  `type example.Observation struct{Name *string "json:\"name,omitempty\" tf:\"name,omitempty\""; ReferenceID *string "json:\"referenceId,omitempty\" tf:\"reference_id,omitempty\""}`,
   376  				validationRules: `
   377  // +kubebuilder:validation:XValidation:rule="!('*' in self.managementPolicies || 'Create' in self.managementPolicies || 'Update' in self.managementPolicies) || has(self.forProvider.name) || (has(self.initProvider) && has(self.initProvider.name))",message="spec.forProvider.name is a required parameter"`,
   378  			},
   379  		},
   380  		"Invalid_Schema_Type": {
   381  			args: args{
   382  				cfg: &config.Resource{
   383  					Name: "test_resource",
   384  					TerraformResource: &schema.Resource{
   385  						Schema: map[string]*schema.Schema{
   386  							"name": {
   387  								Type:     schema.TypeInvalid,
   388  								Required: true,
   389  							},
   390  						},
   391  					},
   392  				},
   393  			},
   394  			want: want{
   395  				err: errors.Wrapf(errors.Wrapf(errors.Errorf("invalid schema type %s", "TypeInvalid"), "cannot infer type from schema of field %s", "name"), `cannot build the Types for resource "test_resource"`),
   396  			},
   397  		},
   398  		"Validation_Rules_With_Keywords": {
   399  			args: args{
   400  				cfg: &config.Resource{
   401  					TerraformResource: &schema.Resource{
   402  						Schema: map[string]*schema.Schema{
   403  							"name": {
   404  								Type:     schema.TypeString,
   405  								Required: true,
   406  							},
   407  							// "namespace" is a cel reserved value and should be wrapped when used in
   408  							// validation rules (i.e., __namespace__)
   409  							"namespace": {
   410  								Type:     schema.TypeString,
   411  								Required: true,
   412  							},
   413  						},
   414  					},
   415  				},
   416  			},
   417  			want: want{
   418  				forProvider: `type example.Parameters struct{Name *string "json:\"name,omitempty\" tf:\"name,omitempty\""; Namespace *string "json:\"namespace,omitempty\" tf:\"namespace,omitempty\""}`,
   419  				atProvider:  `type example.Observation struct{Name *string "json:\"name,omitempty\" tf:\"name,omitempty\""; Namespace *string "json:\"namespace,omitempty\" tf:\"namespace,omitempty\""}`,
   420  				validationRules: `
   421  // +kubebuilder:validation:XValidation:rule="!('*' in self.managementPolicies || 'Create' in self.managementPolicies || 'Update' in self.managementPolicies) || has(self.forProvider.name) || (has(self.initProvider) && has(self.initProvider.name))",message="spec.forProvider.name is a required parameter"
   422  // +kubebuilder:validation:XValidation:rule="!('*' in self.managementPolicies || 'Create' in self.managementPolicies || 'Update' in self.managementPolicies) || has(self.forProvider.__namespace__) || (has(self.initProvider) && has(self.initProvider.__namespace__))",message="spec.forProvider.namespace is a required parameter"`,
   423  			},
   424  		},
   425  	}
   426  	for n, tc := range cases {
   427  		t.Run(n, func(t *testing.T) {
   428  			builder := NewBuilder(types.NewPackage("example", ""))
   429  			g, err := builder.Build(tc.cfg)
   430  
   431  			if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" {
   432  				t.Fatalf("Build(...): -want error, +got error: %s", diff)
   433  			}
   434  			if g.ForProviderType != nil {
   435  				if diff := cmp.Diff(tc.want.forProvider, g.ForProviderType.Obj().String()); diff != "" {
   436  					t.Fatalf("Build(...): -want forProvider, +got forProvider: %s", diff)
   437  				}
   438  			}
   439  			if g.AtProviderType != nil {
   440  				if diff := cmp.Diff(tc.want.atProvider, g.AtProviderType.Obj().String()); diff != "" {
   441  					t.Fatalf("Build(...): -want atProvider, +got atProvider: %s", diff)
   442  				}
   443  			}
   444  			if diff := cmp.Diff(tc.want.validationRules, g.ValidationRules); diff != "" {
   445  				t.Fatalf("Build(...): -want validationRules, +got validationRules: %s", diff)
   446  			}
   447  		})
   448  	}
   449  }