github.com/terramate-io/tf@v0.0.0-20230830114523-fce866b4dfcd/legacy/helper/schema/core_schema_test.go (about) 1 // Copyright (c) HashiCorp, Inc. 2 // SPDX-License-Identifier: MPL-2.0 3 4 package schema 5 6 import ( 7 "fmt" 8 "testing" 9 10 "github.com/google/go-cmp/cmp" 11 "github.com/zclconf/go-cty/cty" 12 13 "github.com/terramate-io/tf/configs/configschema" 14 ) 15 16 // add the implicit "id" attribute for test resources 17 func testResource(block *configschema.Block) *configschema.Block { 18 if block.Attributes == nil { 19 block.Attributes = make(map[string]*configschema.Attribute) 20 } 21 22 if block.BlockTypes == nil { 23 block.BlockTypes = make(map[string]*configschema.NestedBlock) 24 } 25 26 if block.Attributes["id"] == nil { 27 block.Attributes["id"] = &configschema.Attribute{ 28 Type: cty.String, 29 Optional: true, 30 Computed: true, 31 } 32 } 33 return block 34 } 35 36 func TestSchemaMapCoreConfigSchema(t *testing.T) { 37 tests := map[string]struct { 38 Schema map[string]*Schema 39 Want *configschema.Block 40 }{ 41 "empty": { 42 map[string]*Schema{}, 43 testResource(&configschema.Block{}), 44 }, 45 "primitives": { 46 map[string]*Schema{ 47 "int": { 48 Type: TypeInt, 49 Required: true, 50 Description: "foo bar baz", 51 }, 52 "float": { 53 Type: TypeFloat, 54 Optional: true, 55 }, 56 "bool": { 57 Type: TypeBool, 58 Computed: true, 59 }, 60 "string": { 61 Type: TypeString, 62 Optional: true, 63 Computed: true, 64 }, 65 }, 66 testResource(&configschema.Block{ 67 Attributes: map[string]*configschema.Attribute{ 68 "int": { 69 Type: cty.Number, 70 Required: true, 71 Description: "foo bar baz", 72 }, 73 "float": { 74 Type: cty.Number, 75 Optional: true, 76 }, 77 "bool": { 78 Type: cty.Bool, 79 Computed: true, 80 }, 81 "string": { 82 Type: cty.String, 83 Optional: true, 84 Computed: true, 85 }, 86 }, 87 BlockTypes: map[string]*configschema.NestedBlock{}, 88 }), 89 }, 90 "simple collections": { 91 map[string]*Schema{ 92 "list": { 93 Type: TypeList, 94 Required: true, 95 Elem: &Schema{ 96 Type: TypeInt, 97 }, 98 }, 99 "set": { 100 Type: TypeSet, 101 Optional: true, 102 Elem: &Schema{ 103 Type: TypeString, 104 }, 105 }, 106 "map": { 107 Type: TypeMap, 108 Optional: true, 109 Elem: &Schema{ 110 Type: TypeBool, 111 }, 112 }, 113 "map_default_type": { 114 Type: TypeMap, 115 Optional: true, 116 // Maps historically don't have elements because we 117 // assumed they would be strings, so this needs to work 118 // for pre-existing schemas. 119 }, 120 }, 121 testResource(&configschema.Block{ 122 Attributes: map[string]*configschema.Attribute{ 123 "list": { 124 Type: cty.List(cty.Number), 125 Required: true, 126 }, 127 "set": { 128 Type: cty.Set(cty.String), 129 Optional: true, 130 }, 131 "map": { 132 Type: cty.Map(cty.Bool), 133 Optional: true, 134 }, 135 "map_default_type": { 136 Type: cty.Map(cty.String), 137 Optional: true, 138 }, 139 }, 140 BlockTypes: map[string]*configschema.NestedBlock{}, 141 }), 142 }, 143 "incorrectly-specified collections": { 144 // Historically we tolerated setting a type directly as the Elem 145 // attribute, rather than a Schema object. This is common enough 146 // in existing provider code that we must support it as an alias 147 // for a schema object with the given type. 148 map[string]*Schema{ 149 "list": { 150 Type: TypeList, 151 Required: true, 152 Elem: TypeInt, 153 }, 154 "set": { 155 Type: TypeSet, 156 Optional: true, 157 Elem: TypeString, 158 }, 159 "map": { 160 Type: TypeMap, 161 Optional: true, 162 Elem: TypeBool, 163 }, 164 }, 165 testResource(&configschema.Block{ 166 Attributes: map[string]*configschema.Attribute{ 167 "list": { 168 Type: cty.List(cty.Number), 169 Required: true, 170 }, 171 "set": { 172 Type: cty.Set(cty.String), 173 Optional: true, 174 }, 175 "map": { 176 Type: cty.Map(cty.Bool), 177 Optional: true, 178 }, 179 }, 180 BlockTypes: map[string]*configschema.NestedBlock{}, 181 }), 182 }, 183 "sub-resource collections": { 184 map[string]*Schema{ 185 "list": { 186 Type: TypeList, 187 Required: true, 188 Elem: &Resource{ 189 Schema: map[string]*Schema{}, 190 }, 191 MinItems: 1, 192 MaxItems: 2, 193 }, 194 "set": { 195 Type: TypeSet, 196 Required: true, 197 Elem: &Resource{ 198 Schema: map[string]*Schema{}, 199 }, 200 }, 201 "map": { 202 Type: TypeMap, 203 Optional: true, 204 Elem: &Resource{ 205 Schema: map[string]*Schema{}, 206 }, 207 }, 208 }, 209 testResource(&configschema.Block{ 210 Attributes: map[string]*configschema.Attribute{ 211 // This one becomes a string attribute because helper/schema 212 // doesn't actually support maps of resource. The given 213 // "Elem" is just ignored entirely here, which is important 214 // because that is also true of the helper/schema logic and 215 // existing providers rely on this being ignored for 216 // correct operation. 217 "map": { 218 Type: cty.Map(cty.String), 219 Optional: true, 220 }, 221 }, 222 BlockTypes: map[string]*configschema.NestedBlock{ 223 "list": { 224 Nesting: configschema.NestingList, 225 Block: configschema.Block{}, 226 MinItems: 1, 227 MaxItems: 2, 228 }, 229 "set": { 230 Nesting: configschema.NestingSet, 231 Block: configschema.Block{}, 232 MinItems: 1, // because schema is Required 233 }, 234 }, 235 }), 236 }, 237 "sub-resource collections minitems+optional": { 238 // This particular case is an odd one where the provider gives 239 // conflicting information about whether a sub-resource is required, 240 // by marking it as optional but also requiring one item. 241 // Historically the optional-ness "won" here, and so we must 242 // honor that for compatibility with providers that relied on this 243 // undocumented interaction. 244 map[string]*Schema{ 245 "list": { 246 Type: TypeList, 247 Optional: true, 248 Elem: &Resource{ 249 Schema: map[string]*Schema{}, 250 }, 251 MinItems: 1, 252 MaxItems: 1, 253 }, 254 "set": { 255 Type: TypeSet, 256 Optional: true, 257 Elem: &Resource{ 258 Schema: map[string]*Schema{}, 259 }, 260 MinItems: 1, 261 MaxItems: 1, 262 }, 263 }, 264 testResource(&configschema.Block{ 265 Attributes: map[string]*configschema.Attribute{}, 266 BlockTypes: map[string]*configschema.NestedBlock{ 267 "list": { 268 Nesting: configschema.NestingList, 269 Block: configschema.Block{}, 270 MinItems: 0, 271 MaxItems: 1, 272 }, 273 "set": { 274 Nesting: configschema.NestingSet, 275 Block: configschema.Block{}, 276 MinItems: 0, 277 MaxItems: 1, 278 }, 279 }, 280 }), 281 }, 282 "sub-resource collections minitems+computed": { 283 map[string]*Schema{ 284 "list": { 285 Type: TypeList, 286 Computed: true, 287 Elem: &Resource{ 288 Schema: map[string]*Schema{}, 289 }, 290 MinItems: 1, 291 MaxItems: 1, 292 }, 293 "set": { 294 Type: TypeSet, 295 Computed: true, 296 Elem: &Resource{ 297 Schema: map[string]*Schema{}, 298 }, 299 MinItems: 1, 300 MaxItems: 1, 301 }, 302 }, 303 testResource(&configschema.Block{ 304 Attributes: map[string]*configschema.Attribute{ 305 "list": { 306 Type: cty.List(cty.EmptyObject), 307 Computed: true, 308 }, 309 "set": { 310 Type: cty.Set(cty.EmptyObject), 311 Computed: true, 312 }, 313 }, 314 }), 315 }, 316 "nested attributes and blocks": { 317 map[string]*Schema{ 318 "foo": { 319 Type: TypeList, 320 Required: true, 321 Elem: &Resource{ 322 Schema: map[string]*Schema{ 323 "bar": { 324 Type: TypeList, 325 Required: true, 326 Elem: &Schema{ 327 Type: TypeList, 328 Elem: &Schema{ 329 Type: TypeString, 330 }, 331 }, 332 }, 333 "baz": { 334 Type: TypeSet, 335 Optional: true, 336 Elem: &Resource{ 337 Schema: map[string]*Schema{}, 338 }, 339 }, 340 }, 341 }, 342 }, 343 }, 344 testResource(&configschema.Block{ 345 Attributes: map[string]*configschema.Attribute{}, 346 BlockTypes: map[string]*configschema.NestedBlock{ 347 "foo": &configschema.NestedBlock{ 348 Nesting: configschema.NestingList, 349 Block: configschema.Block{ 350 Attributes: map[string]*configschema.Attribute{ 351 "bar": { 352 Type: cty.List(cty.List(cty.String)), 353 Required: true, 354 }, 355 }, 356 BlockTypes: map[string]*configschema.NestedBlock{ 357 "baz": { 358 Nesting: configschema.NestingSet, 359 Block: configschema.Block{}, 360 }, 361 }, 362 }, 363 MinItems: 1, // because schema is Required 364 }, 365 }, 366 }), 367 }, 368 "sensitive": { 369 map[string]*Schema{ 370 "string": { 371 Type: TypeString, 372 Optional: true, 373 Sensitive: true, 374 }, 375 }, 376 testResource(&configschema.Block{ 377 Attributes: map[string]*configschema.Attribute{ 378 "string": { 379 Type: cty.String, 380 Optional: true, 381 Sensitive: true, 382 }, 383 }, 384 BlockTypes: map[string]*configschema.NestedBlock{}, 385 }), 386 }, 387 "conditionally required on": { 388 map[string]*Schema{ 389 "string": { 390 Type: TypeString, 391 Required: true, 392 DefaultFunc: func() (interface{}, error) { 393 return nil, nil 394 }, 395 }, 396 }, 397 testResource(&configschema.Block{ 398 Attributes: map[string]*configschema.Attribute{ 399 "string": { 400 Type: cty.String, 401 Required: true, 402 }, 403 }, 404 BlockTypes: map[string]*configschema.NestedBlock{}, 405 }), 406 }, 407 "conditionally required off": { 408 map[string]*Schema{ 409 "string": { 410 Type: TypeString, 411 Required: true, 412 DefaultFunc: func() (interface{}, error) { 413 // If we return a non-nil default then this overrides 414 // the "Required: true" for the purpose of building 415 // the core schema, so that core will ignore it not 416 // being set and let the provider handle it. 417 return "boop", nil 418 }, 419 }, 420 }, 421 testResource(&configschema.Block{ 422 Attributes: map[string]*configschema.Attribute{ 423 "string": { 424 Type: cty.String, 425 Optional: true, 426 }, 427 }, 428 BlockTypes: map[string]*configschema.NestedBlock{}, 429 }), 430 }, 431 "conditionally required error": { 432 map[string]*Schema{ 433 "string": { 434 Type: TypeString, 435 Required: true, 436 DefaultFunc: func() (interface{}, error) { 437 return nil, fmt.Errorf("placeholder error") 438 }, 439 }, 440 }, 441 testResource(&configschema.Block{ 442 Attributes: map[string]*configschema.Attribute{ 443 "string": { 444 Type: cty.String, 445 Optional: true, // Just so we can progress to provider-driven validation and return the error there 446 }, 447 }, 448 BlockTypes: map[string]*configschema.NestedBlock{}, 449 }), 450 }, 451 } 452 453 for name, test := range tests { 454 t.Run(name, func(t *testing.T) { 455 got := (&Resource{Schema: test.Schema}).CoreConfigSchema() 456 if !cmp.Equal(got, test.Want, equateEmpty, typeComparer) { 457 t.Error(cmp.Diff(got, test.Want, equateEmpty, typeComparer)) 458 } 459 }) 460 } 461 }