github.com/terramate-io/tf@v0.0.0-20230830114523-fce866b4dfcd/addrs/move_endpoint_test.go (about)

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