github.com/terramate-io/tf@v0.0.0-20230830114523-fce866b4dfcd/configs/configschema/coerce_value_test.go (about) 1 // Copyright (c) HashiCorp, Inc. 2 // SPDX-License-Identifier: MPL-2.0 3 4 package configschema 5 6 import ( 7 "testing" 8 9 "github.com/zclconf/go-cty/cty" 10 11 "github.com/terramate-io/tf/tfdiags" 12 ) 13 14 func TestCoerceValue(t *testing.T) { 15 tests := map[string]struct { 16 Schema *Block 17 Input cty.Value 18 WantValue cty.Value 19 WantErr string 20 }{ 21 "empty schema and value": { 22 &Block{}, 23 cty.EmptyObjectVal, 24 cty.EmptyObjectVal, 25 ``, 26 }, 27 "attribute present": { 28 &Block{ 29 Attributes: map[string]*Attribute{ 30 "foo": { 31 Type: cty.String, 32 Optional: true, 33 }, 34 }, 35 }, 36 cty.ObjectVal(map[string]cty.Value{ 37 "foo": cty.True, 38 }), 39 cty.ObjectVal(map[string]cty.Value{ 40 "foo": cty.StringVal("true"), 41 }), 42 ``, 43 }, 44 "single block present": { 45 &Block{ 46 BlockTypes: map[string]*NestedBlock{ 47 "foo": { 48 Block: Block{}, 49 Nesting: NestingSingle, 50 }, 51 }, 52 }, 53 cty.ObjectVal(map[string]cty.Value{ 54 "foo": cty.EmptyObjectVal, 55 }), 56 cty.ObjectVal(map[string]cty.Value{ 57 "foo": cty.EmptyObjectVal, 58 }), 59 ``, 60 }, 61 "single block wrong type": { 62 &Block{ 63 BlockTypes: map[string]*NestedBlock{ 64 "foo": { 65 Block: Block{}, 66 Nesting: NestingSingle, 67 }, 68 }, 69 }, 70 cty.ObjectVal(map[string]cty.Value{ 71 "foo": cty.True, 72 }), 73 cty.DynamicVal, 74 `.foo: an object is required`, 75 }, 76 "list block with one item": { 77 &Block{ 78 BlockTypes: map[string]*NestedBlock{ 79 "foo": { 80 Block: Block{}, 81 Nesting: NestingList, 82 }, 83 }, 84 }, 85 cty.ObjectVal(map[string]cty.Value{ 86 "foo": cty.ListVal([]cty.Value{cty.EmptyObjectVal}), 87 }), 88 cty.ObjectVal(map[string]cty.Value{ 89 "foo": cty.ListVal([]cty.Value{cty.EmptyObjectVal}), 90 }), 91 ``, 92 }, 93 "set block with one item": { 94 &Block{ 95 BlockTypes: map[string]*NestedBlock{ 96 "foo": { 97 Block: Block{}, 98 Nesting: NestingSet, 99 }, 100 }, 101 }, 102 cty.ObjectVal(map[string]cty.Value{ 103 "foo": cty.ListVal([]cty.Value{cty.EmptyObjectVal}), // can implicitly convert to set 104 }), 105 cty.ObjectVal(map[string]cty.Value{ 106 "foo": cty.SetVal([]cty.Value{cty.EmptyObjectVal}), 107 }), 108 ``, 109 }, 110 "map block with one item": { 111 &Block{ 112 BlockTypes: map[string]*NestedBlock{ 113 "foo": { 114 Block: Block{}, 115 Nesting: NestingMap, 116 }, 117 }, 118 }, 119 cty.ObjectVal(map[string]cty.Value{ 120 "foo": cty.MapVal(map[string]cty.Value{"foo": cty.EmptyObjectVal}), 121 }), 122 cty.ObjectVal(map[string]cty.Value{ 123 "foo": cty.MapVal(map[string]cty.Value{"foo": cty.EmptyObjectVal}), 124 }), 125 ``, 126 }, 127 "list block with one item having an attribute": { 128 &Block{ 129 BlockTypes: map[string]*NestedBlock{ 130 "foo": { 131 Block: Block{ 132 Attributes: map[string]*Attribute{ 133 "bar": { 134 Type: cty.String, 135 Required: true, 136 }, 137 }, 138 }, 139 Nesting: NestingList, 140 }, 141 }, 142 }, 143 cty.ObjectVal(map[string]cty.Value{ 144 "foo": cty.ListVal([]cty.Value{cty.ObjectVal(map[string]cty.Value{ 145 "bar": cty.StringVal("hello"), 146 })}), 147 }), 148 cty.ObjectVal(map[string]cty.Value{ 149 "foo": cty.ListVal([]cty.Value{cty.ObjectVal(map[string]cty.Value{ 150 "bar": cty.StringVal("hello"), 151 })}), 152 }), 153 ``, 154 }, 155 "list block with one item having a missing attribute": { 156 &Block{ 157 BlockTypes: map[string]*NestedBlock{ 158 "foo": { 159 Block: Block{ 160 Attributes: map[string]*Attribute{ 161 "bar": { 162 Type: cty.String, 163 Required: true, 164 }, 165 }, 166 }, 167 Nesting: NestingList, 168 }, 169 }, 170 }, 171 cty.ObjectVal(map[string]cty.Value{ 172 "foo": cty.ListVal([]cty.Value{cty.EmptyObjectVal}), 173 }), 174 cty.DynamicVal, 175 `.foo[0]: attribute "bar" is required`, 176 }, 177 "list block with one item having an extraneous attribute": { 178 &Block{ 179 BlockTypes: map[string]*NestedBlock{ 180 "foo": { 181 Block: Block{}, 182 Nesting: NestingList, 183 }, 184 }, 185 }, 186 cty.ObjectVal(map[string]cty.Value{ 187 "foo": cty.ListVal([]cty.Value{cty.ObjectVal(map[string]cty.Value{ 188 "bar": cty.StringVal("hello"), 189 })}), 190 }), 191 cty.DynamicVal, 192 `.foo[0]: unexpected attribute "bar"`, 193 }, 194 "missing optional attribute": { 195 &Block{ 196 Attributes: map[string]*Attribute{ 197 "foo": { 198 Type: cty.String, 199 Optional: true, 200 }, 201 }, 202 }, 203 cty.EmptyObjectVal, 204 cty.ObjectVal(map[string]cty.Value{ 205 "foo": cty.NullVal(cty.String), 206 }), 207 ``, 208 }, 209 "missing optional single block": { 210 &Block{ 211 BlockTypes: map[string]*NestedBlock{ 212 "foo": { 213 Block: Block{}, 214 Nesting: NestingSingle, 215 }, 216 }, 217 }, 218 cty.EmptyObjectVal, 219 cty.ObjectVal(map[string]cty.Value{ 220 "foo": cty.NullVal(cty.EmptyObject), 221 }), 222 ``, 223 }, 224 "missing optional list block": { 225 &Block{ 226 BlockTypes: map[string]*NestedBlock{ 227 "foo": { 228 Block: Block{}, 229 Nesting: NestingList, 230 }, 231 }, 232 }, 233 cty.EmptyObjectVal, 234 cty.ObjectVal(map[string]cty.Value{ 235 "foo": cty.ListValEmpty(cty.EmptyObject), 236 }), 237 ``, 238 }, 239 "missing optional set block": { 240 &Block{ 241 BlockTypes: map[string]*NestedBlock{ 242 "foo": { 243 Block: Block{}, 244 Nesting: NestingSet, 245 }, 246 }, 247 }, 248 cty.EmptyObjectVal, 249 cty.ObjectVal(map[string]cty.Value{ 250 "foo": cty.SetValEmpty(cty.EmptyObject), 251 }), 252 ``, 253 }, 254 "missing optional map block": { 255 &Block{ 256 BlockTypes: map[string]*NestedBlock{ 257 "foo": { 258 Block: Block{}, 259 Nesting: NestingMap, 260 }, 261 }, 262 }, 263 cty.EmptyObjectVal, 264 cty.ObjectVal(map[string]cty.Value{ 265 "foo": cty.MapValEmpty(cty.EmptyObject), 266 }), 267 ``, 268 }, 269 "missing required attribute": { 270 &Block{ 271 Attributes: map[string]*Attribute{ 272 "foo": { 273 Type: cty.String, 274 Required: true, 275 }, 276 }, 277 }, 278 cty.EmptyObjectVal, 279 cty.DynamicVal, 280 `attribute "foo" is required`, 281 }, 282 "missing required single block": { 283 &Block{ 284 BlockTypes: map[string]*NestedBlock{ 285 "foo": { 286 Block: Block{}, 287 Nesting: NestingSingle, 288 MinItems: 1, 289 MaxItems: 1, 290 }, 291 }, 292 }, 293 cty.EmptyObjectVal, 294 cty.ObjectVal(map[string]cty.Value{ 295 "foo": cty.NullVal(cty.EmptyObject), 296 }), 297 ``, 298 }, 299 "unknown nested list": { 300 &Block{ 301 Attributes: map[string]*Attribute{ 302 "attr": { 303 Type: cty.String, 304 Required: true, 305 }, 306 }, 307 BlockTypes: map[string]*NestedBlock{ 308 "foo": { 309 Block: Block{}, 310 Nesting: NestingList, 311 MinItems: 2, 312 }, 313 }, 314 }, 315 cty.ObjectVal(map[string]cty.Value{ 316 "attr": cty.StringVal("test"), 317 "foo": cty.UnknownVal(cty.EmptyObject), 318 }), 319 cty.ObjectVal(map[string]cty.Value{ 320 "attr": cty.StringVal("test"), 321 "foo": cty.UnknownVal(cty.List(cty.EmptyObject)), 322 }), 323 "", 324 }, 325 "unknowns in nested list": { 326 &Block{ 327 BlockTypes: map[string]*NestedBlock{ 328 "foo": { 329 Block: Block{ 330 Attributes: map[string]*Attribute{ 331 "attr": { 332 Type: cty.String, 333 Required: true, 334 }, 335 }, 336 }, 337 Nesting: NestingList, 338 MinItems: 2, 339 }, 340 }, 341 }, 342 cty.ObjectVal(map[string]cty.Value{ 343 "foo": cty.ListVal([]cty.Value{ 344 cty.ObjectVal(map[string]cty.Value{ 345 "attr": cty.UnknownVal(cty.String), 346 }), 347 }), 348 }), 349 cty.ObjectVal(map[string]cty.Value{ 350 "foo": cty.ListVal([]cty.Value{ 351 cty.ObjectVal(map[string]cty.Value{ 352 "attr": cty.UnknownVal(cty.String), 353 }), 354 }), 355 }), 356 "", 357 }, 358 "unknown nested set": { 359 &Block{ 360 Attributes: map[string]*Attribute{ 361 "attr": { 362 Type: cty.String, 363 Required: true, 364 }, 365 }, 366 BlockTypes: map[string]*NestedBlock{ 367 "foo": { 368 Block: Block{}, 369 Nesting: NestingSet, 370 MinItems: 1, 371 }, 372 }, 373 }, 374 cty.ObjectVal(map[string]cty.Value{ 375 "attr": cty.StringVal("test"), 376 "foo": cty.UnknownVal(cty.EmptyObject), 377 }), 378 cty.ObjectVal(map[string]cty.Value{ 379 "attr": cty.StringVal("test"), 380 "foo": cty.UnknownVal(cty.Set(cty.EmptyObject)), 381 }), 382 "", 383 }, 384 "unknown nested map": { 385 &Block{ 386 Attributes: map[string]*Attribute{ 387 "attr": { 388 Type: cty.String, 389 Required: true, 390 }, 391 }, 392 BlockTypes: map[string]*NestedBlock{ 393 "foo": { 394 Block: Block{}, 395 Nesting: NestingMap, 396 MinItems: 1, 397 }, 398 }, 399 }, 400 cty.ObjectVal(map[string]cty.Value{ 401 "attr": cty.StringVal("test"), 402 "foo": cty.UnknownVal(cty.Map(cty.String)), 403 }), 404 cty.ObjectVal(map[string]cty.Value{ 405 "attr": cty.StringVal("test"), 406 "foo": cty.UnknownVal(cty.Map(cty.EmptyObject)), 407 }), 408 "", 409 }, 410 "extraneous attribute": { 411 &Block{}, 412 cty.ObjectVal(map[string]cty.Value{ 413 "foo": cty.StringVal("bar"), 414 }), 415 cty.DynamicVal, 416 `unexpected attribute "foo"`, 417 }, 418 "wrong attribute type": { 419 &Block{ 420 Attributes: map[string]*Attribute{ 421 "foo": { 422 Type: cty.Number, 423 Required: true, 424 }, 425 }, 426 }, 427 cty.ObjectVal(map[string]cty.Value{ 428 "foo": cty.False, 429 }), 430 cty.DynamicVal, 431 `.foo: number required`, 432 }, 433 "unset computed value": { 434 &Block{ 435 Attributes: map[string]*Attribute{ 436 "foo": { 437 Type: cty.String, 438 Optional: true, 439 Computed: true, 440 }, 441 }, 442 }, 443 cty.ObjectVal(map[string]cty.Value{}), 444 cty.ObjectVal(map[string]cty.Value{ 445 "foo": cty.NullVal(cty.String), 446 }), 447 ``, 448 }, 449 "dynamic value attributes": { 450 &Block{ 451 BlockTypes: map[string]*NestedBlock{ 452 "foo": { 453 Nesting: NestingMap, 454 Block: Block{ 455 Attributes: map[string]*Attribute{ 456 "bar": { 457 Type: cty.String, 458 Optional: true, 459 Computed: true, 460 }, 461 "baz": { 462 Type: cty.DynamicPseudoType, 463 Optional: true, 464 Computed: true, 465 }, 466 }, 467 }, 468 }, 469 }, 470 }, 471 cty.ObjectVal(map[string]cty.Value{ 472 "foo": cty.ObjectVal(map[string]cty.Value{ 473 "a": cty.ObjectVal(map[string]cty.Value{ 474 "bar": cty.StringVal("beep"), 475 }), 476 "b": cty.ObjectVal(map[string]cty.Value{ 477 "bar": cty.StringVal("boop"), 478 "baz": cty.NumberIntVal(8), 479 }), 480 }), 481 }), 482 cty.ObjectVal(map[string]cty.Value{ 483 "foo": cty.ObjectVal(map[string]cty.Value{ 484 "a": cty.ObjectVal(map[string]cty.Value{ 485 "bar": cty.StringVal("beep"), 486 "baz": cty.NullVal(cty.DynamicPseudoType), 487 }), 488 "b": cty.ObjectVal(map[string]cty.Value{ 489 "bar": cty.StringVal("boop"), 490 "baz": cty.NumberIntVal(8), 491 }), 492 }), 493 }), 494 ``, 495 }, 496 "dynamic attributes in map": { 497 // Convert a block represented as a map to an object if a 498 // DynamicPseudoType causes the element types to mismatch. 499 &Block{ 500 BlockTypes: map[string]*NestedBlock{ 501 "foo": { 502 Nesting: NestingMap, 503 Block: Block{ 504 Attributes: map[string]*Attribute{ 505 "bar": { 506 Type: cty.String, 507 Optional: true, 508 Computed: true, 509 }, 510 "baz": { 511 Type: cty.DynamicPseudoType, 512 Optional: true, 513 Computed: true, 514 }, 515 }, 516 }, 517 }, 518 }, 519 }, 520 cty.ObjectVal(map[string]cty.Value{ 521 "foo": cty.MapVal(map[string]cty.Value{ 522 "a": cty.ObjectVal(map[string]cty.Value{ 523 "bar": cty.StringVal("beep"), 524 }), 525 "b": cty.ObjectVal(map[string]cty.Value{ 526 "bar": cty.StringVal("boop"), 527 }), 528 }), 529 }), 530 cty.ObjectVal(map[string]cty.Value{ 531 "foo": cty.ObjectVal(map[string]cty.Value{ 532 "a": cty.ObjectVal(map[string]cty.Value{ 533 "bar": cty.StringVal("beep"), 534 "baz": cty.NullVal(cty.DynamicPseudoType), 535 }), 536 "b": cty.ObjectVal(map[string]cty.Value{ 537 "bar": cty.StringVal("boop"), 538 "baz": cty.NullVal(cty.DynamicPseudoType), 539 }), 540 }), 541 }), 542 ``, 543 }, 544 "nested types": { 545 // handle NestedTypes 546 &Block{ 547 Attributes: map[string]*Attribute{ 548 "foo": { 549 NestedType: &Object{ 550 Nesting: NestingList, 551 Attributes: map[string]*Attribute{ 552 "bar": { 553 Type: cty.String, 554 Required: true, 555 }, 556 "baz": { 557 Type: cty.Map(cty.String), 558 Optional: true, 559 }, 560 }, 561 }, 562 Optional: true, 563 }, 564 "fob": { 565 NestedType: &Object{ 566 Nesting: NestingSet, 567 Attributes: map[string]*Attribute{ 568 "bar": { 569 Type: cty.String, 570 Optional: true, 571 }, 572 }, 573 }, 574 Optional: true, 575 }, 576 }, 577 }, 578 cty.ObjectVal(map[string]cty.Value{ 579 "foo": cty.ListVal([]cty.Value{ 580 cty.ObjectVal(map[string]cty.Value{ 581 "bar": cty.StringVal("beep"), 582 }), 583 cty.ObjectVal(map[string]cty.Value{ 584 "bar": cty.StringVal("boop"), 585 }), 586 }), 587 }), 588 cty.ObjectVal(map[string]cty.Value{ 589 "foo": cty.ListVal([]cty.Value{ 590 cty.ObjectVal(map[string]cty.Value{ 591 "bar": cty.StringVal("beep"), 592 "baz": cty.NullVal(cty.Map(cty.String)), 593 }), 594 cty.ObjectVal(map[string]cty.Value{ 595 "bar": cty.StringVal("boop"), 596 "baz": cty.NullVal(cty.Map(cty.String)), 597 }), 598 }), 599 "fob": cty.NullVal(cty.Set(cty.Object(map[string]cty.Type{ 600 "bar": cty.String, 601 }))), 602 }), 603 ``, 604 }, 605 } 606 607 for name, test := range tests { 608 t.Run(name, func(t *testing.T) { 609 gotValue, gotErrObj := test.Schema.CoerceValue(test.Input) 610 611 if gotErrObj == nil { 612 if test.WantErr != "" { 613 t.Fatalf("coersion succeeded; want error: %q", test.WantErr) 614 } 615 } else { 616 gotErr := tfdiags.FormatError(gotErrObj) 617 if gotErr != test.WantErr { 618 t.Fatalf("wrong error\ngot: %s\nwant: %s", gotErr, test.WantErr) 619 } 620 return 621 } 622 623 if !gotValue.RawEquals(test.WantValue) { 624 t.Errorf("wrong result\ninput: %#v\ngot: %#v\nwant: %#v", test.Input, gotValue, test.WantValue) 625 } 626 }) 627 } 628 }