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 }