github.com/opentofu/opentofu@v1.7.1/internal/configs/hcl2shim/paths_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 hcl2shim
     7  
     8  import (
     9  	"fmt"
    10  	"reflect"
    11  	"strconv"
    12  	"strings"
    13  	"testing"
    14  
    15  	"github.com/google/go-cmp/cmp/cmpopts"
    16  
    17  	"github.com/google/go-cmp/cmp"
    18  
    19  	"github.com/zclconf/go-cty/cty"
    20  )
    21  
    22  var (
    23  	ignoreUnexported = cmpopts.IgnoreUnexported(cty.GetAttrStep{}, cty.IndexStep{})
    24  	valueComparer    = cmp.Comparer(cty.Value.RawEquals)
    25  )
    26  
    27  func TestPathFromFlatmap(t *testing.T) {
    28  	tests := []struct {
    29  		Flatmap string
    30  		Type    cty.Type
    31  		Want    cty.Path
    32  		WantErr string
    33  	}{
    34  		{
    35  			Flatmap: "",
    36  			Type:    cty.EmptyObject,
    37  			Want:    nil,
    38  		},
    39  		{
    40  			Flatmap: "attr",
    41  			Type:    cty.EmptyObject,
    42  			Want:    nil,
    43  			WantErr: `attribute "attr" not found`,
    44  		},
    45  		{
    46  			Flatmap: "foo",
    47  			Type: cty.Object(map[string]cty.Type{
    48  				"foo": cty.String,
    49  			}),
    50  			Want: cty.Path{
    51  				cty.GetAttrStep{Name: "foo"},
    52  			},
    53  		},
    54  		{
    55  			Flatmap: "foo.#",
    56  			Type: cty.Object(map[string]cty.Type{
    57  				"foo": cty.List(cty.String),
    58  			}),
    59  			Want: cty.Path{
    60  				cty.GetAttrStep{Name: "foo"},
    61  			},
    62  		},
    63  		{
    64  			Flatmap: "foo.1",
    65  			Type: cty.Object(map[string]cty.Type{
    66  				"foo": cty.List(cty.String),
    67  			}),
    68  			Want: cty.Path{
    69  				cty.GetAttrStep{Name: "foo"},
    70  				cty.IndexStep{Key: cty.NumberIntVal(1)},
    71  			},
    72  		},
    73  		{
    74  			Flatmap: "foo.1",
    75  			Type: cty.Object(map[string]cty.Type{
    76  				"foo": cty.Tuple([]cty.Type{
    77  					cty.String,
    78  					cty.Bool,
    79  				}),
    80  			}),
    81  			Want: cty.Path{
    82  				cty.GetAttrStep{Name: "foo"},
    83  				cty.IndexStep{Key: cty.NumberIntVal(1)},
    84  			},
    85  		},
    86  		{
    87  			// a set index returns the set itself, since this being applied to
    88  			// a diff and the set is changing.
    89  			Flatmap: "foo.24534534",
    90  			Type: cty.Object(map[string]cty.Type{
    91  				"foo": cty.Set(cty.String),
    92  			}),
    93  			Want: cty.Path{
    94  				cty.GetAttrStep{Name: "foo"},
    95  			},
    96  		},
    97  		{
    98  			Flatmap: "foo.%",
    99  			Type: cty.Object(map[string]cty.Type{
   100  				"foo": cty.Map(cty.String),
   101  			}),
   102  			Want: cty.Path{
   103  				cty.GetAttrStep{Name: "foo"},
   104  			},
   105  		},
   106  		{
   107  			Flatmap: "foo.baz",
   108  			Type: cty.Object(map[string]cty.Type{
   109  				"foo": cty.Map(cty.Bool),
   110  			}),
   111  			Want: cty.Path{
   112  				cty.GetAttrStep{Name: "foo"},
   113  				cty.IndexStep{Key: cty.StringVal("baz")},
   114  			},
   115  		},
   116  		{
   117  			Flatmap: "foo.bar.baz",
   118  			Type: cty.Object(map[string]cty.Type{
   119  				"foo": cty.Map(
   120  					cty.Map(cty.Bool),
   121  				),
   122  			}),
   123  			Want: cty.Path{
   124  				cty.GetAttrStep{Name: "foo"},
   125  				cty.IndexStep{Key: cty.StringVal("bar")},
   126  				cty.IndexStep{Key: cty.StringVal("baz")},
   127  			},
   128  		},
   129  		{
   130  			Flatmap: "foo.bar.baz",
   131  			Type: cty.Object(map[string]cty.Type{
   132  				"foo": cty.Map(
   133  					cty.Object(map[string]cty.Type{
   134  						"baz": cty.String,
   135  					}),
   136  				),
   137  			}),
   138  			Want: cty.Path{
   139  				cty.GetAttrStep{Name: "foo"},
   140  				cty.IndexStep{Key: cty.StringVal("bar")},
   141  				cty.GetAttrStep{Name: "baz"},
   142  			},
   143  		},
   144  		{
   145  			Flatmap: "foo.0.bar",
   146  			Type: cty.Object(map[string]cty.Type{
   147  				"foo": cty.List(cty.Object(map[string]cty.Type{
   148  					"bar": cty.String,
   149  					"baz": cty.Bool,
   150  				})),
   151  			}),
   152  			Want: cty.Path{
   153  				cty.GetAttrStep{Name: "foo"},
   154  				cty.IndexStep{Key: cty.NumberIntVal(0)},
   155  				cty.GetAttrStep{Name: "bar"},
   156  			},
   157  		},
   158  		{
   159  			Flatmap: "foo.34534534.baz",
   160  			Type: cty.Object(map[string]cty.Type{
   161  				"foo": cty.Set(cty.Object(map[string]cty.Type{
   162  					"bar": cty.String,
   163  					"baz": cty.Bool,
   164  				})),
   165  			}),
   166  			Want: cty.Path{
   167  				cty.GetAttrStep{Name: "foo"},
   168  			},
   169  		},
   170  		{
   171  			Flatmap: "foo.bar.bang",
   172  			Type: cty.Object(map[string]cty.Type{
   173  				"foo": cty.String,
   174  			}),
   175  			WantErr: `invalid step "bar.bang"`,
   176  		},
   177  		{
   178  			// there should not be any attribute names with dots
   179  			Flatmap: "foo.bar.bang",
   180  			Type: cty.Object(map[string]cty.Type{
   181  				"foo.bar": cty.Map(cty.String),
   182  			}),
   183  			WantErr: `attribute "foo" not found`,
   184  		},
   185  		{
   186  			// We can only handle key names with dots if the map elements are a
   187  			// primitive type.
   188  			Flatmap: "foo.bar.bop",
   189  			Type: cty.Object(map[string]cty.Type{
   190  				"foo": cty.Map(cty.String),
   191  			}),
   192  			Want: cty.Path{
   193  				cty.GetAttrStep{Name: "foo"},
   194  				cty.IndexStep{Key: cty.StringVal("bar.bop")},
   195  			},
   196  		},
   197  		{
   198  			Flatmap: "foo.bar.0.baz",
   199  			Type: cty.Object(map[string]cty.Type{
   200  				"foo": cty.Map(
   201  					cty.List(
   202  						cty.Map(cty.String),
   203  					),
   204  				),
   205  			}),
   206  			Want: cty.Path{
   207  				cty.GetAttrStep{Name: "foo"},
   208  				cty.IndexStep{Key: cty.StringVal("bar")},
   209  				cty.IndexStep{Key: cty.NumberIntVal(0)},
   210  				cty.IndexStep{Key: cty.StringVal("baz")},
   211  			},
   212  		},
   213  	}
   214  
   215  	for _, test := range tests {
   216  		t.Run(fmt.Sprintf("%s as %#v", test.Flatmap, test.Type), func(t *testing.T) {
   217  			got, err := requiresReplacePath(test.Flatmap, test.Type)
   218  
   219  			if test.WantErr != "" {
   220  				if err == nil {
   221  					t.Fatalf("succeeded; want error: %s", test.WantErr)
   222  				}
   223  				if got, want := err.Error(), test.WantErr; !strings.Contains(got, want) {
   224  					t.Fatalf("wrong error\ngot:  %s\nwant: %s", got, want)
   225  				}
   226  				return
   227  			} else {
   228  				if err != nil {
   229  					t.Fatalf("unexpected error: %s", err.Error())
   230  				}
   231  			}
   232  
   233  			if !reflect.DeepEqual(got, test.Want) {
   234  				t.Fatalf("incorrect path\ngot:  %#v\nwant: %#v\n", got, test.Want)
   235  			}
   236  		})
   237  	}
   238  }
   239  
   240  func TestRequiresReplace(t *testing.T) {
   241  	for _, tc := range []struct {
   242  		name     string
   243  		attrs    []string
   244  		expected []cty.Path
   245  		ty       cty.Type
   246  	}{
   247  		{
   248  			name: "basic",
   249  			attrs: []string{
   250  				"foo",
   251  			},
   252  			ty: cty.Object(map[string]cty.Type{
   253  				"foo": cty.String,
   254  			}),
   255  			expected: []cty.Path{
   256  				cty.Path{cty.GetAttrStep{Name: "foo"}},
   257  			},
   258  		},
   259  		{
   260  			name: "two",
   261  			attrs: []string{
   262  				"foo",
   263  				"bar",
   264  			},
   265  			ty: cty.Object(map[string]cty.Type{
   266  				"foo": cty.String,
   267  				"bar": cty.String,
   268  			}),
   269  			expected: []cty.Path{
   270  				cty.Path{cty.GetAttrStep{Name: "foo"}},
   271  				cty.Path{cty.GetAttrStep{Name: "bar"}},
   272  			},
   273  		},
   274  		{
   275  			name: "nested object",
   276  			attrs: []string{
   277  				"foo.bar",
   278  			},
   279  			ty: cty.Object(map[string]cty.Type{
   280  				"foo": cty.Object(map[string]cty.Type{
   281  					"bar": cty.String,
   282  				}),
   283  			}),
   284  			expected: []cty.Path{
   285  				cty.Path{cty.GetAttrStep{Name: "foo"}, cty.GetAttrStep{Name: "bar"}},
   286  			},
   287  		},
   288  		{
   289  			name: "nested objects",
   290  			attrs: []string{
   291  				"foo.bar.baz",
   292  			},
   293  			ty: cty.Object(map[string]cty.Type{
   294  				"foo": cty.Object(map[string]cty.Type{
   295  					"bar": cty.Object(map[string]cty.Type{
   296  						"baz": cty.String,
   297  					}),
   298  				}),
   299  			}),
   300  			expected: []cty.Path{
   301  				cty.Path{cty.GetAttrStep{Name: "foo"}, cty.GetAttrStep{Name: "bar"}, cty.GetAttrStep{Name: "baz"}},
   302  			},
   303  		},
   304  		{
   305  			name: "nested map",
   306  			attrs: []string{
   307  				"foo.%",
   308  				"foo.bar",
   309  			},
   310  			ty: cty.Object(map[string]cty.Type{
   311  				"foo": cty.Map(cty.String),
   312  			}),
   313  			expected: []cty.Path{
   314  				cty.Path{cty.GetAttrStep{Name: "foo"}},
   315  			},
   316  		},
   317  		{
   318  			name: "nested list",
   319  			attrs: []string{
   320  				"foo.#",
   321  				"foo.1",
   322  			},
   323  			ty: cty.Object(map[string]cty.Type{
   324  				"foo": cty.Map(cty.String),
   325  			}),
   326  			expected: []cty.Path{
   327  				cty.Path{cty.GetAttrStep{Name: "foo"}},
   328  			},
   329  		},
   330  		{
   331  			name: "object in map",
   332  			attrs: []string{
   333  				"foo.bar.baz",
   334  			},
   335  			ty: cty.Object(map[string]cty.Type{
   336  				"foo": cty.Map(cty.Object(
   337  					map[string]cty.Type{
   338  						"baz": cty.String,
   339  					},
   340  				)),
   341  			}),
   342  			expected: []cty.Path{
   343  				cty.Path{cty.GetAttrStep{Name: "foo"}, cty.IndexStep{Key: cty.StringVal("bar")}, cty.GetAttrStep{Name: "baz"}},
   344  			},
   345  		},
   346  		{
   347  			name: "object in list",
   348  			attrs: []string{
   349  				"foo.1.baz",
   350  			},
   351  			ty: cty.Object(map[string]cty.Type{
   352  				"foo": cty.List(cty.Object(
   353  					map[string]cty.Type{
   354  						"baz": cty.String,
   355  					},
   356  				)),
   357  			}),
   358  			expected: []cty.Path{
   359  				cty.Path{cty.GetAttrStep{Name: "foo"}, cty.IndexStep{Key: cty.NumberIntVal(1)}, cty.GetAttrStep{Name: "baz"}},
   360  			},
   361  		},
   362  	} {
   363  		t.Run(tc.name, func(t *testing.T) {
   364  			rp, err := RequiresReplace(tc.attrs, tc.ty)
   365  			if err != nil {
   366  				t.Fatal(err)
   367  			}
   368  			if !cmp.Equal(tc.expected, rp, ignoreUnexported, valueComparer) {
   369  				t.Fatalf("\nexpected: %#v\ngot: %#v\n", tc.expected, rp)
   370  			}
   371  		})
   372  
   373  	}
   374  }
   375  
   376  func TestFlatmapKeyFromPath(t *testing.T) {
   377  	for i, tc := range []struct {
   378  		path cty.Path
   379  		attr string
   380  	}{
   381  		{
   382  			path: cty.Path{
   383  				cty.GetAttrStep{Name: "force_new"},
   384  			},
   385  			attr: "force_new",
   386  		},
   387  		{
   388  			path: cty.Path{
   389  				cty.GetAttrStep{Name: "attr"},
   390  				cty.IndexStep{Key: cty.NumberIntVal(0)},
   391  				cty.GetAttrStep{Name: "force_new"},
   392  			},
   393  			attr: "attr.0.force_new",
   394  		},
   395  		{
   396  			path: cty.Path{
   397  				cty.GetAttrStep{Name: "attr"},
   398  				cty.IndexStep{Key: cty.StringVal("key")},
   399  				cty.GetAttrStep{Name: "obj_attr"},
   400  				cty.IndexStep{Key: cty.NumberIntVal(0)},
   401  				cty.GetAttrStep{Name: "force_new"},
   402  			},
   403  			attr: "attr.key.obj_attr.0.force_new",
   404  		},
   405  	} {
   406  		t.Run(strconv.Itoa(i), func(t *testing.T) {
   407  			attr := FlatmapKeyFromPath(tc.path)
   408  			if attr != tc.attr {
   409  				t.Fatalf("expected:%q got:%q", tc.attr, attr)
   410  			}
   411  		})
   412  	}
   413  }