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  }