// Copyright (c) The OpenTofu Authors // SPDX-License-Identifier: MPL-2.0 // Copyright (c) 2023 HashiCorp, Inc. // SPDX-License-Identifier: MPL-2.0 package hcl2shim import ( "fmt" "reflect" "testing" "github.com/opentofu/opentofu/internal/configs/configschema" "github.com/zclconf/go-cty/cty" ) func TestConfigValueFromHCL2Block(t *testing.T) { tests := []struct { Input cty.Value Schema *configschema.Block Want map[string]interface{} }{ { cty.ObjectVal(map[string]cty.Value{ "name": cty.StringVal("Ermintrude"), "age": cty.NumberIntVal(19), "address": cty.ObjectVal(map[string]cty.Value{ "street": cty.ListVal([]cty.Value{cty.StringVal("421 Shoreham Loop")}), "city": cty.StringVal("Fridgewater"), "state": cty.StringVal("MA"), "zip": cty.StringVal("91037"), }), }), &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "name": {Type: cty.String, Optional: true}, "age": {Type: cty.Number, Optional: true}, }, BlockTypes: map[string]*configschema.NestedBlock{ "address": { Nesting: configschema.NestingSingle, Block: configschema.Block{ Attributes: map[string]*configschema.Attribute{ "street": {Type: cty.List(cty.String), Optional: true}, "city": {Type: cty.String, Optional: true}, "state": {Type: cty.String, Optional: true}, "zip": {Type: cty.String, Optional: true}, }, }, }, }, }, map[string]interface{}{ "name": "Ermintrude", "age": int(19), "address": map[string]interface{}{ "street": []interface{}{"421 Shoreham Loop"}, "city": "Fridgewater", "state": "MA", "zip": "91037", }, }, }, { cty.ObjectVal(map[string]cty.Value{ "name": cty.StringVal("Ermintrude"), "age": cty.NumberIntVal(19), "address": cty.NullVal(cty.Object(map[string]cty.Type{ "street": cty.List(cty.String), "city": cty.String, "state": cty.String, "zip": cty.String, })), }), &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "name": {Type: cty.String, Optional: true}, "age": {Type: cty.Number, Optional: true}, }, BlockTypes: map[string]*configschema.NestedBlock{ "address": { Nesting: configschema.NestingSingle, Block: configschema.Block{ Attributes: map[string]*configschema.Attribute{ "street": {Type: cty.List(cty.String), Optional: true}, "city": {Type: cty.String, Optional: true}, "state": {Type: cty.String, Optional: true}, "zip": {Type: cty.String, Optional: true}, }, }, }, }, }, map[string]interface{}{ "name": "Ermintrude", "age": int(19), }, }, { cty.ObjectVal(map[string]cty.Value{ "name": cty.StringVal("Ermintrude"), "age": cty.NumberIntVal(19), "address": cty.ObjectVal(map[string]cty.Value{ "street": cty.ListVal([]cty.Value{cty.StringVal("421 Shoreham Loop")}), "city": cty.StringVal("Fridgewater"), "state": cty.StringVal("MA"), "zip": cty.NullVal(cty.String), // should be omitted altogether in result }), }), &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "name": {Type: cty.String, Optional: true}, "age": {Type: cty.Number, Optional: true}, }, BlockTypes: map[string]*configschema.NestedBlock{ "address": { Nesting: configschema.NestingSingle, Block: configschema.Block{ Attributes: map[string]*configschema.Attribute{ "street": {Type: cty.List(cty.String), Optional: true}, "city": {Type: cty.String, Optional: true}, "state": {Type: cty.String, Optional: true}, "zip": {Type: cty.String, Optional: true}, }, }, }, }, }, map[string]interface{}{ "name": "Ermintrude", "age": int(19), "address": map[string]interface{}{ "street": []interface{}{"421 Shoreham Loop"}, "city": "Fridgewater", "state": "MA", }, }, }, { cty.ObjectVal(map[string]cty.Value{ "address": cty.ListVal([]cty.Value{cty.EmptyObjectVal}), }), &configschema.Block{ BlockTypes: map[string]*configschema.NestedBlock{ "address": { Nesting: configschema.NestingList, Block: configschema.Block{}, }, }, }, map[string]interface{}{ "address": []interface{}{ map[string]interface{}{}, }, }, }, { cty.ObjectVal(map[string]cty.Value{ "address": cty.ListValEmpty(cty.EmptyObject), // should be omitted altogether in result }), &configschema.Block{ BlockTypes: map[string]*configschema.NestedBlock{ "address": { Nesting: configschema.NestingList, Block: configschema.Block{}, }, }, }, map[string]interface{}{}, }, { cty.ObjectVal(map[string]cty.Value{ "address": cty.SetVal([]cty.Value{cty.EmptyObjectVal}), }), &configschema.Block{ BlockTypes: map[string]*configschema.NestedBlock{ "address": { Nesting: configschema.NestingSet, Block: configschema.Block{}, }, }, }, map[string]interface{}{ "address": []interface{}{ map[string]interface{}{}, }, }, }, { cty.ObjectVal(map[string]cty.Value{ "address": cty.SetValEmpty(cty.EmptyObject), }), &configschema.Block{ BlockTypes: map[string]*configschema.NestedBlock{ "address": { Nesting: configschema.NestingSet, Block: configschema.Block{}, }, }, }, map[string]interface{}{}, }, { cty.ObjectVal(map[string]cty.Value{ "address": cty.MapVal(map[string]cty.Value{"foo": cty.EmptyObjectVal}), }), &configschema.Block{ BlockTypes: map[string]*configschema.NestedBlock{ "address": { Nesting: configschema.NestingMap, Block: configschema.Block{}, }, }, }, map[string]interface{}{ "address": map[string]interface{}{ "foo": map[string]interface{}{}, }, }, }, { cty.ObjectVal(map[string]cty.Value{ "address": cty.MapValEmpty(cty.EmptyObject), }), &configschema.Block{ BlockTypes: map[string]*configschema.NestedBlock{ "address": { Nesting: configschema.NestingMap, Block: configschema.Block{}, }, }, }, map[string]interface{}{}, }, { cty.NullVal(cty.EmptyObject), &configschema.Block{}, nil, }, } for _, test := range tests { t.Run(fmt.Sprintf("%#v", test.Input), func(t *testing.T) { got := ConfigValueFromHCL2Block(test.Input, test.Schema) if !reflect.DeepEqual(got, test.Want) { t.Errorf("wrong result\ninput: %#v\ngot: %#v\nwant: %#v", test.Input, got, test.Want) } }) } } func TestConfigValueFromHCL2(t *testing.T) { tests := []struct { Input cty.Value Want interface{} }{ { cty.True, true, }, { cty.False, false, }, { cty.NumberIntVal(12), int(12), }, { cty.NumberFloatVal(12.5), float64(12.5), }, { cty.StringVal("hello world"), "hello world", }, { cty.ObjectVal(map[string]cty.Value{ "name": cty.StringVal("Ermintrude"), "age": cty.NumberIntVal(19), "address": cty.ObjectVal(map[string]cty.Value{ "street": cty.ListVal([]cty.Value{cty.StringVal("421 Shoreham Loop")}), "city": cty.StringVal("Fridgewater"), "state": cty.StringVal("MA"), "zip": cty.StringVal("91037"), }), }), map[string]interface{}{ "name": "Ermintrude", "age": int(19), "address": map[string]interface{}{ "street": []interface{}{"421 Shoreham Loop"}, "city": "Fridgewater", "state": "MA", "zip": "91037", }, }, }, { cty.MapVal(map[string]cty.Value{ "foo": cty.StringVal("bar"), "bar": cty.StringVal("baz"), }), map[string]interface{}{ "foo": "bar", "bar": "baz", }, }, { cty.TupleVal([]cty.Value{ cty.StringVal("foo"), cty.True, }), []interface{}{ "foo", true, }, }, { cty.NullVal(cty.String), nil, }, { cty.UnknownVal(cty.String), UnknownVariableValue, }, } for _, test := range tests { t.Run(fmt.Sprintf("%#v", test.Input), func(t *testing.T) { got := ConfigValueFromHCL2(test.Input) if !reflect.DeepEqual(got, test.Want) { t.Errorf("wrong result\ninput: %#v\ngot: %#v\nwant: %#v", test.Input, got, test.Want) } }) } } func TestHCL2ValueFromConfigValue(t *testing.T) { tests := []struct { Input interface{} Want cty.Value }{ { nil, cty.NullVal(cty.DynamicPseudoType), }, { UnknownVariableValue, cty.DynamicVal, }, { true, cty.True, }, { false, cty.False, }, { int(12), cty.NumberIntVal(12), }, { int(0), cty.Zero, }, { float64(12.5), cty.NumberFloatVal(12.5), }, { "hello world", cty.StringVal("hello world"), }, { "O\u0308", // decomposed letter + diacritic cty.StringVal("\u00D6"), // NFC-normalized on entry into cty }, { []interface{}{}, cty.EmptyTupleVal, }, { []interface{}(nil), cty.EmptyTupleVal, }, { []interface{}{"hello", "world"}, cty.TupleVal([]cty.Value{cty.StringVal("hello"), cty.StringVal("world")}), }, { map[string]interface{}{}, cty.EmptyObjectVal, }, { map[string]interface{}(nil), cty.EmptyObjectVal, }, { map[string]interface{}{ "foo": "bar", "bar": "baz", }, cty.ObjectVal(map[string]cty.Value{ "foo": cty.StringVal("bar"), "bar": cty.StringVal("baz"), }), }, } for _, test := range tests { t.Run(fmt.Sprintf("%#v", test.Input), func(t *testing.T) { got := HCL2ValueFromConfigValue(test.Input) if !got.RawEquals(test.Want) { t.Errorf("wrong result\ninput: %#v\ngot: %#v\nwant: %#v", test.Input, got, test.Want) } }) } }