opentofu/internal/lang/blocktoattr/fixup_test.go
James Bardin 8706a18c4b refine the skipFixup heuristic
We can also rule out some attribute types as indicating something other
than the legacy SDK.

- Tuple types were not generated at all.
- There were no single objects types, the convention was to use a block
  list or set of length 1.
- Maps of objects were not possible to generate, since named blocks were
  not implemented.
- Nested collections were not supported, but when they were generated they
  would have primitive types.
2021-09-22 16:29:50 -04:00

522 lines
11 KiB
Go

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)
}
})
}
}