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