github.com/hashicorp/terraform-plugin-sdk@v1.17.2/internal/plans/objchange/plan_valid_test.go (about) 1 package objchange 2 3 import ( 4 "testing" 5 6 "github.com/apparentlymart/go-dump/dump" 7 "github.com/zclconf/go-cty/cty" 8 9 "github.com/hashicorp/terraform-plugin-sdk/internal/configs/configschema" 10 "github.com/hashicorp/terraform-plugin-sdk/internal/tfdiags" 11 ) 12 13 func TestAssertPlanValid(t *testing.T) { 14 tests := map[string]struct { 15 Schema *configschema.Block 16 Prior cty.Value 17 Config cty.Value 18 Planned cty.Value 19 WantErrs []string 20 }{ 21 "all empty": { 22 &configschema.Block{}, 23 cty.EmptyObjectVal, 24 cty.EmptyObjectVal, 25 cty.EmptyObjectVal, 26 nil, 27 }, 28 "no computed, all match": { 29 &configschema.Block{ 30 Attributes: map[string]*configschema.Attribute{ 31 "a": { 32 Type: cty.String, 33 Optional: true, 34 }, 35 }, 36 BlockTypes: map[string]*configschema.NestedBlock{ 37 "b": { 38 Nesting: configschema.NestingList, 39 Block: configschema.Block{ 40 Attributes: map[string]*configschema.Attribute{ 41 "c": { 42 Type: cty.String, 43 Optional: true, 44 }, 45 }, 46 }, 47 }, 48 }, 49 }, 50 cty.ObjectVal(map[string]cty.Value{ 51 "a": cty.StringVal("a value"), 52 "b": cty.ListVal([]cty.Value{ 53 cty.ObjectVal(map[string]cty.Value{ 54 "c": cty.StringVal("c value"), 55 }), 56 }), 57 }), 58 cty.ObjectVal(map[string]cty.Value{ 59 "a": cty.StringVal("a value"), 60 "b": cty.ListVal([]cty.Value{ 61 cty.ObjectVal(map[string]cty.Value{ 62 "c": cty.StringVal("c value"), 63 }), 64 }), 65 }), 66 cty.ObjectVal(map[string]cty.Value{ 67 "a": cty.StringVal("a value"), 68 "b": cty.ListVal([]cty.Value{ 69 cty.ObjectVal(map[string]cty.Value{ 70 "c": cty.StringVal("c value"), 71 }), 72 }), 73 }), 74 nil, 75 }, 76 "no computed, plan matches, no prior": { 77 &configschema.Block{ 78 Attributes: map[string]*configschema.Attribute{ 79 "a": { 80 Type: cty.String, 81 Optional: true, 82 }, 83 }, 84 BlockTypes: map[string]*configschema.NestedBlock{ 85 "b": { 86 Nesting: configschema.NestingList, 87 Block: configschema.Block{ 88 Attributes: map[string]*configschema.Attribute{ 89 "c": { 90 Type: cty.String, 91 Optional: true, 92 }, 93 }, 94 }, 95 }, 96 }, 97 }, 98 cty.NullVal(cty.Object(map[string]cty.Type{ 99 "a": cty.String, 100 "b": cty.List(cty.Object(map[string]cty.Type{ 101 "c": cty.String, 102 })), 103 })), 104 cty.ObjectVal(map[string]cty.Value{ 105 "a": cty.StringVal("a value"), 106 "b": cty.ListVal([]cty.Value{ 107 cty.ObjectVal(map[string]cty.Value{ 108 "c": cty.StringVal("c value"), 109 }), 110 }), 111 }), 112 cty.ObjectVal(map[string]cty.Value{ 113 "a": cty.StringVal("a value"), 114 "b": cty.ListVal([]cty.Value{ 115 cty.ObjectVal(map[string]cty.Value{ 116 "c": cty.StringVal("c value"), 117 }), 118 }), 119 }), 120 nil, 121 }, 122 "no computed, invalid change in plan": { 123 &configschema.Block{ 124 Attributes: map[string]*configschema.Attribute{ 125 "a": { 126 Type: cty.String, 127 Optional: true, 128 }, 129 }, 130 BlockTypes: map[string]*configschema.NestedBlock{ 131 "b": { 132 Nesting: configschema.NestingList, 133 Block: configschema.Block{ 134 Attributes: map[string]*configschema.Attribute{ 135 "c": { 136 Type: cty.String, 137 Optional: true, 138 }, 139 }, 140 }, 141 }, 142 }, 143 }, 144 cty.NullVal(cty.Object(map[string]cty.Type{ 145 "a": cty.String, 146 "b": cty.List(cty.Object(map[string]cty.Type{ 147 "c": cty.String, 148 })), 149 })), 150 cty.ObjectVal(map[string]cty.Value{ 151 "a": cty.StringVal("a value"), 152 "b": cty.ListVal([]cty.Value{ 153 cty.ObjectVal(map[string]cty.Value{ 154 "c": cty.StringVal("c value"), 155 }), 156 }), 157 }), 158 cty.ObjectVal(map[string]cty.Value{ 159 "a": cty.StringVal("a value"), 160 "b": cty.ListVal([]cty.Value{ 161 cty.ObjectVal(map[string]cty.Value{ 162 "c": cty.StringVal("new c value"), 163 }), 164 }), 165 }), 166 []string{ 167 `.b[0].c: planned value cty.StringVal("new c value") does not match config value cty.StringVal("c value")`, 168 }, 169 }, 170 "no computed, invalid change in plan sensitive": { 171 &configschema.Block{ 172 Attributes: map[string]*configschema.Attribute{ 173 "a": { 174 Type: cty.String, 175 Optional: true, 176 }, 177 }, 178 BlockTypes: map[string]*configschema.NestedBlock{ 179 "b": { 180 Nesting: configschema.NestingList, 181 Block: configschema.Block{ 182 Attributes: map[string]*configschema.Attribute{ 183 "c": { 184 Type: cty.String, 185 Optional: true, 186 Sensitive: true, 187 }, 188 }, 189 }, 190 }, 191 }, 192 }, 193 cty.NullVal(cty.Object(map[string]cty.Type{ 194 "a": cty.String, 195 "b": cty.List(cty.Object(map[string]cty.Type{ 196 "c": cty.String, 197 })), 198 })), 199 cty.ObjectVal(map[string]cty.Value{ 200 "a": cty.StringVal("a value"), 201 "b": cty.ListVal([]cty.Value{ 202 cty.ObjectVal(map[string]cty.Value{ 203 "c": cty.StringVal("c value"), 204 }), 205 }), 206 }), 207 cty.ObjectVal(map[string]cty.Value{ 208 "a": cty.StringVal("a value"), 209 "b": cty.ListVal([]cty.Value{ 210 cty.ObjectVal(map[string]cty.Value{ 211 "c": cty.StringVal("new c value"), 212 }), 213 }), 214 }), 215 []string{ 216 `.b[0].c: sensitive planned value does not match config value`, 217 }, 218 }, 219 "no computed, diff suppression in plan": { 220 &configschema.Block{ 221 Attributes: map[string]*configschema.Attribute{ 222 "a": { 223 Type: cty.String, 224 Optional: true, 225 }, 226 }, 227 BlockTypes: map[string]*configschema.NestedBlock{ 228 "b": { 229 Nesting: configschema.NestingList, 230 Block: configschema.Block{ 231 Attributes: map[string]*configschema.Attribute{ 232 "c": { 233 Type: cty.String, 234 Optional: true, 235 }, 236 }, 237 }, 238 }, 239 }, 240 }, 241 cty.ObjectVal(map[string]cty.Value{ 242 "a": cty.StringVal("a value"), 243 "b": cty.ListVal([]cty.Value{ 244 cty.ObjectVal(map[string]cty.Value{ 245 "c": cty.StringVal("c value"), 246 }), 247 }), 248 }), 249 cty.ObjectVal(map[string]cty.Value{ 250 "a": cty.StringVal("a value"), 251 "b": cty.ListVal([]cty.Value{ 252 cty.ObjectVal(map[string]cty.Value{ 253 "c": cty.StringVal("new c value"), 254 }), 255 }), 256 }), 257 cty.ObjectVal(map[string]cty.Value{ 258 "a": cty.StringVal("a value"), 259 "b": cty.ListVal([]cty.Value{ 260 cty.ObjectVal(map[string]cty.Value{ 261 "c": cty.StringVal("c value"), // plan uses value from prior object 262 }), 263 }), 264 }), 265 nil, 266 }, 267 "no computed, all null": { 268 &configschema.Block{ 269 Attributes: map[string]*configschema.Attribute{ 270 "a": { 271 Type: cty.String, 272 Optional: true, 273 }, 274 }, 275 BlockTypes: map[string]*configschema.NestedBlock{ 276 "b": { 277 Nesting: configschema.NestingList, 278 Block: configschema.Block{ 279 Attributes: map[string]*configschema.Attribute{ 280 "c": { 281 Type: cty.String, 282 Optional: true, 283 }, 284 }, 285 }, 286 }, 287 }, 288 }, 289 cty.ObjectVal(map[string]cty.Value{ 290 "a": cty.NullVal(cty.String), 291 "b": cty.ListVal([]cty.Value{ 292 cty.ObjectVal(map[string]cty.Value{ 293 "c": cty.NullVal(cty.String), 294 }), 295 }), 296 }), 297 cty.ObjectVal(map[string]cty.Value{ 298 "a": cty.NullVal(cty.String), 299 "b": cty.ListVal([]cty.Value{ 300 cty.ObjectVal(map[string]cty.Value{ 301 "c": cty.NullVal(cty.String), 302 }), 303 }), 304 }), 305 cty.ObjectVal(map[string]cty.Value{ 306 "a": cty.NullVal(cty.String), 307 "b": cty.ListVal([]cty.Value{ 308 cty.ObjectVal(map[string]cty.Value{ 309 "c": cty.NullVal(cty.String), 310 }), 311 }), 312 }), 313 nil, 314 }, 315 "nested map, normal update": { 316 &configschema.Block{ 317 BlockTypes: map[string]*configschema.NestedBlock{ 318 "b": { 319 Nesting: configschema.NestingMap, 320 Block: configschema.Block{ 321 Attributes: map[string]*configschema.Attribute{ 322 "c": { 323 Type: cty.String, 324 Optional: true, 325 }, 326 }, 327 }, 328 }, 329 }, 330 }, 331 cty.ObjectVal(map[string]cty.Value{ 332 "b": cty.MapVal(map[string]cty.Value{ 333 "boop": cty.ObjectVal(map[string]cty.Value{ 334 "c": cty.StringVal("hello"), 335 }), 336 }), 337 }), 338 cty.ObjectVal(map[string]cty.Value{ 339 "b": cty.MapVal(map[string]cty.Value{ 340 "boop": cty.ObjectVal(map[string]cty.Value{ 341 "c": cty.StringVal("howdy"), 342 }), 343 }), 344 }), 345 cty.ObjectVal(map[string]cty.Value{ 346 "b": cty.MapVal(map[string]cty.Value{ 347 "boop": cty.ObjectVal(map[string]cty.Value{ 348 "c": cty.StringVal("howdy"), 349 }), 350 }), 351 }), 352 nil, 353 }, 354 355 // Nested block collections are never null 356 "nested list, null in plan": { 357 &configschema.Block{ 358 BlockTypes: map[string]*configschema.NestedBlock{ 359 "b": { 360 Nesting: configschema.NestingList, 361 Block: configschema.Block{ 362 Attributes: map[string]*configschema.Attribute{ 363 "c": { 364 Type: cty.String, 365 Optional: true, 366 }, 367 }, 368 }, 369 }, 370 }, 371 }, 372 cty.NullVal(cty.Object(map[string]cty.Type{ 373 "b": cty.List(cty.Object(map[string]cty.Type{ 374 "c": cty.String, 375 })), 376 })), 377 cty.ObjectVal(map[string]cty.Value{ 378 "b": cty.ListValEmpty(cty.Object(map[string]cty.Type{ 379 "c": cty.String, 380 })), 381 }), 382 cty.ObjectVal(map[string]cty.Value{ 383 "b": cty.NullVal(cty.List(cty.Object(map[string]cty.Type{ 384 "c": cty.String, 385 }))), 386 }), 387 []string{ 388 `.b: attribute representing a list of nested blocks must be empty to indicate no blocks, not null`, 389 }, 390 }, 391 "nested set, null in plan": { 392 &configschema.Block{ 393 BlockTypes: map[string]*configschema.NestedBlock{ 394 "b": { 395 Nesting: configschema.NestingSet, 396 Block: configschema.Block{ 397 Attributes: map[string]*configschema.Attribute{ 398 "c": { 399 Type: cty.String, 400 Optional: true, 401 }, 402 }, 403 }, 404 }, 405 }, 406 }, 407 cty.NullVal(cty.Object(map[string]cty.Type{ 408 "b": cty.Set(cty.Object(map[string]cty.Type{ 409 "c": cty.String, 410 })), 411 })), 412 cty.ObjectVal(map[string]cty.Value{ 413 "b": cty.SetValEmpty(cty.Object(map[string]cty.Type{ 414 "c": cty.String, 415 })), 416 }), 417 cty.ObjectVal(map[string]cty.Value{ 418 "b": cty.NullVal(cty.Set(cty.Object(map[string]cty.Type{ 419 "c": cty.String, 420 }))), 421 }), 422 []string{ 423 `.b: attribute representing a set of nested blocks must be empty to indicate no blocks, not null`, 424 }, 425 }, 426 "nested map, null in plan": { 427 &configschema.Block{ 428 BlockTypes: map[string]*configschema.NestedBlock{ 429 "b": { 430 Nesting: configschema.NestingMap, 431 Block: configschema.Block{ 432 Attributes: map[string]*configschema.Attribute{ 433 "c": { 434 Type: cty.String, 435 Optional: true, 436 }, 437 }, 438 }, 439 }, 440 }, 441 }, 442 cty.NullVal(cty.Object(map[string]cty.Type{ 443 "b": cty.Map(cty.Object(map[string]cty.Type{ 444 "c": cty.String, 445 })), 446 })), 447 cty.ObjectVal(map[string]cty.Value{ 448 "b": cty.MapValEmpty(cty.Object(map[string]cty.Type{ 449 "c": cty.String, 450 })), 451 }), 452 cty.ObjectVal(map[string]cty.Value{ 453 "b": cty.NullVal(cty.Map(cty.Object(map[string]cty.Type{ 454 "c": cty.String, 455 }))), 456 }), 457 []string{ 458 `.b: attribute representing a map of nested blocks must be empty to indicate no blocks, not null`, 459 }, 460 }, 461 462 // We don't actually do any validation for nested set blocks, and so 463 // the remaining cases here are just intending to ensure we don't 464 // inadvertently start generating errors incorrectly in future. 465 "nested set, no computed, no changes": { 466 &configschema.Block{ 467 BlockTypes: map[string]*configschema.NestedBlock{ 468 "b": { 469 Nesting: configschema.NestingSet, 470 Block: configschema.Block{ 471 Attributes: map[string]*configschema.Attribute{ 472 "c": { 473 Type: cty.String, 474 Optional: true, 475 }, 476 }, 477 }, 478 }, 479 }, 480 }, 481 cty.ObjectVal(map[string]cty.Value{ 482 "b": cty.SetVal([]cty.Value{ 483 cty.ObjectVal(map[string]cty.Value{ 484 "c": cty.StringVal("c value"), 485 }), 486 }), 487 }), 488 cty.ObjectVal(map[string]cty.Value{ 489 "b": cty.SetVal([]cty.Value{ 490 cty.ObjectVal(map[string]cty.Value{ 491 "c": cty.StringVal("c value"), 492 }), 493 }), 494 }), 495 cty.ObjectVal(map[string]cty.Value{ 496 "b": cty.SetVal([]cty.Value{ 497 cty.ObjectVal(map[string]cty.Value{ 498 "c": cty.StringVal("c value"), 499 }), 500 }), 501 }), 502 nil, 503 }, 504 "nested set, no computed, invalid change in plan": { 505 &configschema.Block{ 506 BlockTypes: map[string]*configschema.NestedBlock{ 507 "b": { 508 Nesting: configschema.NestingSet, 509 Block: configschema.Block{ 510 Attributes: map[string]*configschema.Attribute{ 511 "c": { 512 Type: cty.String, 513 Optional: true, 514 }, 515 }, 516 }, 517 }, 518 }, 519 }, 520 cty.ObjectVal(map[string]cty.Value{ 521 "b": cty.SetVal([]cty.Value{ 522 cty.ObjectVal(map[string]cty.Value{ 523 "c": cty.StringVal("c value"), 524 }), 525 }), 526 }), 527 cty.ObjectVal(map[string]cty.Value{ 528 "b": cty.SetVal([]cty.Value{ 529 cty.ObjectVal(map[string]cty.Value{ 530 "c": cty.StringVal("c value"), 531 }), 532 }), 533 }), 534 cty.ObjectVal(map[string]cty.Value{ 535 "b": cty.SetVal([]cty.Value{ 536 cty.ObjectVal(map[string]cty.Value{ 537 "c": cty.StringVal("new c value"), // matches neither prior nor config 538 }), 539 }), 540 }), 541 nil, 542 }, 543 "nested set, no computed, diff suppressed": { 544 &configschema.Block{ 545 BlockTypes: map[string]*configschema.NestedBlock{ 546 "b": { 547 Nesting: configschema.NestingSet, 548 Block: configschema.Block{ 549 Attributes: map[string]*configschema.Attribute{ 550 "c": { 551 Type: cty.String, 552 Optional: true, 553 }, 554 }, 555 }, 556 }, 557 }, 558 }, 559 cty.ObjectVal(map[string]cty.Value{ 560 "b": cty.SetVal([]cty.Value{ 561 cty.ObjectVal(map[string]cty.Value{ 562 "c": cty.StringVal("c value"), 563 }), 564 }), 565 }), 566 cty.ObjectVal(map[string]cty.Value{ 567 "b": cty.SetVal([]cty.Value{ 568 cty.ObjectVal(map[string]cty.Value{ 569 "c": cty.StringVal("new c value"), 570 }), 571 }), 572 }), 573 cty.ObjectVal(map[string]cty.Value{ 574 "b": cty.SetVal([]cty.Value{ 575 cty.ObjectVal(map[string]cty.Value{ 576 "c": cty.StringVal("c value"), // plan uses value from prior object 577 }), 578 }), 579 }), 580 nil, 581 }, 582 } 583 584 for name, test := range tests { 585 t.Run(name, func(t *testing.T) { 586 errs := AssertPlanValid(test.Schema, test.Prior, test.Config, test.Planned) 587 588 wantErrs := make(map[string]struct{}) 589 gotErrs := make(map[string]struct{}) 590 for _, err := range errs { 591 gotErrs[tfdiags.FormatError(err)] = struct{}{} 592 } 593 for _, msg := range test.WantErrs { 594 wantErrs[msg] = struct{}{} 595 } 596 597 t.Logf( 598 "\nprior: %sconfig: %splanned: %s", 599 dump.Value(test.Planned), 600 dump.Value(test.Config), 601 dump.Value(test.Planned), 602 ) 603 for msg := range wantErrs { 604 if _, ok := gotErrs[msg]; !ok { 605 t.Errorf("missing expected error: %s", msg) 606 } 607 } 608 for msg := range gotErrs { 609 if _, ok := wantErrs[msg]; !ok { 610 t.Errorf("unexpected extra error: %s", msg) 611 } 612 } 613 }) 614 } 615 }