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 }