github.com/hashicorp/terraform-plugin-sdk@v1.17.2/internal/plans/objchange/objchange_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 ) 11 12 func TestProposedNewObject(t *testing.T) { 13 tests := map[string]struct { 14 Schema *configschema.Block 15 Prior cty.Value 16 Config cty.Value 17 Want cty.Value 18 }{ 19 "empty": { 20 &configschema.Block{}, 21 cty.EmptyObjectVal, 22 cty.EmptyObjectVal, 23 cty.EmptyObjectVal, 24 }, 25 "no prior": { 26 &configschema.Block{ 27 Attributes: map[string]*configschema.Attribute{ 28 "foo": { 29 Type: cty.String, 30 Optional: true, 31 }, 32 "bar": { 33 Type: cty.String, 34 Computed: true, 35 }, 36 }, 37 BlockTypes: map[string]*configschema.NestedBlock{ 38 "baz": { 39 Nesting: configschema.NestingSingle, 40 Block: configschema.Block{ 41 Attributes: map[string]*configschema.Attribute{ 42 "boz": { 43 Type: cty.String, 44 Optional: true, 45 Computed: true, 46 }, 47 "biz": { 48 Type: cty.String, 49 Optional: true, 50 Computed: true, 51 }, 52 }, 53 }, 54 }, 55 }, 56 }, 57 cty.NullVal(cty.DynamicPseudoType), 58 cty.ObjectVal(map[string]cty.Value{ 59 "foo": cty.StringVal("hello"), 60 "bar": cty.NullVal(cty.String), 61 "baz": cty.ObjectVal(map[string]cty.Value{ 62 "boz": cty.StringVal("world"), 63 64 // An unknown in the config represents a situation where 65 // an argument is explicitly set to an expression result 66 // that is derived from an unknown value. This is distinct 67 // from leaving it null, which allows the provider itself 68 // to decide the value during PlanResourceChange. 69 "biz": cty.UnknownVal(cty.String), 70 }), 71 }), 72 cty.ObjectVal(map[string]cty.Value{ 73 "foo": cty.StringVal("hello"), 74 75 // unset computed attributes are null in the proposal; provider 76 // usually changes them to "unknown" during PlanResourceChange, 77 // to indicate that the value will be decided during apply. 78 "bar": cty.NullVal(cty.String), 79 80 "baz": cty.ObjectVal(map[string]cty.Value{ 81 "boz": cty.StringVal("world"), 82 "biz": cty.UnknownVal(cty.String), // explicit unknown preserved from config 83 }), 84 }), 85 }, 86 "null block remains null": { 87 &configschema.Block{ 88 Attributes: map[string]*configschema.Attribute{ 89 "foo": { 90 Type: cty.String, 91 Optional: true, 92 }, 93 }, 94 BlockTypes: map[string]*configschema.NestedBlock{ 95 "baz": { 96 Nesting: configschema.NestingSingle, 97 Block: configschema.Block{ 98 Attributes: map[string]*configschema.Attribute{ 99 "boz": { 100 Type: cty.String, 101 Optional: true, 102 Computed: true, 103 }, 104 }, 105 }, 106 }, 107 }, 108 }, 109 cty.NullVal(cty.DynamicPseudoType), 110 cty.ObjectVal(map[string]cty.Value{ 111 "foo": cty.StringVal("bar"), 112 "baz": cty.NullVal(cty.Object(map[string]cty.Type{ 113 "boz": cty.String, 114 })), 115 }), 116 // The baz block does not exist in the config, and therefore 117 // shouldn't be planned. 118 cty.ObjectVal(map[string]cty.Value{ 119 "foo": cty.StringVal("bar"), 120 "baz": cty.NullVal(cty.Object(map[string]cty.Type{ 121 "boz": cty.String, 122 })), 123 }), 124 }, 125 "no prior with set": { 126 // This one is here because our handling of sets is more complex 127 // than others (due to the fuzzy correlation heuristic) and 128 // historically that caused us some panic-related grief. 129 &configschema.Block{ 130 BlockTypes: map[string]*configschema.NestedBlock{ 131 "baz": { 132 Nesting: configschema.NestingSet, 133 Block: configschema.Block{ 134 Attributes: map[string]*configschema.Attribute{ 135 "boz": { 136 Type: cty.String, 137 Optional: true, 138 Computed: true, 139 }, 140 }, 141 }, 142 }, 143 }, 144 }, 145 cty.NullVal(cty.DynamicPseudoType), 146 cty.ObjectVal(map[string]cty.Value{ 147 "baz": cty.SetVal([]cty.Value{ 148 cty.ObjectVal(map[string]cty.Value{ 149 "boz": cty.StringVal("world"), 150 }), 151 }), 152 }), 153 cty.ObjectVal(map[string]cty.Value{ 154 "baz": cty.SetVal([]cty.Value{ 155 cty.ObjectVal(map[string]cty.Value{ 156 "boz": cty.StringVal("world"), 157 }), 158 }), 159 }), 160 }, 161 "prior attributes": { 162 &configschema.Block{ 163 Attributes: map[string]*configschema.Attribute{ 164 "foo": { 165 Type: cty.String, 166 Optional: true, 167 }, 168 "bar": { 169 Type: cty.String, 170 Computed: true, 171 }, 172 "baz": { 173 Type: cty.String, 174 Optional: true, 175 Computed: true, 176 }, 177 "boz": { 178 Type: cty.String, 179 Optional: true, 180 Computed: true, 181 }, 182 }, 183 }, 184 cty.ObjectVal(map[string]cty.Value{ 185 "foo": cty.StringVal("bonjour"), 186 "bar": cty.StringVal("petit dejeuner"), 187 "baz": cty.StringVal("grande dejeuner"), 188 "boz": cty.StringVal("a la monde"), 189 }), 190 cty.ObjectVal(map[string]cty.Value{ 191 "foo": cty.StringVal("hello"), 192 "bar": cty.NullVal(cty.String), 193 "baz": cty.NullVal(cty.String), 194 "boz": cty.StringVal("world"), 195 }), 196 cty.ObjectVal(map[string]cty.Value{ 197 "foo": cty.StringVal("hello"), 198 "bar": cty.StringVal("petit dejeuner"), 199 "baz": cty.StringVal("grande dejeuner"), 200 "boz": cty.StringVal("world"), 201 }), 202 }, 203 "prior nested single": { 204 &configschema.Block{ 205 BlockTypes: map[string]*configschema.NestedBlock{ 206 "foo": { 207 Nesting: configschema.NestingSingle, 208 Block: configschema.Block{ 209 Attributes: map[string]*configschema.Attribute{ 210 "bar": { 211 Type: cty.String, 212 Optional: true, 213 Computed: true, 214 }, 215 "baz": { 216 Type: cty.String, 217 Optional: true, 218 Computed: true, 219 }, 220 }, 221 }, 222 }, 223 }, 224 }, 225 cty.ObjectVal(map[string]cty.Value{ 226 "foo": cty.ObjectVal(map[string]cty.Value{ 227 "bar": cty.StringVal("beep"), 228 "baz": cty.StringVal("boop"), 229 }), 230 }), 231 cty.ObjectVal(map[string]cty.Value{ 232 "foo": cty.ObjectVal(map[string]cty.Value{ 233 "bar": cty.StringVal("bap"), 234 "baz": cty.NullVal(cty.String), 235 }), 236 }), 237 cty.ObjectVal(map[string]cty.Value{ 238 "foo": cty.ObjectVal(map[string]cty.Value{ 239 "bar": cty.StringVal("bap"), 240 "baz": cty.StringVal("boop"), 241 }), 242 }), 243 }, 244 "prior nested list": { 245 &configschema.Block{ 246 BlockTypes: map[string]*configschema.NestedBlock{ 247 "foo": { 248 Nesting: configschema.NestingList, 249 Block: configschema.Block{ 250 Attributes: map[string]*configschema.Attribute{ 251 "bar": { 252 Type: cty.String, 253 Optional: true, 254 Computed: true, 255 }, 256 "baz": { 257 Type: cty.String, 258 Optional: true, 259 Computed: true, 260 }, 261 }, 262 }, 263 }, 264 }, 265 }, 266 cty.ObjectVal(map[string]cty.Value{ 267 "foo": cty.ListVal([]cty.Value{ 268 cty.ObjectVal(map[string]cty.Value{ 269 "bar": cty.StringVal("beep"), 270 "baz": cty.StringVal("boop"), 271 }), 272 }), 273 }), 274 cty.ObjectVal(map[string]cty.Value{ 275 "foo": cty.ListVal([]cty.Value{ 276 cty.ObjectVal(map[string]cty.Value{ 277 "bar": cty.StringVal("bap"), 278 "baz": cty.NullVal(cty.String), 279 }), 280 cty.ObjectVal(map[string]cty.Value{ 281 "bar": cty.StringVal("blep"), 282 "baz": cty.NullVal(cty.String), 283 }), 284 }), 285 }), 286 cty.ObjectVal(map[string]cty.Value{ 287 "foo": cty.ListVal([]cty.Value{ 288 cty.ObjectVal(map[string]cty.Value{ 289 "bar": cty.StringVal("bap"), 290 "baz": cty.StringVal("boop"), 291 }), 292 cty.ObjectVal(map[string]cty.Value{ 293 "bar": cty.StringVal("blep"), 294 "baz": cty.NullVal(cty.String), 295 }), 296 }), 297 }), 298 }, 299 "prior nested list with dynamic": { 300 &configschema.Block{ 301 BlockTypes: map[string]*configschema.NestedBlock{ 302 "foo": { 303 Nesting: configschema.NestingList, 304 Block: configschema.Block{ 305 Attributes: map[string]*configschema.Attribute{ 306 "bar": { 307 Type: cty.String, 308 Optional: true, 309 Computed: true, 310 }, 311 "baz": { 312 Type: cty.DynamicPseudoType, 313 Optional: true, 314 Computed: true, 315 }, 316 }, 317 }, 318 }, 319 }, 320 }, 321 cty.ObjectVal(map[string]cty.Value{ 322 "foo": cty.TupleVal([]cty.Value{ 323 cty.ObjectVal(map[string]cty.Value{ 324 "bar": cty.StringVal("beep"), 325 "baz": cty.StringVal("boop"), 326 }), 327 }), 328 }), 329 cty.ObjectVal(map[string]cty.Value{ 330 "foo": cty.TupleVal([]cty.Value{ 331 cty.ObjectVal(map[string]cty.Value{ 332 "bar": cty.StringVal("bap"), 333 "baz": cty.NullVal(cty.String), 334 }), 335 cty.ObjectVal(map[string]cty.Value{ 336 "bar": cty.StringVal("blep"), 337 "baz": cty.NullVal(cty.String), 338 }), 339 }), 340 }), 341 cty.ObjectVal(map[string]cty.Value{ 342 "foo": cty.TupleVal([]cty.Value{ 343 cty.ObjectVal(map[string]cty.Value{ 344 "bar": cty.StringVal("bap"), 345 "baz": cty.StringVal("boop"), 346 }), 347 cty.ObjectVal(map[string]cty.Value{ 348 "bar": cty.StringVal("blep"), 349 "baz": cty.NullVal(cty.String), 350 }), 351 }), 352 }), 353 }, 354 "prior nested map": { 355 &configschema.Block{ 356 BlockTypes: map[string]*configschema.NestedBlock{ 357 "foo": { 358 Nesting: configschema.NestingMap, 359 Block: configschema.Block{ 360 Attributes: map[string]*configschema.Attribute{ 361 "bar": { 362 Type: cty.String, 363 Optional: true, 364 Computed: true, 365 }, 366 "baz": { 367 Type: cty.String, 368 Optional: true, 369 Computed: true, 370 }, 371 }, 372 }, 373 }, 374 }, 375 }, 376 cty.ObjectVal(map[string]cty.Value{ 377 "foo": cty.MapVal(map[string]cty.Value{ 378 "a": cty.ObjectVal(map[string]cty.Value{ 379 "bar": cty.StringVal("beep"), 380 "baz": cty.StringVal("boop"), 381 }), 382 "b": cty.ObjectVal(map[string]cty.Value{ 383 "bar": cty.StringVal("blep"), 384 "baz": cty.StringVal("boot"), 385 }), 386 }), 387 }), 388 cty.ObjectVal(map[string]cty.Value{ 389 "foo": cty.MapVal(map[string]cty.Value{ 390 "a": cty.ObjectVal(map[string]cty.Value{ 391 "bar": cty.StringVal("bap"), 392 "baz": cty.NullVal(cty.String), 393 }), 394 "c": cty.ObjectVal(map[string]cty.Value{ 395 "bar": cty.StringVal("bosh"), 396 "baz": cty.NullVal(cty.String), 397 }), 398 }), 399 }), 400 cty.ObjectVal(map[string]cty.Value{ 401 "foo": cty.MapVal(map[string]cty.Value{ 402 "a": cty.ObjectVal(map[string]cty.Value{ 403 "bar": cty.StringVal("bap"), 404 "baz": cty.StringVal("boop"), 405 }), 406 "c": cty.ObjectVal(map[string]cty.Value{ 407 "bar": cty.StringVal("bosh"), 408 "baz": cty.NullVal(cty.String), 409 }), 410 }), 411 }), 412 }, 413 "prior nested map with dynamic": { 414 &configschema.Block{ 415 BlockTypes: map[string]*configschema.NestedBlock{ 416 "foo": { 417 Nesting: configschema.NestingMap, 418 Block: configschema.Block{ 419 Attributes: map[string]*configschema.Attribute{ 420 "bar": { 421 Type: cty.String, 422 Optional: true, 423 Computed: true, 424 }, 425 "baz": { 426 Type: cty.DynamicPseudoType, 427 Optional: true, 428 Computed: true, 429 }, 430 }, 431 }, 432 }, 433 }, 434 }, 435 cty.ObjectVal(map[string]cty.Value{ 436 "foo": cty.ObjectVal(map[string]cty.Value{ 437 "a": cty.ObjectVal(map[string]cty.Value{ 438 "bar": cty.StringVal("beep"), 439 "baz": cty.StringVal("boop"), 440 }), 441 "b": cty.ObjectVal(map[string]cty.Value{ 442 "bar": cty.StringVal("blep"), 443 "baz": cty.StringVal("boot"), 444 }), 445 }), 446 }), 447 cty.ObjectVal(map[string]cty.Value{ 448 "foo": cty.ObjectVal(map[string]cty.Value{ 449 "a": cty.ObjectVal(map[string]cty.Value{ 450 "bar": cty.StringVal("bap"), 451 "baz": cty.NullVal(cty.String), 452 }), 453 "c": cty.ObjectVal(map[string]cty.Value{ 454 "bar": cty.StringVal("bosh"), 455 "baz": cty.NullVal(cty.String), 456 }), 457 }), 458 }), 459 cty.ObjectVal(map[string]cty.Value{ 460 "foo": cty.ObjectVal(map[string]cty.Value{ 461 "a": cty.ObjectVal(map[string]cty.Value{ 462 "bar": cty.StringVal("bap"), 463 "baz": cty.StringVal("boop"), 464 }), 465 "c": cty.ObjectVal(map[string]cty.Value{ 466 "bar": cty.StringVal("bosh"), 467 "baz": cty.NullVal(cty.String), 468 }), 469 }), 470 }), 471 }, 472 "prior nested set": { 473 &configschema.Block{ 474 BlockTypes: map[string]*configschema.NestedBlock{ 475 "foo": { 476 Nesting: configschema.NestingSet, 477 Block: configschema.Block{ 478 Attributes: map[string]*configschema.Attribute{ 479 "bar": { 480 // This non-computed attribute will serve 481 // as our matching key for propagating 482 // "baz" from elements in the prior value. 483 Type: cty.String, 484 Optional: true, 485 }, 486 "baz": { 487 Type: cty.String, 488 Optional: true, 489 Computed: true, 490 }, 491 }, 492 }, 493 }, 494 }, 495 }, 496 cty.ObjectVal(map[string]cty.Value{ 497 "foo": cty.SetVal([]cty.Value{ 498 cty.ObjectVal(map[string]cty.Value{ 499 "bar": cty.StringVal("beep"), 500 "baz": cty.StringVal("boop"), 501 }), 502 cty.ObjectVal(map[string]cty.Value{ 503 "bar": cty.StringVal("blep"), 504 "baz": cty.StringVal("boot"), 505 }), 506 }), 507 }), 508 cty.ObjectVal(map[string]cty.Value{ 509 "foo": cty.SetVal([]cty.Value{ 510 cty.ObjectVal(map[string]cty.Value{ 511 "bar": cty.StringVal("beep"), 512 "baz": cty.NullVal(cty.String), 513 }), 514 cty.ObjectVal(map[string]cty.Value{ 515 "bar": cty.StringVal("bosh"), 516 "baz": cty.NullVal(cty.String), 517 }), 518 }), 519 }), 520 cty.ObjectVal(map[string]cty.Value{ 521 "foo": cty.SetVal([]cty.Value{ 522 cty.ObjectVal(map[string]cty.Value{ 523 "bar": cty.StringVal("beep"), 524 "baz": cty.StringVal("boop"), 525 }), 526 cty.ObjectVal(map[string]cty.Value{ 527 "bar": cty.StringVal("bosh"), 528 "baz": cty.NullVal(cty.String), 529 }), 530 }), 531 }), 532 }, 533 "sets differing only by unknown": { 534 &configschema.Block{ 535 BlockTypes: map[string]*configschema.NestedBlock{ 536 "multi": { 537 Nesting: configschema.NestingSet, 538 Block: configschema.Block{ 539 Attributes: map[string]*configschema.Attribute{ 540 "optional": { 541 Type: cty.String, 542 Optional: true, 543 Computed: true, 544 }, 545 }, 546 }, 547 }, 548 }, 549 }, 550 cty.NullVal(cty.DynamicPseudoType), 551 cty.ObjectVal(map[string]cty.Value{ 552 "multi": cty.SetVal([]cty.Value{ 553 cty.ObjectVal(map[string]cty.Value{ 554 "optional": cty.UnknownVal(cty.String), 555 }), 556 cty.ObjectVal(map[string]cty.Value{ 557 "optional": cty.UnknownVal(cty.String), 558 }), 559 }), 560 }), 561 cty.ObjectVal(map[string]cty.Value{ 562 "multi": cty.SetVal([]cty.Value{ 563 // These remain distinct because unknown values never 564 // compare equal. They may be consolidated together once 565 // the values become known, though. 566 cty.ObjectVal(map[string]cty.Value{ 567 "optional": cty.UnknownVal(cty.String), 568 }), 569 cty.ObjectVal(map[string]cty.Value{ 570 "optional": cty.UnknownVal(cty.String), 571 }), 572 }), 573 }), 574 }, 575 } 576 577 for name, test := range tests { 578 t.Run(name, func(t *testing.T) { 579 got := ProposedNewObject(test.Schema, test.Prior, test.Config) 580 if !got.RawEquals(test.Want) { 581 t.Errorf("wrong result\ngot: %swant: %s", dump.Value(got), dump.Value(test.Want)) 582 } 583 }) 584 } 585 }