package blocktoattr import ( "testing" "github.com/hashicorp/hcl/v2" "github.com/hashicorp/hcl/v2/ext/dynblock" "github.com/hashicorp/hcl/v2/hcldec" "github.com/hashicorp/hcl/v2/hclsyntax" hcljson "github.com/hashicorp/hcl/v2/json" "github.com/hashicorp/terraform/internal/configs/configschema" "github.com/zclconf/go-cty/cty" ) func TestFixUpBlockAttrs(t *testing.T) { fooSchema := &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "foo": { Type: cty.List(cty.Object(map[string]cty.Type{ "bar": cty.String, })), Optional: true, }, }, } tests := map[string]struct { src string json bool schema *configschema.Block want cty.Value wantErrs bool }{ "empty": { src: ``, schema: &configschema.Block{}, want: cty.EmptyObjectVal, }, "empty JSON": { src: `{}`, json: true, schema: &configschema.Block{}, want: cty.EmptyObjectVal, }, "unset": { src: ``, schema: fooSchema, want: cty.ObjectVal(map[string]cty.Value{ "foo": cty.NullVal(fooSchema.Attributes["foo"].Type), }), }, "unset JSON": { src: `{}`, json: true, schema: fooSchema, want: cty.ObjectVal(map[string]cty.Value{ "foo": cty.NullVal(fooSchema.Attributes["foo"].Type), }), }, "no fixup required, with one value": { src: ` foo = [ { bar = "baz" }, ] `, schema: fooSchema, want: cty.ObjectVal(map[string]cty.Value{ "foo": cty.ListVal([]cty.Value{ cty.ObjectVal(map[string]cty.Value{ "bar": cty.StringVal("baz"), }), }), }), }, "no fixup required, with two values": { src: ` foo = [ { bar = "baz" }, { bar = "boop" }, ] `, schema: fooSchema, want: cty.ObjectVal(map[string]cty.Value{ "foo": cty.ListVal([]cty.Value{ cty.ObjectVal(map[string]cty.Value{ "bar": cty.StringVal("baz"), }), cty.ObjectVal(map[string]cty.Value{ "bar": cty.StringVal("boop"), }), }), }), }, "no fixup required, with values, JSON": { src: `{"foo": [{"bar": "baz"}]}`, json: true, schema: fooSchema, want: cty.ObjectVal(map[string]cty.Value{ "foo": cty.ListVal([]cty.Value{ cty.ObjectVal(map[string]cty.Value{ "bar": cty.StringVal("baz"), }), }), }), }, "no fixup required, empty": { src: ` foo = [] `, schema: fooSchema, want: cty.ObjectVal(map[string]cty.Value{ "foo": cty.ListValEmpty(fooSchema.Attributes["foo"].Type.ElementType()), }), }, "no fixup required, empty, JSON": { src: `{"foo":[]}`, json: true, schema: fooSchema, want: cty.ObjectVal(map[string]cty.Value{ "foo": cty.ListValEmpty(fooSchema.Attributes["foo"].Type.ElementType()), }), }, "fixup one block": { src: ` foo { bar = "baz" } `, schema: fooSchema, want: cty.ObjectVal(map[string]cty.Value{ "foo": cty.ListVal([]cty.Value{ cty.ObjectVal(map[string]cty.Value{ "bar": cty.StringVal("baz"), }), }), }), }, "fixup one block omitting attribute": { src: ` foo {} `, schema: fooSchema, want: cty.ObjectVal(map[string]cty.Value{ "foo": cty.ListVal([]cty.Value{ cty.ObjectVal(map[string]cty.Value{ "bar": cty.NullVal(cty.String), }), }), }), }, "fixup two blocks": { src: ` foo { bar = baz } foo { bar = "boop" } `, schema: fooSchema, want: cty.ObjectVal(map[string]cty.Value{ "foo": cty.ListVal([]cty.Value{ cty.ObjectVal(map[string]cty.Value{ "bar": cty.StringVal("baz value"), }), cty.ObjectVal(map[string]cty.Value{ "bar": cty.StringVal("boop"), }), }), }), }, "interaction with dynamic block generation": { src: ` dynamic "foo" { for_each = ["baz", beep] content { bar = foo.value } } `, schema: fooSchema, want: cty.ObjectVal(map[string]cty.Value{ "foo": cty.ListVal([]cty.Value{ cty.ObjectVal(map[string]cty.Value{ "bar": cty.StringVal("baz"), }), cty.ObjectVal(map[string]cty.Value{ "bar": cty.StringVal("beep value"), }), }), }), }, "dynamic block with empty iterator": { src: ` dynamic "foo" { for_each = [] content { bar = foo.value } } `, schema: fooSchema, want: cty.ObjectVal(map[string]cty.Value{ "foo": cty.NullVal(fooSchema.Attributes["foo"].Type), }), }, "both attribute and block syntax": { src: ` foo = [] foo { bar = "baz" } `, schema: fooSchema, wantErrs: true, // Unsupported block type (user must be consistent about whether they consider foo to be a block type or an attribute) want: cty.ObjectVal(map[string]cty.Value{ "foo": cty.ListVal([]cty.Value{ cty.ObjectVal(map[string]cty.Value{ "bar": cty.StringVal("baz"), }), cty.ObjectVal(map[string]cty.Value{ "bar": cty.StringVal("boop"), }), }), }), }, "fixup inside block": { src: ` container { foo { bar = "baz" } foo { bar = "boop" } } container { foo { bar = beep } } `, schema: &configschema.Block{ BlockTypes: map[string]*configschema.NestedBlock{ "container": { Nesting: configschema.NestingList, Block: *fooSchema, }, }, }, want: cty.ObjectVal(map[string]cty.Value{ "container": cty.ListVal([]cty.Value{ cty.ObjectVal(map[string]cty.Value{ "foo": cty.ListVal([]cty.Value{ cty.ObjectVal(map[string]cty.Value{ "bar": cty.StringVal("baz"), }), cty.ObjectVal(map[string]cty.Value{ "bar": cty.StringVal("boop"), }), }), }), cty.ObjectVal(map[string]cty.Value{ "foo": cty.ListVal([]cty.Value{ cty.ObjectVal(map[string]cty.Value{ "bar": cty.StringVal("beep value"), }), }), }), }), }), }, "fixup inside attribute-as-block": { src: ` container { foo { bar = "baz" } foo { bar = "boop" } } container { foo { bar = beep } } `, schema: &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "container": { Type: cty.List(cty.Object(map[string]cty.Type{ "foo": cty.List(cty.Object(map[string]cty.Type{ "bar": cty.String, })), })), Optional: true, }, }, }, want: cty.ObjectVal(map[string]cty.Value{ "container": cty.ListVal([]cty.Value{ cty.ObjectVal(map[string]cty.Value{ "foo": cty.ListVal([]cty.Value{ cty.ObjectVal(map[string]cty.Value{ "bar": cty.StringVal("baz"), }), cty.ObjectVal(map[string]cty.Value{ "bar": cty.StringVal("boop"), }), }), }), cty.ObjectVal(map[string]cty.Value{ "foo": cty.ListVal([]cty.Value{ cty.ObjectVal(map[string]cty.Value{ "bar": cty.StringVal("beep value"), }), }), }), }), }), }, "nested fixup with dynamic block generation": { src: ` container { dynamic "foo" { for_each = ["baz", beep] content { bar = foo.value } } } `, schema: &configschema.Block{ BlockTypes: map[string]*configschema.NestedBlock{ "container": { Nesting: configschema.NestingList, Block: *fooSchema, }, }, }, want: cty.ObjectVal(map[string]cty.Value{ "container": cty.ListVal([]cty.Value{ cty.ObjectVal(map[string]cty.Value{ "foo": cty.ListVal([]cty.Value{ cty.ObjectVal(map[string]cty.Value{ "bar": cty.StringVal("baz"), }), cty.ObjectVal(map[string]cty.Value{ "bar": cty.StringVal("beep value"), }), }), }), }), }), }, "missing nested block items": { src: ` container { foo { bar = "one" } } `, schema: &configschema.Block{ BlockTypes: map[string]*configschema.NestedBlock{ "container": { Nesting: configschema.NestingList, MinItems: 2, Block: configschema.Block{ Attributes: map[string]*configschema.Attribute{ "foo": { Type: cty.List(cty.Object(map[string]cty.Type{ "bar": cty.String, })), Optional: true, }, }, }, }, }, }, want: cty.ObjectVal(map[string]cty.Value{ "container": cty.ListVal([]cty.Value{ cty.ObjectVal(map[string]cty.Value{ "foo": cty.ListVal([]cty.Value{ cty.ObjectVal(map[string]cty.Value{ "bar": cty.StringVal("baz"), }), }), }), }), }), wantErrs: true, }, "no fixup allowed with NestedType": { src: ` container { foo = "one" } `, schema: &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "container": { NestedType: &configschema.Object{ Nesting: configschema.NestingList, Attributes: map[string]*configschema.Attribute{ "foo": { Type: cty.String, }, }, }, }, }, }, want: cty.ObjectVal(map[string]cty.Value{ "container": cty.NullVal(cty.List( cty.Object(map[string]cty.Type{ "foo": cty.String, }), )), }), wantErrs: true, }, "no fixup allowed new types": { src: ` container { foo = "one" } `, schema: &configschema.Block{ Attributes: map[string]*configschema.Attribute{ // This could be a ConfigModeAttr fixup "container": { Type: cty.List(cty.Object(map[string]cty.Type{ "foo": cty.String, })), }, // But the presence of this type means it must have been // declared by a new SDK "new_type": { Type: cty.Object(map[string]cty.Type{ "boo": cty.String, }), }, }, }, want: cty.ObjectVal(map[string]cty.Value{ "container": cty.NullVal(cty.List( cty.Object(map[string]cty.Type{ "foo": cty.String, }), )), }), wantErrs: true, }, } ctx := &hcl.EvalContext{ Variables: map[string]cty.Value{ "bar": cty.StringVal("bar value"), "baz": cty.StringVal("baz value"), "beep": cty.StringVal("beep value"), }, } for name, test := range tests { t.Run(name, func(t *testing.T) { var f *hcl.File var diags hcl.Diagnostics if test.json { f, diags = hcljson.Parse([]byte(test.src), "test.tf.json") } else { f, diags = hclsyntax.ParseConfig([]byte(test.src), "test.tf", hcl.Pos{Line: 1, Column: 1}) } if diags.HasErrors() { for _, diag := range diags { t.Errorf("unexpected diagnostic: %s", diag) } t.FailNow() } // We'll expand dynamic blocks in the body first, to mimic how // we process this fixup when using the main "lang" package API. spec := test.schema.DecoderSpec() body := dynblock.Expand(f.Body, ctx) body = FixUpBlockAttrs(body, test.schema) got, diags := hcldec.Decode(body, spec, ctx) if test.wantErrs { if !diags.HasErrors() { t.Errorf("succeeded, but want error\ngot: %#v", got) } // check that our wrapped body returns the correct context by // verifying the Subject is valid. for _, d := range diags { if d.Subject.Filename == "" { t.Errorf("empty diagnostic subject: %#v", d.Subject) } } return } if !test.want.RawEquals(got) { t.Errorf("wrong result\ngot: %#v\nwant: %#v", got, test.want) } for _, diag := range diags { t.Errorf("unexpected diagnostic: %s", diag) } }) } }