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