package hcl2shim import ( "fmt" "reflect" "strconv" "strings" "testing" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/go-cmp/cmp" "github.com/zclconf/go-cty/cty" ) var ( ignoreUnexported = cmpopts.IgnoreUnexported(cty.GetAttrStep{}, cty.IndexStep{}) valueComparer = cmp.Comparer(cty.Value.RawEquals) ) func TestPathFromFlatmap(t *testing.T) { tests := []struct { Flatmap string Type cty.Type Want cty.Path WantErr string }{ { Flatmap: "", Type: cty.EmptyObject, Want: nil, }, { Flatmap: "attr", Type: cty.EmptyObject, Want: nil, WantErr: `attribute "attr" not found`, }, { Flatmap: "foo", Type: cty.Object(map[string]cty.Type{ "foo": cty.String, }), Want: cty.Path{ cty.GetAttrStep{Name: "foo"}, }, }, { Flatmap: "foo.#", Type: cty.Object(map[string]cty.Type{ "foo": cty.List(cty.String), }), Want: cty.Path{ cty.GetAttrStep{Name: "foo"}, }, }, { Flatmap: "foo.1", Type: cty.Object(map[string]cty.Type{ "foo": cty.List(cty.String), }), Want: cty.Path{ cty.GetAttrStep{Name: "foo"}, cty.IndexStep{Key: cty.NumberIntVal(1)}, }, }, { Flatmap: "foo.1", Type: cty.Object(map[string]cty.Type{ "foo": cty.Tuple([]cty.Type{ cty.String, cty.Bool, }), }), Want: cty.Path{ cty.GetAttrStep{Name: "foo"}, cty.IndexStep{Key: cty.NumberIntVal(1)}, }, }, { // a set index returns the set itself, since this being applied to // a diff and the set is changing. Flatmap: "foo.24534534", Type: cty.Object(map[string]cty.Type{ "foo": cty.Set(cty.String), }), Want: cty.Path{ cty.GetAttrStep{Name: "foo"}, }, }, { Flatmap: "foo.%", Type: cty.Object(map[string]cty.Type{ "foo": cty.Map(cty.String), }), Want: cty.Path{ cty.GetAttrStep{Name: "foo"}, }, }, { Flatmap: "foo.baz", Type: cty.Object(map[string]cty.Type{ "foo": cty.Map(cty.Bool), }), Want: cty.Path{ cty.GetAttrStep{Name: "foo"}, cty.IndexStep{Key: cty.StringVal("baz")}, }, }, { Flatmap: "foo.bar.baz", Type: cty.Object(map[string]cty.Type{ "foo": cty.Map( cty.Map(cty.Bool), ), }), Want: cty.Path{ cty.GetAttrStep{Name: "foo"}, cty.IndexStep{Key: cty.StringVal("bar")}, cty.IndexStep{Key: cty.StringVal("baz")}, }, }, { Flatmap: "foo.bar.baz", Type: cty.Object(map[string]cty.Type{ "foo": cty.Map( cty.Object(map[string]cty.Type{ "baz": cty.String, }), ), }), Want: cty.Path{ cty.GetAttrStep{Name: "foo"}, cty.IndexStep{Key: cty.StringVal("bar")}, cty.GetAttrStep{Name: "baz"}, }, }, { Flatmap: "foo.0.bar", Type: cty.Object(map[string]cty.Type{ "foo": cty.List(cty.Object(map[string]cty.Type{ "bar": cty.String, "baz": cty.Bool, })), }), Want: cty.Path{ cty.GetAttrStep{Name: "foo"}, cty.IndexStep{Key: cty.NumberIntVal(0)}, cty.GetAttrStep{Name: "bar"}, }, }, { Flatmap: "foo.34534534.baz", Type: cty.Object(map[string]cty.Type{ "foo": cty.Set(cty.Object(map[string]cty.Type{ "bar": cty.String, "baz": cty.Bool, })), }), Want: cty.Path{ cty.GetAttrStep{Name: "foo"}, }, }, { Flatmap: "foo.bar.bang", Type: cty.Object(map[string]cty.Type{ "foo": cty.String, }), WantErr: `invalid step "bar.bang"`, }, { // there should not be any attribute names with dots Flatmap: "foo.bar.bang", Type: cty.Object(map[string]cty.Type{ "foo.bar": cty.Map(cty.String), }), WantErr: `attribute "foo" not found`, }, { // We can only handle key names with dots if the map elements are a // primitive type. Flatmap: "foo.bar.bop", Type: cty.Object(map[string]cty.Type{ "foo": cty.Map(cty.String), }), Want: cty.Path{ cty.GetAttrStep{Name: "foo"}, cty.IndexStep{Key: cty.StringVal("bar.bop")}, }, }, { Flatmap: "foo.bar.0.baz", Type: cty.Object(map[string]cty.Type{ "foo": cty.Map( cty.List( cty.Map(cty.String), ), ), }), Want: cty.Path{ cty.GetAttrStep{Name: "foo"}, cty.IndexStep{Key: cty.StringVal("bar")}, cty.IndexStep{Key: cty.NumberIntVal(0)}, cty.IndexStep{Key: cty.StringVal("baz")}, }, }, } for _, test := range tests { t.Run(fmt.Sprintf("%s as %#v", test.Flatmap, test.Type), func(t *testing.T) { got, err := requiresReplacePath(test.Flatmap, test.Type) if test.WantErr != "" { if err == nil { t.Fatalf("succeeded; want error: %s", test.WantErr) } if got, want := err.Error(), test.WantErr; !strings.Contains(got, want) { t.Fatalf("wrong error\ngot: %s\nwant: %s", got, want) } return } else { if err != nil { t.Fatalf("unexpected error: %s", err.Error()) } } if !reflect.DeepEqual(got, test.Want) { t.Fatalf("incorrect path\ngot: %#v\nwant: %#v\n", got, test.Want) } }) } } func TestRequiresReplace(t *testing.T) { for _, tc := range []struct { name string attrs []string expected []cty.Path ty cty.Type }{ { name: "basic", attrs: []string{ "foo", }, ty: cty.Object(map[string]cty.Type{ "foo": cty.String, }), expected: []cty.Path{ cty.Path{cty.GetAttrStep{Name: "foo"}}, }, }, { name: "two", attrs: []string{ "foo", "bar", }, ty: cty.Object(map[string]cty.Type{ "foo": cty.String, "bar": cty.String, }), expected: []cty.Path{ cty.Path{cty.GetAttrStep{Name: "foo"}}, cty.Path{cty.GetAttrStep{Name: "bar"}}, }, }, { name: "nested object", attrs: []string{ "foo.bar", }, ty: cty.Object(map[string]cty.Type{ "foo": cty.Object(map[string]cty.Type{ "bar": cty.String, }), }), expected: []cty.Path{ cty.Path{cty.GetAttrStep{Name: "foo"}, cty.GetAttrStep{Name: "bar"}}, }, }, { name: "nested objects", attrs: []string{ "foo.bar.baz", }, ty: cty.Object(map[string]cty.Type{ "foo": cty.Object(map[string]cty.Type{ "bar": cty.Object(map[string]cty.Type{ "baz": cty.String, }), }), }), expected: []cty.Path{ cty.Path{cty.GetAttrStep{Name: "foo"}, cty.GetAttrStep{Name: "bar"}, cty.GetAttrStep{Name: "baz"}}, }, }, { name: "nested map", attrs: []string{ "foo.%", "foo.bar", }, ty: cty.Object(map[string]cty.Type{ "foo": cty.Map(cty.String), }), expected: []cty.Path{ cty.Path{cty.GetAttrStep{Name: "foo"}}, }, }, { name: "nested list", attrs: []string{ "foo.#", "foo.1", }, ty: cty.Object(map[string]cty.Type{ "foo": cty.Map(cty.String), }), expected: []cty.Path{ cty.Path{cty.GetAttrStep{Name: "foo"}}, }, }, { name: "object in map", attrs: []string{ "foo.bar.baz", }, ty: cty.Object(map[string]cty.Type{ "foo": cty.Map(cty.Object( map[string]cty.Type{ "baz": cty.String, }, )), }), expected: []cty.Path{ cty.Path{cty.GetAttrStep{Name: "foo"}, cty.IndexStep{Key: cty.StringVal("bar")}, cty.GetAttrStep{Name: "baz"}}, }, }, { name: "object in list", attrs: []string{ "foo.1.baz", }, ty: cty.Object(map[string]cty.Type{ "foo": cty.List(cty.Object( map[string]cty.Type{ "baz": cty.String, }, )), }), expected: []cty.Path{ cty.Path{cty.GetAttrStep{Name: "foo"}, cty.IndexStep{Key: cty.NumberIntVal(1)}, cty.GetAttrStep{Name: "baz"}}, }, }, } { t.Run(tc.name, func(t *testing.T) { rp, err := RequiresReplace(tc.attrs, tc.ty) if err != nil { t.Fatal(err) } if !cmp.Equal(tc.expected, rp, ignoreUnexported, valueComparer) { t.Fatalf("\nexpected: %#v\ngot: %#v\n", tc.expected, rp) } }) } } func TestFlatmapKeyFromPath(t *testing.T) { for i, tc := range []struct { path cty.Path attr string }{ { path: cty.Path{ cty.GetAttrStep{Name: "force_new"}, }, attr: "force_new", }, { path: cty.Path{ cty.GetAttrStep{Name: "attr"}, cty.IndexStep{Key: cty.NumberIntVal(0)}, cty.GetAttrStep{Name: "force_new"}, }, attr: "attr.0.force_new", }, { path: cty.Path{ cty.GetAttrStep{Name: "attr"}, cty.IndexStep{Key: cty.StringVal("key")}, cty.GetAttrStep{Name: "obj_attr"}, cty.IndexStep{Key: cty.NumberIntVal(0)}, cty.GetAttrStep{Name: "force_new"}, }, attr: "attr.key.obj_attr.0.force_new", }, } { t.Run(strconv.Itoa(i), func(t *testing.T) { attr := FlatmapKeyFromPath(tc.path) if attr != tc.attr { t.Fatalf("expected:%q got:%q", tc.attr, attr) } }) } }