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