github.com/hashicorp/terraform-plugin-sdk@v1.17.2/internal/configs/configschema/decoder_spec_test.go (about) 1 package configschema 2 3 import ( 4 "testing" 5 6 "github.com/apparentlymart/go-dump/dump" 7 "github.com/davecgh/go-spew/spew" 8 9 "github.com/hashicorp/hcl/v2" 10 "github.com/hashicorp/hcl/v2/hcldec" 11 "github.com/hashicorp/hcl/v2/hcltest" 12 "github.com/zclconf/go-cty/cty" 13 ) 14 15 func TestBlockDecoderSpec(t *testing.T) { 16 tests := map[string]struct { 17 Schema *Block 18 TestBody hcl.Body 19 Want cty.Value 20 DiagCount int 21 }{ 22 "empty": { 23 &Block{}, 24 hcl.EmptyBody(), 25 cty.EmptyObjectVal, 26 0, 27 }, 28 "nil": { 29 nil, 30 hcl.EmptyBody(), 31 cty.EmptyObjectVal, 32 0, 33 }, 34 "attributes": { 35 &Block{ 36 Attributes: map[string]*Attribute{ 37 "optional": { 38 Type: cty.Number, 39 Optional: true, 40 }, 41 "required": { 42 Type: cty.String, 43 Required: true, 44 }, 45 "computed": { 46 Type: cty.List(cty.Bool), 47 Computed: true, 48 }, 49 "optional_computed": { 50 Type: cty.Map(cty.Bool), 51 Optional: true, 52 Computed: true, 53 }, 54 "optional_computed_overridden": { 55 Type: cty.Bool, 56 Optional: true, 57 Computed: true, 58 }, 59 "optional_computed_unknown": { 60 Type: cty.String, 61 Optional: true, 62 Computed: true, 63 }, 64 }, 65 }, 66 hcltest.MockBody(&hcl.BodyContent{ 67 Attributes: hcl.Attributes{ 68 "required": { 69 Name: "required", 70 Expr: hcltest.MockExprLiteral(cty.NumberIntVal(5)), 71 }, 72 "optional_computed_overridden": { 73 Name: "optional_computed_overridden", 74 Expr: hcltest.MockExprLiteral(cty.True), 75 }, 76 "optional_computed_unknown": { 77 Name: "optional_computed_overridden", 78 Expr: hcltest.MockExprLiteral(cty.UnknownVal(cty.String)), 79 }, 80 }, 81 }), 82 cty.ObjectVal(map[string]cty.Value{ 83 "optional": cty.NullVal(cty.Number), 84 "required": cty.StringVal("5"), // converted from number to string 85 "computed": cty.NullVal(cty.List(cty.Bool)), 86 "optional_computed": cty.NullVal(cty.Map(cty.Bool)), 87 "optional_computed_overridden": cty.True, 88 "optional_computed_unknown": cty.UnknownVal(cty.String), 89 }), 90 0, 91 }, 92 "dynamically-typed attribute": { 93 &Block{ 94 Attributes: map[string]*Attribute{ 95 "foo": { 96 Type: cty.DynamicPseudoType, // any type is permitted 97 Required: true, 98 }, 99 }, 100 }, 101 hcltest.MockBody(&hcl.BodyContent{ 102 Attributes: hcl.Attributes{ 103 "foo": { 104 Name: "foo", 105 Expr: hcltest.MockExprLiteral(cty.True), 106 }, 107 }, 108 }), 109 cty.ObjectVal(map[string]cty.Value{ 110 "foo": cty.True, 111 }), 112 0, 113 }, 114 "dynamically-typed attribute omitted": { 115 &Block{ 116 Attributes: map[string]*Attribute{ 117 "foo": { 118 Type: cty.DynamicPseudoType, // any type is permitted 119 Optional: true, 120 }, 121 }, 122 }, 123 hcltest.MockBody(&hcl.BodyContent{}), 124 cty.ObjectVal(map[string]cty.Value{ 125 "foo": cty.NullVal(cty.DynamicPseudoType), 126 }), 127 0, 128 }, 129 "required attribute omitted": { 130 &Block{ 131 Attributes: map[string]*Attribute{ 132 "foo": { 133 Type: cty.Bool, 134 Required: true, 135 }, 136 }, 137 }, 138 hcltest.MockBody(&hcl.BodyContent{}), 139 cty.ObjectVal(map[string]cty.Value{ 140 "foo": cty.NullVal(cty.Bool), 141 }), 142 1, // missing required attribute 143 }, 144 "wrong attribute type": { 145 &Block{ 146 Attributes: map[string]*Attribute{ 147 "optional": { 148 Type: cty.Number, 149 Optional: true, 150 }, 151 }, 152 }, 153 hcltest.MockBody(&hcl.BodyContent{ 154 Attributes: hcl.Attributes{ 155 "optional": { 156 Name: "optional", 157 Expr: hcltest.MockExprLiteral(cty.True), 158 }, 159 }, 160 }), 161 cty.ObjectVal(map[string]cty.Value{ 162 "optional": cty.UnknownVal(cty.Number), 163 }), 164 1, // incorrect type; number required 165 }, 166 "blocks": { 167 &Block{ 168 BlockTypes: map[string]*NestedBlock{ 169 "single": { 170 Nesting: NestingSingle, 171 Block: Block{}, 172 }, 173 "list": { 174 Nesting: NestingList, 175 Block: Block{}, 176 }, 177 "set": { 178 Nesting: NestingSet, 179 Block: Block{}, 180 }, 181 "map": { 182 Nesting: NestingMap, 183 Block: Block{}, 184 }, 185 }, 186 }, 187 hcltest.MockBody(&hcl.BodyContent{ 188 Blocks: hcl.Blocks{ 189 &hcl.Block{ 190 Type: "list", 191 Body: hcl.EmptyBody(), 192 }, 193 &hcl.Block{ 194 Type: "single", 195 Body: hcl.EmptyBody(), 196 }, 197 &hcl.Block{ 198 Type: "list", 199 Body: hcl.EmptyBody(), 200 }, 201 &hcl.Block{ 202 Type: "set", 203 Body: hcl.EmptyBody(), 204 }, 205 &hcl.Block{ 206 Type: "map", 207 Labels: []string{"foo"}, 208 LabelRanges: []hcl.Range{{}}, 209 Body: hcl.EmptyBody(), 210 }, 211 &hcl.Block{ 212 Type: "map", 213 Labels: []string{"bar"}, 214 LabelRanges: []hcl.Range{{}}, 215 Body: hcl.EmptyBody(), 216 }, 217 &hcl.Block{ 218 Type: "set", 219 Body: hcl.EmptyBody(), 220 }, 221 }, 222 }), 223 cty.ObjectVal(map[string]cty.Value{ 224 "single": cty.EmptyObjectVal, 225 "list": cty.ListVal([]cty.Value{ 226 cty.EmptyObjectVal, 227 cty.EmptyObjectVal, 228 }), 229 "set": cty.SetVal([]cty.Value{ 230 cty.EmptyObjectVal, 231 cty.EmptyObjectVal, 232 }), 233 "map": cty.MapVal(map[string]cty.Value{ 234 "foo": cty.EmptyObjectVal, 235 "bar": cty.EmptyObjectVal, 236 }), 237 }), 238 0, 239 }, 240 "blocks with dynamically-typed attributes": { 241 &Block{ 242 BlockTypes: map[string]*NestedBlock{ 243 "single": { 244 Nesting: NestingSingle, 245 Block: Block{ 246 Attributes: map[string]*Attribute{ 247 "a": { 248 Type: cty.DynamicPseudoType, 249 Optional: true, 250 }, 251 }, 252 }, 253 }, 254 "list": { 255 Nesting: NestingList, 256 Block: Block{ 257 Attributes: map[string]*Attribute{ 258 "a": { 259 Type: cty.DynamicPseudoType, 260 Optional: true, 261 }, 262 }, 263 }, 264 }, 265 "map": { 266 Nesting: NestingMap, 267 Block: Block{ 268 Attributes: map[string]*Attribute{ 269 "a": { 270 Type: cty.DynamicPseudoType, 271 Optional: true, 272 }, 273 }, 274 }, 275 }, 276 }, 277 }, 278 hcltest.MockBody(&hcl.BodyContent{ 279 Blocks: hcl.Blocks{ 280 &hcl.Block{ 281 Type: "list", 282 Body: hcl.EmptyBody(), 283 }, 284 &hcl.Block{ 285 Type: "single", 286 Body: hcl.EmptyBody(), 287 }, 288 &hcl.Block{ 289 Type: "list", 290 Body: hcl.EmptyBody(), 291 }, 292 &hcl.Block{ 293 Type: "map", 294 Labels: []string{"foo"}, 295 LabelRanges: []hcl.Range{{}}, 296 Body: hcl.EmptyBody(), 297 }, 298 &hcl.Block{ 299 Type: "map", 300 Labels: []string{"bar"}, 301 LabelRanges: []hcl.Range{{}}, 302 Body: hcl.EmptyBody(), 303 }, 304 }, 305 }), 306 cty.ObjectVal(map[string]cty.Value{ 307 "single": cty.ObjectVal(map[string]cty.Value{ 308 "a": cty.NullVal(cty.DynamicPseudoType), 309 }), 310 "list": cty.TupleVal([]cty.Value{ 311 cty.ObjectVal(map[string]cty.Value{ 312 "a": cty.NullVal(cty.DynamicPseudoType), 313 }), 314 cty.ObjectVal(map[string]cty.Value{ 315 "a": cty.NullVal(cty.DynamicPseudoType), 316 }), 317 }), 318 "map": cty.ObjectVal(map[string]cty.Value{ 319 "foo": cty.ObjectVal(map[string]cty.Value{ 320 "a": cty.NullVal(cty.DynamicPseudoType), 321 }), 322 "bar": cty.ObjectVal(map[string]cty.Value{ 323 "a": cty.NullVal(cty.DynamicPseudoType), 324 }), 325 }), 326 }), 327 0, 328 }, 329 "too many list items": { 330 &Block{ 331 BlockTypes: map[string]*NestedBlock{ 332 "foo": { 333 Nesting: NestingList, 334 Block: Block{}, 335 MaxItems: 1, 336 }, 337 }, 338 }, 339 hcltest.MockBody(&hcl.BodyContent{ 340 Blocks: hcl.Blocks{ 341 &hcl.Block{ 342 Type: "foo", 343 Body: hcl.EmptyBody(), 344 }, 345 &hcl.Block{ 346 Type: "foo", 347 Body: hcl.EmptyBody(), 348 }, 349 }, 350 }), 351 cty.ObjectVal(map[string]cty.Value{ 352 "foo": cty.ListVal([]cty.Value{ 353 cty.EmptyObjectVal, 354 cty.EmptyObjectVal, 355 }), 356 }), 357 0, // max items cannot be validated during decode 358 }, 359 // dynamic blocks may fulfill MinItems, but there is only one block to 360 // decode. 361 "required MinItems": { 362 &Block{ 363 BlockTypes: map[string]*NestedBlock{ 364 "foo": { 365 Nesting: NestingList, 366 Block: Block{}, 367 MinItems: 2, 368 }, 369 }, 370 }, 371 hcltest.MockBody(&hcl.BodyContent{ 372 Blocks: hcl.Blocks{ 373 &hcl.Block{ 374 Type: "foo", 375 Body: hcl.EmptyBody(), 376 }, 377 }, 378 }), 379 cty.ObjectVal(map[string]cty.Value{ 380 "foo": cty.ListVal([]cty.Value{ 381 cty.EmptyObjectVal, 382 }), 383 }), 384 0, 385 }, 386 "extraneous attribute": { 387 &Block{}, 388 hcltest.MockBody(&hcl.BodyContent{ 389 Attributes: hcl.Attributes{ 390 "extra": { 391 Name: "extra", 392 Expr: hcltest.MockExprLiteral(cty.StringVal("hello")), 393 }, 394 }, 395 }), 396 cty.EmptyObjectVal, 397 1, // extraneous attribute 398 }, 399 } 400 401 for name, test := range tests { 402 t.Run(name, func(t *testing.T) { 403 spec := test.Schema.DecoderSpec() 404 got, diags := hcldec.Decode(test.TestBody, spec, nil) 405 if len(diags) != test.DiagCount { 406 t.Errorf("wrong number of diagnostics %d; want %d", len(diags), test.DiagCount) 407 for _, diag := range diags { 408 t.Logf("- %s", diag.Error()) 409 } 410 } 411 412 if !got.RawEquals(test.Want) { 413 t.Logf("[INFO] implied schema is %s", spew.Sdump(hcldec.ImpliedSchema(spec))) 414 t.Errorf("wrong result\ngot: %s\nwant: %s", dump.Value(got), dump.Value(test.Want)) 415 } 416 417 // Double-check that we're producing consistent results for DecoderSpec 418 // and ImpliedType. 419 impliedType := test.Schema.ImpliedType() 420 if errs := got.Type().TestConformance(impliedType); len(errs) != 0 { 421 t.Errorf("result does not conform to the schema's implied type") 422 for _, err := range errs { 423 t.Logf("- %s", err.Error()) 424 } 425 } 426 }) 427 } 428 }