github.com/opentofu/opentofu@v1.7.1/internal/addrs/move_endpoint_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 addrs 7 8 import ( 9 "fmt" 10 "testing" 11 12 "github.com/google/go-cmp/cmp" 13 "github.com/hashicorp/hcl/v2" 14 "github.com/hashicorp/hcl/v2/hclsyntax" 15 ) 16 17 func TestParseMoveEndpoint(t *testing.T) { 18 tests := []struct { 19 Input string 20 WantRel AbsMoveable // funny intermediate subset of AbsMoveable 21 WantErr string 22 }{ 23 { 24 `foo.bar`, 25 AbsResourceInstance{ 26 Module: RootModuleInstance, 27 Resource: ResourceInstance{ 28 Resource: Resource{ 29 Mode: ManagedResourceMode, 30 Type: "foo", 31 Name: "bar", 32 }, 33 Key: NoKey, 34 }, 35 }, 36 ``, 37 }, 38 { 39 `foo.bar[0]`, 40 AbsResourceInstance{ 41 Module: RootModuleInstance, 42 Resource: ResourceInstance{ 43 Resource: Resource{ 44 Mode: ManagedResourceMode, 45 Type: "foo", 46 Name: "bar", 47 }, 48 Key: IntKey(0), 49 }, 50 }, 51 ``, 52 }, 53 { 54 `foo.bar["a"]`, 55 AbsResourceInstance{ 56 Module: RootModuleInstance, 57 Resource: ResourceInstance{ 58 Resource: Resource{ 59 Mode: ManagedResourceMode, 60 Type: "foo", 61 Name: "bar", 62 }, 63 Key: StringKey("a"), 64 }, 65 }, 66 ``, 67 }, 68 { 69 `module.boop.foo.bar`, 70 AbsResourceInstance{ 71 Module: ModuleInstance{ 72 ModuleInstanceStep{Name: "boop"}, 73 }, 74 Resource: ResourceInstance{ 75 Resource: Resource{ 76 Mode: ManagedResourceMode, 77 Type: "foo", 78 Name: "bar", 79 }, 80 Key: NoKey, 81 }, 82 }, 83 ``, 84 }, 85 { 86 `module.boop.foo.bar[0]`, 87 AbsResourceInstance{ 88 Module: ModuleInstance{ 89 ModuleInstanceStep{Name: "boop"}, 90 }, 91 Resource: ResourceInstance{ 92 Resource: Resource{ 93 Mode: ManagedResourceMode, 94 Type: "foo", 95 Name: "bar", 96 }, 97 Key: IntKey(0), 98 }, 99 }, 100 ``, 101 }, 102 { 103 `module.boop.foo.bar["a"]`, 104 AbsResourceInstance{ 105 Module: ModuleInstance{ 106 ModuleInstanceStep{Name: "boop"}, 107 }, 108 Resource: ResourceInstance{ 109 Resource: Resource{ 110 Mode: ManagedResourceMode, 111 Type: "foo", 112 Name: "bar", 113 }, 114 Key: StringKey("a"), 115 }, 116 }, 117 ``, 118 }, 119 { 120 `data.foo.bar`, 121 AbsResourceInstance{ 122 Module: RootModuleInstance, 123 Resource: ResourceInstance{ 124 Resource: Resource{ 125 Mode: DataResourceMode, 126 Type: "foo", 127 Name: "bar", 128 }, 129 Key: NoKey, 130 }, 131 }, 132 ``, 133 }, 134 { 135 `data.foo.bar[0]`, 136 AbsResourceInstance{ 137 Module: RootModuleInstance, 138 Resource: ResourceInstance{ 139 Resource: Resource{ 140 Mode: DataResourceMode, 141 Type: "foo", 142 Name: "bar", 143 }, 144 Key: IntKey(0), 145 }, 146 }, 147 ``, 148 }, 149 { 150 `data.foo.bar["a"]`, 151 AbsResourceInstance{ 152 Module: RootModuleInstance, 153 Resource: ResourceInstance{ 154 Resource: Resource{ 155 Mode: DataResourceMode, 156 Type: "foo", 157 Name: "bar", 158 }, 159 Key: StringKey("a"), 160 }, 161 }, 162 ``, 163 }, 164 { 165 `module.boop.data.foo.bar`, 166 AbsResourceInstance{ 167 Module: ModuleInstance{ 168 ModuleInstanceStep{Name: "boop"}, 169 }, 170 Resource: ResourceInstance{ 171 Resource: Resource{ 172 Mode: DataResourceMode, 173 Type: "foo", 174 Name: "bar", 175 }, 176 Key: NoKey, 177 }, 178 }, 179 ``, 180 }, 181 { 182 `module.boop.data.foo.bar[0]`, 183 AbsResourceInstance{ 184 Module: ModuleInstance{ 185 ModuleInstanceStep{Name: "boop"}, 186 }, 187 Resource: ResourceInstance{ 188 Resource: Resource{ 189 Mode: DataResourceMode, 190 Type: "foo", 191 Name: "bar", 192 }, 193 Key: IntKey(0), 194 }, 195 }, 196 ``, 197 }, 198 { 199 `module.boop.data.foo.bar["a"]`, 200 AbsResourceInstance{ 201 Module: ModuleInstance{ 202 ModuleInstanceStep{Name: "boop"}, 203 }, 204 Resource: ResourceInstance{ 205 Resource: Resource{ 206 Mode: DataResourceMode, 207 Type: "foo", 208 Name: "bar", 209 }, 210 Key: StringKey("a"), 211 }, 212 }, 213 ``, 214 }, 215 { 216 `module.foo`, 217 ModuleInstance{ 218 ModuleInstanceStep{Name: "foo"}, 219 }, 220 ``, 221 }, 222 { 223 `module.foo[0]`, 224 ModuleInstance{ 225 ModuleInstanceStep{Name: "foo", InstanceKey: IntKey(0)}, 226 }, 227 ``, 228 }, 229 { 230 `module.foo["a"]`, 231 ModuleInstance{ 232 ModuleInstanceStep{Name: "foo", InstanceKey: StringKey("a")}, 233 }, 234 ``, 235 }, 236 { 237 `module.foo.module.bar`, 238 ModuleInstance{ 239 ModuleInstanceStep{Name: "foo"}, 240 ModuleInstanceStep{Name: "bar"}, 241 }, 242 ``, 243 }, 244 { 245 `module.foo[1].module.bar`, 246 ModuleInstance{ 247 ModuleInstanceStep{Name: "foo", InstanceKey: IntKey(1)}, 248 ModuleInstanceStep{Name: "bar"}, 249 }, 250 ``, 251 }, 252 { 253 `module.foo.module.bar[1]`, 254 ModuleInstance{ 255 ModuleInstanceStep{Name: "foo"}, 256 ModuleInstanceStep{Name: "bar", InstanceKey: IntKey(1)}, 257 }, 258 ``, 259 }, 260 { 261 `module.foo[0].module.bar[1]`, 262 ModuleInstance{ 263 ModuleInstanceStep{Name: "foo", InstanceKey: IntKey(0)}, 264 ModuleInstanceStep{Name: "bar", InstanceKey: IntKey(1)}, 265 }, 266 ``, 267 }, 268 { 269 `module`, 270 nil, 271 `Invalid address operator: Prefix "module." must be followed by a module name.`, 272 }, 273 { 274 `module[0]`, 275 nil, 276 `Invalid address operator: Prefix "module." must be followed by a module name.`, 277 }, 278 { 279 `module.foo.data`, 280 nil, 281 `Invalid address: Resource specification must include a resource type and name.`, 282 }, 283 { 284 `module.foo.data.bar`, 285 nil, 286 `Invalid address: Resource specification must include a resource type and name.`, 287 }, 288 { 289 `module.foo.data[0]`, 290 nil, 291 `Invalid address: Resource specification must include a resource type and name.`, 292 }, 293 { 294 `module.foo.data.bar[0]`, 295 nil, 296 `Invalid address: A resource name is required.`, 297 }, 298 { 299 `module.foo.bar`, 300 nil, 301 `Invalid address: Resource specification must include a resource type and name.`, 302 }, 303 { 304 `module.foo.bar[0]`, 305 nil, 306 `Invalid address: A resource name is required.`, 307 }, 308 } 309 310 for _, test := range tests { 311 t.Run(test.Input, func(t *testing.T) { 312 traversal, hclDiags := hclsyntax.ParseTraversalAbs([]byte(test.Input), "", hcl.InitialPos) 313 if hclDiags.HasErrors() { 314 // We're not trying to test the HCL parser here, so any 315 // failures at this point are likely to be bugs in the 316 // test case itself. 317 t.Fatalf("syntax error: %s", hclDiags.Error()) 318 } 319 320 moveEp, diags := ParseMoveEndpoint(traversal) 321 322 switch { 323 case test.WantErr != "": 324 if !diags.HasErrors() { 325 t.Fatalf("unexpected success\nwant error: %s", test.WantErr) 326 } 327 gotErr := diags.Err().Error() 328 if gotErr != test.WantErr { 329 t.Fatalf("wrong error\ngot: %s\nwant: %s", gotErr, test.WantErr) 330 } 331 default: 332 if diags.HasErrors() { 333 t.Fatalf("unexpected error: %s", diags.Err().Error()) 334 } 335 if diff := cmp.Diff(test.WantRel, moveEp.relSubject); diff != "" { 336 t.Errorf("wrong result\n%s", diff) 337 } 338 } 339 }) 340 } 341 } 342 343 func TestUnifyMoveEndpoints(t *testing.T) { 344 tests := []struct { 345 InputFrom, InputTo string 346 Module Module 347 WantFrom, WantTo string 348 }{ 349 { 350 InputFrom: `foo.bar`, 351 InputTo: `foo.baz`, 352 Module: RootModule, 353 WantFrom: `foo.bar[*]`, 354 WantTo: `foo.baz[*]`, 355 }, 356 { 357 InputFrom: `foo.bar`, 358 InputTo: `foo.baz`, 359 Module: RootModule.Child("a"), 360 WantFrom: `module.a[*].foo.bar[*]`, 361 WantTo: `module.a[*].foo.baz[*]`, 362 }, 363 { 364 InputFrom: `foo.bar`, 365 InputTo: `module.b[0].foo.baz`, 366 Module: RootModule.Child("a"), 367 WantFrom: `module.a[*].foo.bar[*]`, 368 WantTo: `module.a[*].module.b[0].foo.baz[*]`, 369 }, 370 { 371 InputFrom: `foo.bar`, 372 InputTo: `foo.bar["thing"]`, 373 Module: RootModule, 374 WantFrom: `foo.bar`, 375 WantTo: `foo.bar["thing"]`, 376 }, 377 { 378 InputFrom: `foo.bar["thing"]`, 379 InputTo: `foo.bar`, 380 Module: RootModule, 381 WantFrom: `foo.bar["thing"]`, 382 WantTo: `foo.bar`, 383 }, 384 { 385 InputFrom: `foo.bar["a"]`, 386 InputTo: `foo.bar["b"]`, 387 Module: RootModule, 388 WantFrom: `foo.bar["a"]`, 389 WantTo: `foo.bar["b"]`, 390 }, 391 { 392 InputFrom: `module.foo`, 393 InputTo: `module.bar`, 394 Module: RootModule, 395 WantFrom: `module.foo[*]`, 396 WantTo: `module.bar[*]`, 397 }, 398 { 399 InputFrom: `module.foo`, 400 InputTo: `module.bar.module.baz`, 401 Module: RootModule, 402 WantFrom: `module.foo[*]`, 403 WantTo: `module.bar.module.baz[*]`, 404 }, 405 { 406 InputFrom: `module.foo`, 407 InputTo: `module.bar.module.baz`, 408 Module: RootModule.Child("bloop"), 409 WantFrom: `module.bloop[*].module.foo[*]`, 410 WantTo: `module.bloop[*].module.bar.module.baz[*]`, 411 }, 412 { 413 InputFrom: `module.foo[0]`, 414 InputTo: `module.foo["a"]`, 415 Module: RootModule, 416 WantFrom: `module.foo[0]`, 417 WantTo: `module.foo["a"]`, 418 }, 419 { 420 InputFrom: `module.foo`, 421 InputTo: `module.foo["a"]`, 422 Module: RootModule, 423 WantFrom: `module.foo`, 424 WantTo: `module.foo["a"]`, 425 }, 426 { 427 InputFrom: `module.foo[0]`, 428 InputTo: `module.foo`, 429 Module: RootModule, 430 WantFrom: `module.foo[0]`, 431 WantTo: `module.foo`, 432 }, 433 { 434 InputFrom: `module.foo[0]`, 435 InputTo: `module.foo`, 436 Module: RootModule.Child("bloop"), 437 WantFrom: `module.bloop[*].module.foo[0]`, 438 WantTo: `module.bloop[*].module.foo`, 439 }, 440 { 441 InputFrom: `module.foo`, 442 InputTo: `foo.bar`, 443 Module: RootModule, 444 WantFrom: ``, // Can't unify module call with resource 445 WantTo: ``, 446 }, 447 { 448 InputFrom: `module.foo[0]`, 449 InputTo: `foo.bar`, 450 Module: RootModule, 451 WantFrom: ``, // Can't unify module instance with resource 452 WantTo: ``, 453 }, 454 { 455 InputFrom: `module.foo`, 456 InputTo: `foo.bar[0]`, 457 Module: RootModule, 458 WantFrom: ``, // Can't unify module call with resource instance 459 WantTo: ``, 460 }, 461 { 462 InputFrom: `module.foo[0]`, 463 InputTo: `foo.bar[0]`, 464 Module: RootModule, 465 WantFrom: ``, // Can't unify module instance with resource instance 466 WantTo: ``, 467 }, 468 } 469 470 for _, test := range tests { 471 t.Run(fmt.Sprintf("%s to %s in %s", test.InputFrom, test.InputTo, test.Module), func(t *testing.T) { 472 parseInput := func(input string) *MoveEndpoint { 473 t.Helper() 474 475 traversal, hclDiags := hclsyntax.ParseTraversalAbs([]byte(input), "", hcl.InitialPos) 476 if hclDiags.HasErrors() { 477 // We're not trying to test the HCL parser here, so any 478 // failures at this point are likely to be bugs in the 479 // test case itself. 480 t.Fatalf("syntax error: %s", hclDiags.Error()) 481 } 482 483 moveEp, diags := ParseMoveEndpoint(traversal) 484 if diags.HasErrors() { 485 t.Fatalf("unexpected error: %s", diags.Err().Error()) 486 } 487 return moveEp 488 } 489 490 fromEp := parseInput(test.InputFrom) 491 toEp := parseInput(test.InputTo) 492 493 gotFrom, gotTo := UnifyMoveEndpoints(test.Module, fromEp, toEp) 494 if got, want := gotFrom.String(), test.WantFrom; got != want { 495 t.Errorf("wrong 'from' result\ngot: %s\nwant: %s", got, want) 496 } 497 if got, want := gotTo.String(), test.WantTo; got != want { 498 t.Errorf("wrong 'to' result\ngot: %s\nwant: %s", got, want) 499 } 500 }) 501 } 502 } 503 504 func TestMoveEndpointConfigMoveable(t *testing.T) { 505 tests := []struct { 506 Input string 507 Module Module 508 Want ConfigMoveable 509 }{ 510 { 511 `foo.bar`, 512 RootModule, 513 ConfigResource{ 514 Module: RootModule, 515 Resource: Resource{ 516 Mode: ManagedResourceMode, 517 Type: "foo", 518 Name: "bar", 519 }, 520 }, 521 }, 522 { 523 `foo.bar[0]`, 524 RootModule, 525 ConfigResource{ 526 Module: RootModule, 527 Resource: Resource{ 528 Mode: ManagedResourceMode, 529 Type: "foo", 530 Name: "bar", 531 }, 532 }, 533 }, 534 { 535 `module.foo.bar.baz`, 536 RootModule, 537 ConfigResource{ 538 Module: Module{"foo"}, 539 Resource: Resource{ 540 Mode: ManagedResourceMode, 541 Type: "bar", 542 Name: "baz", 543 }, 544 }, 545 }, 546 { 547 `module.foo[0].bar.baz`, 548 RootModule, 549 ConfigResource{ 550 Module: Module{"foo"}, 551 Resource: Resource{ 552 Mode: ManagedResourceMode, 553 Type: "bar", 554 Name: "baz", 555 }, 556 }, 557 }, 558 { 559 `foo.bar`, 560 Module{"boop"}, 561 ConfigResource{ 562 Module: Module{"boop"}, 563 Resource: Resource{ 564 Mode: ManagedResourceMode, 565 Type: "foo", 566 Name: "bar", 567 }, 568 }, 569 }, 570 { 571 `module.bloop.foo.bar`, 572 Module{"bleep"}, 573 ConfigResource{ 574 Module: Module{"bleep", "bloop"}, 575 Resource: Resource{ 576 Mode: ManagedResourceMode, 577 Type: "foo", 578 Name: "bar", 579 }, 580 }, 581 }, 582 { 583 `module.foo.bar.baz`, 584 RootModule, 585 ConfigResource{ 586 Module: Module{"foo"}, 587 Resource: Resource{ 588 Mode: ManagedResourceMode, 589 Type: "bar", 590 Name: "baz", 591 }, 592 }, 593 }, 594 { 595 `module.foo`, 596 RootModule, 597 Module{"foo"}, 598 }, 599 { 600 `module.foo[0]`, 601 RootModule, 602 Module{"foo"}, 603 }, 604 { 605 `module.bloop`, 606 Module{"bleep"}, 607 Module{"bleep", "bloop"}, 608 }, 609 { 610 `module.bloop[0]`, 611 Module{"bleep"}, 612 Module{"bleep", "bloop"}, 613 }, 614 } 615 616 for _, test := range tests { 617 t.Run(fmt.Sprintf("%s in %s", test.Input, test.Module), func(t *testing.T) { 618 traversal, hclDiags := hclsyntax.ParseTraversalAbs([]byte(test.Input), "", hcl.InitialPos) 619 if hclDiags.HasErrors() { 620 // We're not trying to test the HCL parser here, so any 621 // failures at this point are likely to be bugs in the 622 // test case itself. 623 t.Fatalf("syntax error: %s", hclDiags.Error()) 624 } 625 626 moveEp, diags := ParseMoveEndpoint(traversal) 627 if diags.HasErrors() { 628 t.Fatalf("unexpected error: %s", diags.Err().Error()) 629 } 630 631 got := moveEp.ConfigMoveable(test.Module) 632 if diff := cmp.Diff(test.Want, got); diff != "" { 633 t.Errorf("wrong result\n%s", diff) 634 } 635 }) 636 } 637 }