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