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