930 lines
22 KiB
Go
930 lines
22 KiB
Go
package configschema
|
|
|
|
import (
|
|
"sort"
|
|
"testing"
|
|
|
|
"github.com/apparentlymart/go-dump/dump"
|
|
"github.com/davecgh/go-spew/spew"
|
|
"github.com/google/go-cmp/cmp"
|
|
|
|
"github.com/hashicorp/hcl/v2"
|
|
"github.com/hashicorp/hcl/v2/hcldec"
|
|
"github.com/hashicorp/hcl/v2/hcltest"
|
|
"github.com/zclconf/go-cty/cty"
|
|
)
|
|
|
|
func TestBlockDecoderSpec(t *testing.T) {
|
|
tests := map[string]struct {
|
|
Schema *Block
|
|
TestBody hcl.Body
|
|
Want cty.Value
|
|
DiagCount int
|
|
}{
|
|
"empty": {
|
|
&Block{},
|
|
hcl.EmptyBody(),
|
|
cty.EmptyObjectVal,
|
|
0,
|
|
},
|
|
"nil": {
|
|
nil,
|
|
hcl.EmptyBody(),
|
|
cty.EmptyObjectVal,
|
|
0,
|
|
},
|
|
"attributes": {
|
|
&Block{
|
|
Attributes: map[string]*Attribute{
|
|
"optional": {
|
|
Type: cty.Number,
|
|
Optional: true,
|
|
},
|
|
"required": {
|
|
Type: cty.String,
|
|
Required: true,
|
|
},
|
|
"computed": {
|
|
Type: cty.List(cty.Bool),
|
|
Computed: true,
|
|
},
|
|
"optional_computed": {
|
|
Type: cty.Map(cty.Bool),
|
|
Optional: true,
|
|
Computed: true,
|
|
},
|
|
"optional_computed_overridden": {
|
|
Type: cty.Bool,
|
|
Optional: true,
|
|
Computed: true,
|
|
},
|
|
"optional_computed_unknown": {
|
|
Type: cty.String,
|
|
Optional: true,
|
|
Computed: true,
|
|
},
|
|
},
|
|
},
|
|
hcltest.MockBody(&hcl.BodyContent{
|
|
Attributes: hcl.Attributes{
|
|
"required": {
|
|
Name: "required",
|
|
Expr: hcltest.MockExprLiteral(cty.NumberIntVal(5)),
|
|
},
|
|
"optional_computed_overridden": {
|
|
Name: "optional_computed_overridden",
|
|
Expr: hcltest.MockExprLiteral(cty.True),
|
|
},
|
|
"optional_computed_unknown": {
|
|
Name: "optional_computed_overridden",
|
|
Expr: hcltest.MockExprLiteral(cty.UnknownVal(cty.String)),
|
|
},
|
|
},
|
|
}),
|
|
cty.ObjectVal(map[string]cty.Value{
|
|
"optional": cty.NullVal(cty.Number),
|
|
"required": cty.StringVal("5"), // converted from number to string
|
|
"computed": cty.NullVal(cty.List(cty.Bool)),
|
|
"optional_computed": cty.NullVal(cty.Map(cty.Bool)),
|
|
"optional_computed_overridden": cty.True,
|
|
"optional_computed_unknown": cty.UnknownVal(cty.String),
|
|
}),
|
|
0,
|
|
},
|
|
"dynamically-typed attribute": {
|
|
&Block{
|
|
Attributes: map[string]*Attribute{
|
|
"foo": {
|
|
Type: cty.DynamicPseudoType, // any type is permitted
|
|
Required: true,
|
|
},
|
|
},
|
|
},
|
|
hcltest.MockBody(&hcl.BodyContent{
|
|
Attributes: hcl.Attributes{
|
|
"foo": {
|
|
Name: "foo",
|
|
Expr: hcltest.MockExprLiteral(cty.True),
|
|
},
|
|
},
|
|
}),
|
|
cty.ObjectVal(map[string]cty.Value{
|
|
"foo": cty.True,
|
|
}),
|
|
0,
|
|
},
|
|
"dynamically-typed attribute omitted": {
|
|
&Block{
|
|
Attributes: map[string]*Attribute{
|
|
"foo": {
|
|
Type: cty.DynamicPseudoType, // any type is permitted
|
|
Optional: true,
|
|
},
|
|
},
|
|
},
|
|
hcltest.MockBody(&hcl.BodyContent{}),
|
|
cty.ObjectVal(map[string]cty.Value{
|
|
"foo": cty.NullVal(cty.DynamicPseudoType),
|
|
}),
|
|
0,
|
|
},
|
|
"required attribute omitted": {
|
|
&Block{
|
|
Attributes: map[string]*Attribute{
|
|
"foo": {
|
|
Type: cty.Bool,
|
|
Required: true,
|
|
},
|
|
},
|
|
},
|
|
hcltest.MockBody(&hcl.BodyContent{}),
|
|
cty.ObjectVal(map[string]cty.Value{
|
|
"foo": cty.NullVal(cty.Bool),
|
|
}),
|
|
1, // missing required attribute
|
|
},
|
|
"wrong attribute type": {
|
|
&Block{
|
|
Attributes: map[string]*Attribute{
|
|
"optional": {
|
|
Type: cty.Number,
|
|
Optional: true,
|
|
},
|
|
},
|
|
},
|
|
hcltest.MockBody(&hcl.BodyContent{
|
|
Attributes: hcl.Attributes{
|
|
"optional": {
|
|
Name: "optional",
|
|
Expr: hcltest.MockExprLiteral(cty.True),
|
|
},
|
|
},
|
|
}),
|
|
cty.ObjectVal(map[string]cty.Value{
|
|
"optional": cty.UnknownVal(cty.Number),
|
|
}),
|
|
1, // incorrect type; number required
|
|
},
|
|
"blocks": {
|
|
&Block{
|
|
BlockTypes: map[string]*NestedBlock{
|
|
"single": {
|
|
Nesting: NestingSingle,
|
|
Block: Block{},
|
|
},
|
|
"list": {
|
|
Nesting: NestingList,
|
|
Block: Block{},
|
|
},
|
|
"set": {
|
|
Nesting: NestingSet,
|
|
Block: Block{},
|
|
},
|
|
"map": {
|
|
Nesting: NestingMap,
|
|
Block: Block{},
|
|
},
|
|
},
|
|
},
|
|
hcltest.MockBody(&hcl.BodyContent{
|
|
Blocks: hcl.Blocks{
|
|
&hcl.Block{
|
|
Type: "list",
|
|
Body: hcl.EmptyBody(),
|
|
},
|
|
&hcl.Block{
|
|
Type: "single",
|
|
Body: hcl.EmptyBody(),
|
|
},
|
|
&hcl.Block{
|
|
Type: "list",
|
|
Body: hcl.EmptyBody(),
|
|
},
|
|
&hcl.Block{
|
|
Type: "set",
|
|
Body: hcl.EmptyBody(),
|
|
},
|
|
&hcl.Block{
|
|
Type: "map",
|
|
Labels: []string{"foo"},
|
|
LabelRanges: []hcl.Range{hcl.Range{}},
|
|
Body: hcl.EmptyBody(),
|
|
},
|
|
&hcl.Block{
|
|
Type: "map",
|
|
Labels: []string{"bar"},
|
|
LabelRanges: []hcl.Range{hcl.Range{}},
|
|
Body: hcl.EmptyBody(),
|
|
},
|
|
&hcl.Block{
|
|
Type: "set",
|
|
Body: hcl.EmptyBody(),
|
|
},
|
|
},
|
|
}),
|
|
cty.ObjectVal(map[string]cty.Value{
|
|
"single": cty.EmptyObjectVal,
|
|
"list": cty.ListVal([]cty.Value{
|
|
cty.EmptyObjectVal,
|
|
cty.EmptyObjectVal,
|
|
}),
|
|
"set": cty.SetVal([]cty.Value{
|
|
cty.EmptyObjectVal,
|
|
cty.EmptyObjectVal,
|
|
}),
|
|
"map": cty.MapVal(map[string]cty.Value{
|
|
"foo": cty.EmptyObjectVal,
|
|
"bar": cty.EmptyObjectVal,
|
|
}),
|
|
}),
|
|
0,
|
|
},
|
|
"blocks with dynamically-typed attributes": {
|
|
&Block{
|
|
BlockTypes: map[string]*NestedBlock{
|
|
"single": {
|
|
Nesting: NestingSingle,
|
|
Block: Block{
|
|
Attributes: map[string]*Attribute{
|
|
"a": {
|
|
Type: cty.DynamicPseudoType,
|
|
Optional: true,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
"list": {
|
|
Nesting: NestingList,
|
|
Block: Block{
|
|
Attributes: map[string]*Attribute{
|
|
"a": {
|
|
Type: cty.DynamicPseudoType,
|
|
Optional: true,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
"map": {
|
|
Nesting: NestingMap,
|
|
Block: Block{
|
|
Attributes: map[string]*Attribute{
|
|
"a": {
|
|
Type: cty.DynamicPseudoType,
|
|
Optional: true,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
hcltest.MockBody(&hcl.BodyContent{
|
|
Blocks: hcl.Blocks{
|
|
&hcl.Block{
|
|
Type: "list",
|
|
Body: hcl.EmptyBody(),
|
|
},
|
|
&hcl.Block{
|
|
Type: "single",
|
|
Body: hcl.EmptyBody(),
|
|
},
|
|
&hcl.Block{
|
|
Type: "list",
|
|
Body: hcl.EmptyBody(),
|
|
},
|
|
&hcl.Block{
|
|
Type: "map",
|
|
Labels: []string{"foo"},
|
|
LabelRanges: []hcl.Range{hcl.Range{}},
|
|
Body: hcl.EmptyBody(),
|
|
},
|
|
&hcl.Block{
|
|
Type: "map",
|
|
Labels: []string{"bar"},
|
|
LabelRanges: []hcl.Range{hcl.Range{}},
|
|
Body: hcl.EmptyBody(),
|
|
},
|
|
},
|
|
}),
|
|
cty.ObjectVal(map[string]cty.Value{
|
|
"single": cty.ObjectVal(map[string]cty.Value{
|
|
"a": cty.NullVal(cty.DynamicPseudoType),
|
|
}),
|
|
"list": cty.TupleVal([]cty.Value{
|
|
cty.ObjectVal(map[string]cty.Value{
|
|
"a": cty.NullVal(cty.DynamicPseudoType),
|
|
}),
|
|
cty.ObjectVal(map[string]cty.Value{
|
|
"a": cty.NullVal(cty.DynamicPseudoType),
|
|
}),
|
|
}),
|
|
"map": cty.ObjectVal(map[string]cty.Value{
|
|
"foo": cty.ObjectVal(map[string]cty.Value{
|
|
"a": cty.NullVal(cty.DynamicPseudoType),
|
|
}),
|
|
"bar": cty.ObjectVal(map[string]cty.Value{
|
|
"a": cty.NullVal(cty.DynamicPseudoType),
|
|
}),
|
|
}),
|
|
}),
|
|
0,
|
|
},
|
|
"too many list items": {
|
|
&Block{
|
|
BlockTypes: map[string]*NestedBlock{
|
|
"foo": {
|
|
Nesting: NestingList,
|
|
Block: Block{},
|
|
MaxItems: 1,
|
|
},
|
|
},
|
|
},
|
|
hcltest.MockBody(&hcl.BodyContent{
|
|
Blocks: hcl.Blocks{
|
|
&hcl.Block{
|
|
Type: "foo",
|
|
Body: hcl.EmptyBody(),
|
|
},
|
|
&hcl.Block{
|
|
Type: "foo",
|
|
Body: unknownBody{hcl.EmptyBody()},
|
|
},
|
|
},
|
|
}),
|
|
cty.ObjectVal(map[string]cty.Value{
|
|
"foo": cty.UnknownVal(cty.List(cty.EmptyObject)),
|
|
}),
|
|
0, // max items cannot be validated during decode
|
|
},
|
|
// dynamic blocks may fulfill MinItems, but there is only one block to
|
|
// decode.
|
|
"required MinItems": {
|
|
&Block{
|
|
BlockTypes: map[string]*NestedBlock{
|
|
"foo": {
|
|
Nesting: NestingList,
|
|
Block: Block{},
|
|
MinItems: 2,
|
|
},
|
|
},
|
|
},
|
|
hcltest.MockBody(&hcl.BodyContent{
|
|
Blocks: hcl.Blocks{
|
|
&hcl.Block{
|
|
Type: "foo",
|
|
Body: unknownBody{hcl.EmptyBody()},
|
|
},
|
|
},
|
|
}),
|
|
cty.ObjectVal(map[string]cty.Value{
|
|
"foo": cty.UnknownVal(cty.List(cty.EmptyObject)),
|
|
}),
|
|
0,
|
|
},
|
|
"extraneous attribute": {
|
|
&Block{},
|
|
hcltest.MockBody(&hcl.BodyContent{
|
|
Attributes: hcl.Attributes{
|
|
"extra": {
|
|
Name: "extra",
|
|
Expr: hcltest.MockExprLiteral(cty.StringVal("hello")),
|
|
},
|
|
},
|
|
}),
|
|
cty.EmptyObjectVal,
|
|
1, // extraneous attribute
|
|
},
|
|
}
|
|
|
|
for name, test := range tests {
|
|
t.Run(name, func(t *testing.T) {
|
|
spec := test.Schema.DecoderSpec()
|
|
|
|
got, diags := hcldec.Decode(test.TestBody, spec, nil)
|
|
if len(diags) != test.DiagCount {
|
|
t.Errorf("wrong number of diagnostics %d; want %d", len(diags), test.DiagCount)
|
|
for _, diag := range diags {
|
|
t.Logf("- %s", diag.Error())
|
|
}
|
|
}
|
|
|
|
if !got.RawEquals(test.Want) {
|
|
t.Logf("[INFO] implied schema is %s", spew.Sdump(hcldec.ImpliedSchema(spec)))
|
|
t.Errorf("wrong result\ngot: %s\nwant: %s", dump.Value(got), dump.Value(test.Want))
|
|
}
|
|
|
|
// Double-check that we're producing consistent results for DecoderSpec
|
|
// and ImpliedType.
|
|
impliedType := test.Schema.ImpliedType()
|
|
if errs := got.Type().TestConformance(impliedType); len(errs) != 0 {
|
|
t.Errorf("result does not conform to the schema's implied type")
|
|
for _, err := range errs {
|
|
t.Logf("- %s", err.Error())
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// this satisfies hcldec.UnknownBody to simulate a dynamic block with an
|
|
// unknown number of values.
|
|
type unknownBody struct {
|
|
hcl.Body
|
|
}
|
|
|
|
func (b unknownBody) Unknown() bool {
|
|
return true
|
|
}
|
|
|
|
func TestAttributeDecoderSpec(t *testing.T) {
|
|
tests := map[string]struct {
|
|
Schema *Attribute
|
|
TestBody hcl.Body
|
|
Want cty.Value
|
|
DiagCount int
|
|
}{
|
|
"empty": {
|
|
&Attribute{},
|
|
hcl.EmptyBody(),
|
|
cty.NilVal,
|
|
0,
|
|
},
|
|
"nil": {
|
|
nil,
|
|
hcl.EmptyBody(),
|
|
cty.NilVal,
|
|
0,
|
|
},
|
|
"optional string (null)": {
|
|
&Attribute{
|
|
Type: cty.String,
|
|
Optional: true,
|
|
},
|
|
hcltest.MockBody(&hcl.BodyContent{}),
|
|
cty.NullVal(cty.String),
|
|
0,
|
|
},
|
|
"optional string": {
|
|
&Attribute{
|
|
Type: cty.String,
|
|
Optional: true,
|
|
},
|
|
hcltest.MockBody(&hcl.BodyContent{
|
|
Attributes: hcl.Attributes{
|
|
"attr": {
|
|
Name: "attr",
|
|
Expr: hcltest.MockExprLiteral(cty.StringVal("bar")),
|
|
},
|
|
},
|
|
}),
|
|
cty.StringVal("bar"),
|
|
0,
|
|
},
|
|
"NestedType with required string": {
|
|
&Attribute{
|
|
NestedType: &Object{
|
|
Nesting: NestingSingle,
|
|
Attributes: map[string]*Attribute{
|
|
"foo": {
|
|
Type: cty.String,
|
|
Required: true,
|
|
},
|
|
},
|
|
},
|
|
Optional: true,
|
|
},
|
|
hcltest.MockBody(&hcl.BodyContent{
|
|
Attributes: hcl.Attributes{
|
|
"attr": {
|
|
Name: "attr",
|
|
Expr: hcltest.MockExprLiteral(cty.ObjectVal(map[string]cty.Value{
|
|
"foo": cty.StringVal("bar"),
|
|
})),
|
|
},
|
|
},
|
|
}),
|
|
cty.ObjectVal(map[string]cty.Value{
|
|
"foo": cty.StringVal("bar"),
|
|
}),
|
|
0,
|
|
},
|
|
"NestedType with optional attributes": {
|
|
&Attribute{
|
|
NestedType: &Object{
|
|
Nesting: NestingSingle,
|
|
Attributes: map[string]*Attribute{
|
|
"foo": {
|
|
Type: cty.String,
|
|
Optional: true,
|
|
},
|
|
"bar": {
|
|
Type: cty.String,
|
|
Optional: true,
|
|
},
|
|
},
|
|
},
|
|
Optional: true,
|
|
},
|
|
hcltest.MockBody(&hcl.BodyContent{
|
|
Attributes: hcl.Attributes{
|
|
"attr": {
|
|
Name: "attr",
|
|
Expr: hcltest.MockExprLiteral(cty.ObjectVal(map[string]cty.Value{
|
|
"foo": cty.StringVal("bar"),
|
|
})),
|
|
},
|
|
},
|
|
}),
|
|
cty.ObjectVal(map[string]cty.Value{
|
|
"foo": cty.StringVal("bar"),
|
|
"bar": cty.NullVal(cty.String),
|
|
}),
|
|
0,
|
|
},
|
|
"NestedType with missing required string": {
|
|
&Attribute{
|
|
NestedType: &Object{
|
|
Nesting: NestingSingle,
|
|
Attributes: map[string]*Attribute{
|
|
"foo": {
|
|
Type: cty.String,
|
|
Required: true,
|
|
},
|
|
},
|
|
},
|
|
Optional: true,
|
|
},
|
|
hcltest.MockBody(&hcl.BodyContent{
|
|
Attributes: hcl.Attributes{
|
|
"attr": {
|
|
Name: "attr",
|
|
Expr: hcltest.MockExprLiteral(cty.EmptyObjectVal),
|
|
},
|
|
},
|
|
}),
|
|
cty.UnknownVal(cty.Object(map[string]cty.Type{
|
|
"foo": cty.String,
|
|
})),
|
|
1,
|
|
},
|
|
// NestedModes
|
|
"NestedType NestingModeList valid": {
|
|
&Attribute{
|
|
NestedType: &Object{
|
|
Nesting: NestingList,
|
|
Attributes: map[string]*Attribute{
|
|
"foo": {
|
|
Type: cty.String,
|
|
Required: true,
|
|
},
|
|
},
|
|
},
|
|
Optional: true,
|
|
},
|
|
hcltest.MockBody(&hcl.BodyContent{
|
|
Attributes: hcl.Attributes{
|
|
"attr": {
|
|
Name: "attr",
|
|
Expr: hcltest.MockExprLiteral(cty.ListVal([]cty.Value{
|
|
cty.ObjectVal(map[string]cty.Value{
|
|
"foo": cty.StringVal("bar"),
|
|
}),
|
|
cty.ObjectVal(map[string]cty.Value{
|
|
"foo": cty.StringVal("baz"),
|
|
}),
|
|
})),
|
|
},
|
|
},
|
|
}),
|
|
cty.ListVal([]cty.Value{
|
|
cty.ObjectVal(map[string]cty.Value{"foo": cty.StringVal("bar")}),
|
|
cty.ObjectVal(map[string]cty.Value{"foo": cty.StringVal("baz")}),
|
|
}),
|
|
0,
|
|
},
|
|
"NestedType NestingModeList invalid": {
|
|
&Attribute{
|
|
NestedType: &Object{
|
|
Nesting: NestingList,
|
|
Attributes: map[string]*Attribute{
|
|
"foo": {
|
|
Type: cty.String,
|
|
Required: true,
|
|
},
|
|
},
|
|
},
|
|
Optional: true,
|
|
},
|
|
hcltest.MockBody(&hcl.BodyContent{
|
|
Attributes: hcl.Attributes{
|
|
"attr": {
|
|
Name: "attr",
|
|
Expr: hcltest.MockExprLiteral(cty.ListVal([]cty.Value{cty.ObjectVal(map[string]cty.Value{
|
|
// "foo" should be a string, not a list
|
|
"foo": cty.ListVal([]cty.Value{cty.StringVal("bar"), cty.StringVal("baz")}),
|
|
})})),
|
|
},
|
|
},
|
|
}),
|
|
cty.UnknownVal(cty.List(cty.Object(map[string]cty.Type{"foo": cty.String}))),
|
|
1,
|
|
},
|
|
"NestedType NestingModeSet valid": {
|
|
&Attribute{
|
|
NestedType: &Object{
|
|
Nesting: NestingSet,
|
|
Attributes: map[string]*Attribute{
|
|
"foo": {
|
|
Type: cty.String,
|
|
Required: true,
|
|
},
|
|
},
|
|
},
|
|
Optional: true,
|
|
},
|
|
hcltest.MockBody(&hcl.BodyContent{
|
|
Attributes: hcl.Attributes{
|
|
"attr": {
|
|
Name: "attr",
|
|
Expr: hcltest.MockExprLiteral(cty.SetVal([]cty.Value{
|
|
cty.ObjectVal(map[string]cty.Value{
|
|
"foo": cty.StringVal("bar"),
|
|
}),
|
|
cty.ObjectVal(map[string]cty.Value{
|
|
"foo": cty.StringVal("baz"),
|
|
}),
|
|
})),
|
|
},
|
|
},
|
|
}),
|
|
cty.SetVal([]cty.Value{
|
|
cty.ObjectVal(map[string]cty.Value{"foo": cty.StringVal("bar")}),
|
|
cty.ObjectVal(map[string]cty.Value{"foo": cty.StringVal("baz")}),
|
|
}),
|
|
0,
|
|
},
|
|
"NestedType NestingModeSet invalid": {
|
|
&Attribute{
|
|
NestedType: &Object{
|
|
Nesting: NestingSet,
|
|
Attributes: map[string]*Attribute{
|
|
"foo": {
|
|
Type: cty.String,
|
|
Required: true,
|
|
},
|
|
},
|
|
},
|
|
Optional: true,
|
|
},
|
|
hcltest.MockBody(&hcl.BodyContent{
|
|
Attributes: hcl.Attributes{
|
|
"attr": {
|
|
Name: "attr",
|
|
Expr: hcltest.MockExprLiteral(cty.SetVal([]cty.Value{cty.ObjectVal(map[string]cty.Value{
|
|
// "foo" should be a string, not a list
|
|
"foo": cty.ListVal([]cty.Value{cty.StringVal("bar"), cty.StringVal("baz")}),
|
|
})})),
|
|
},
|
|
},
|
|
}),
|
|
cty.UnknownVal(cty.Set(cty.Object(map[string]cty.Type{"foo": cty.String}))),
|
|
1,
|
|
},
|
|
"NestedType NestingModeMap valid": {
|
|
&Attribute{
|
|
NestedType: &Object{
|
|
Nesting: NestingMap,
|
|
Attributes: map[string]*Attribute{
|
|
"foo": {
|
|
Type: cty.String,
|
|
Required: true,
|
|
},
|
|
},
|
|
},
|
|
Optional: true,
|
|
},
|
|
hcltest.MockBody(&hcl.BodyContent{
|
|
Attributes: hcl.Attributes{
|
|
"attr": {
|
|
Name: "attr",
|
|
Expr: hcltest.MockExprLiteral(cty.MapVal(map[string]cty.Value{
|
|
"one": cty.ObjectVal(map[string]cty.Value{
|
|
"foo": cty.StringVal("bar"),
|
|
}),
|
|
"two": cty.ObjectVal(map[string]cty.Value{
|
|
"foo": cty.StringVal("baz"),
|
|
}),
|
|
})),
|
|
},
|
|
},
|
|
}),
|
|
cty.MapVal(map[string]cty.Value{
|
|
"one": cty.ObjectVal(map[string]cty.Value{"foo": cty.StringVal("bar")}),
|
|
"two": cty.ObjectVal(map[string]cty.Value{"foo": cty.StringVal("baz")}),
|
|
}),
|
|
0,
|
|
},
|
|
"NestedType NestingModeMap invalid": {
|
|
&Attribute{
|
|
NestedType: &Object{
|
|
Nesting: NestingMap,
|
|
Attributes: map[string]*Attribute{
|
|
"foo": {
|
|
Type: cty.String,
|
|
Required: true,
|
|
},
|
|
},
|
|
},
|
|
Optional: true,
|
|
},
|
|
hcltest.MockBody(&hcl.BodyContent{
|
|
Attributes: hcl.Attributes{
|
|
"attr": {
|
|
Name: "attr",
|
|
Expr: hcltest.MockExprLiteral(cty.MapVal(map[string]cty.Value{
|
|
"one": cty.ObjectVal(map[string]cty.Value{
|
|
// "foo" should be a string, not a list
|
|
"foo": cty.ListVal([]cty.Value{cty.StringVal("bar"), cty.StringVal("baz")}),
|
|
}),
|
|
})),
|
|
},
|
|
},
|
|
}),
|
|
cty.UnknownVal(cty.Map(cty.Object(map[string]cty.Type{"foo": cty.String}))),
|
|
1,
|
|
},
|
|
"deeply NestedType NestingModeList valid": {
|
|
&Attribute{
|
|
NestedType: &Object{
|
|
Nesting: NestingList,
|
|
Attributes: map[string]*Attribute{
|
|
"foo": {
|
|
NestedType: &Object{
|
|
Nesting: NestingList,
|
|
Attributes: map[string]*Attribute{
|
|
"bar": {
|
|
Type: cty.String,
|
|
Required: true,
|
|
},
|
|
},
|
|
},
|
|
Required: true,
|
|
},
|
|
},
|
|
},
|
|
Optional: true,
|
|
},
|
|
hcltest.MockBody(&hcl.BodyContent{
|
|
Attributes: hcl.Attributes{
|
|
"attr": {
|
|
Name: "attr",
|
|
Expr: hcltest.MockExprLiteral(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("boz")}),
|
|
}),
|
|
}),
|
|
cty.ObjectVal(map[string]cty.Value{
|
|
"foo": cty.ListVal([]cty.Value{
|
|
cty.ObjectVal(map[string]cty.Value{"bar": cty.StringVal("biz")}),
|
|
cty.ObjectVal(map[string]cty.Value{"bar": cty.StringVal("buz")}),
|
|
}),
|
|
}),
|
|
})),
|
|
},
|
|
},
|
|
}),
|
|
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("boz")}),
|
|
})}),
|
|
cty.ObjectVal(map[string]cty.Value{"foo": cty.ListVal([]cty.Value{
|
|
cty.ObjectVal(map[string]cty.Value{"bar": cty.StringVal("biz")}),
|
|
cty.ObjectVal(map[string]cty.Value{"bar": cty.StringVal("buz")}),
|
|
})}),
|
|
}),
|
|
0,
|
|
},
|
|
"deeply NestedType NestingList invalid": {
|
|
&Attribute{
|
|
NestedType: &Object{
|
|
Nesting: NestingList,
|
|
Attributes: map[string]*Attribute{
|
|
"foo": {
|
|
NestedType: &Object{
|
|
Nesting: NestingList,
|
|
Attributes: map[string]*Attribute{
|
|
"bar": {
|
|
Type: cty.Number,
|
|
Required: true,
|
|
},
|
|
},
|
|
},
|
|
Required: true,
|
|
},
|
|
},
|
|
},
|
|
Optional: true,
|
|
},
|
|
hcltest.MockBody(&hcl.BodyContent{
|
|
Attributes: hcl.Attributes{
|
|
"attr": {
|
|
Name: "attr",
|
|
Expr: hcltest.MockExprLiteral(cty.ListVal([]cty.Value{
|
|
cty.ObjectVal(map[string]cty.Value{
|
|
"foo": cty.ListVal([]cty.Value{
|
|
// bar should be a Number
|
|
cty.ObjectVal(map[string]cty.Value{"bar": cty.True}),
|
|
cty.ObjectVal(map[string]cty.Value{"bar": cty.False}),
|
|
}),
|
|
}),
|
|
})),
|
|
},
|
|
},
|
|
}),
|
|
cty.UnknownVal(cty.List(cty.Object(map[string]cty.Type{
|
|
"foo": cty.List(cty.Object(map[string]cty.Type{"bar": cty.Number})),
|
|
}))),
|
|
1,
|
|
},
|
|
}
|
|
|
|
for name, test := range tests {
|
|
t.Run(name, func(t *testing.T) {
|
|
spec := test.Schema.decoderSpec("attr")
|
|
got, diags := hcldec.Decode(test.TestBody, spec, nil)
|
|
if len(diags) != test.DiagCount {
|
|
t.Errorf("wrong number of diagnostics %d; want %d", len(diags), test.DiagCount)
|
|
for _, diag := range diags {
|
|
t.Logf("- %s", diag.Error())
|
|
}
|
|
}
|
|
|
|
if !got.RawEquals(test.Want) {
|
|
t.Logf("[INFO] implied schema is %s", spew.Sdump(hcldec.ImpliedSchema(spec)))
|
|
t.Errorf("wrong result\ngot: %s\nwant: %s", dump.Value(got), dump.Value(test.Want))
|
|
}
|
|
})
|
|
}
|
|
|
|
}
|
|
|
|
// TestAttributeDecodeSpec_panic is a temporary test which verifies that
|
|
// decoderSpec panics when an invalid Attribute schema is encountered. It will
|
|
// be removed when InternalValidate() is extended to validate Attribute specs
|
|
// (and is used). See the #FIXME in decoderSpec.
|
|
func TestAttributeDecoderSpec_panic(t *testing.T) {
|
|
attrS := &Attribute{
|
|
Type: cty.Object(map[string]cty.Type{
|
|
"nested_attribute": cty.String,
|
|
}),
|
|
NestedType: &Object{},
|
|
Optional: true,
|
|
}
|
|
|
|
defer func() { recover() }()
|
|
attrS.decoderSpec("attr")
|
|
t.Errorf("expected panic")
|
|
}
|
|
|
|
func TestListOptionalAttrsFromObject(t *testing.T) {
|
|
tests := []struct {
|
|
input *Object
|
|
want []string
|
|
}{
|
|
{
|
|
nil,
|
|
[]string{},
|
|
},
|
|
{
|
|
&Object{},
|
|
[]string{},
|
|
},
|
|
{
|
|
&Object{
|
|
Nesting: NestingSingle,
|
|
Attributes: map[string]*Attribute{
|
|
"optional": {Type: cty.String, Optional: true},
|
|
"required": {Type: cty.Number, Required: true},
|
|
"computed": {Type: cty.List(cty.Bool), Computed: true},
|
|
"optional_computed": {Type: cty.Map(cty.Bool), Optional: true, Computed: true},
|
|
},
|
|
},
|
|
[]string{"optional", "computed", "optional_computed"},
|
|
},
|
|
}
|
|
|
|
for _, test := range tests {
|
|
got := listOptionalAttrsFromObject(test.input)
|
|
|
|
// order is irrelevant
|
|
sort.Strings(got)
|
|
sort.Strings(test.want)
|
|
|
|
if diff := cmp.Diff(got, test.want); diff != "" {
|
|
t.Fatalf("wrong result: %s\n", diff)
|
|
}
|
|
}
|
|
}
|