// Copyright (c) HashiCorp, Inc. // SPDX-License-Identifier: BUSL-1.1 package protohcl import ( "encoding/json" "fmt" "reflect" "testing" "time" "github.com/stretchr/testify/require" "github.com/zclconf/go-cty/cty" "github.com/zclconf/go-cty/cty/function" "google.golang.org/protobuf/proto" "google.golang.org/protobuf/types/known/anypb" "github.com/hashicorp/consul/internal/protohcl/testproto" "github.com/hashicorp/hcl/v2/hclparse" ) func TestPrimitives(t *testing.T) { hcl := ` double_val = 1.234 float_val = 2.345 int32_val = 536870912 int64_val = 25769803776 uint32_val = 2148532224 uint64_val = 9223372041149743104 sint32_val = 536870912 sint64_val = 25769803776 fixed32_val = 2148532224 fixed64_val = 9223372041149743104 sfixed32_val = 536870912 sfixed64_val = 25769803776 bool_val = true string_val = "foo" // This is base64 encoded "bar" byte_val = "YmFy" ` var out testproto.Primitives err := Unmarshal([]byte(hcl), &out) require.NoError(t, err) require.Equal(t, out.DoubleVal, float64(1.234)) require.Equal(t, out.FloatVal, float32(2.345)) require.Equal(t, out.Int32Val, int32(536870912)) require.Equal(t, out.Int64Val, int64(25769803776)) require.Equal(t, out.Uint32Val, uint32(2148532224)) require.Equal(t, out.Uint64Val, uint64(9223372041149743104)) require.Equal(t, out.Sint32Val, int32(536870912)) require.Equal(t, out.Sint64Val, int64(25769803776)) require.Equal(t, out.Fixed32Val, uint32(2148532224)) require.Equal(t, out.Fixed64Val, uint64(9223372041149743104)) require.Equal(t, out.Sfixed32Val, int32(536870912)) require.Equal(t, out.Sfixed64Val, int64(25769803776)) require.Equal(t, out.BoolVal, true) require.Equal(t, out.StringVal, "foo") require.Equal(t, out.ByteVal, []byte("bar")) } func TestNestedAndCollections(t *testing.T) { hcl := ` primitives { uint32_val = 42 } primitives_map "foo" { uint32_val = 42 } protocol_map = { "foo" = "PROTOCOL_TCP" } primitives_list { uint32_val = 42 } primitives_list { uint32_val = 56 } int_list = [ 1, 2 ] ` var out testproto.NestedAndCollections err := Unmarshal([]byte(hcl), &out) require.NoError(t, err) require.NotNil(t, out.Primitives) require.Equal(t, out.Primitives.Uint32Val, uint32(42)) require.NotNil(t, out.PrimitivesMap) require.Equal(t, out.PrimitivesMap["foo"].Uint32Val, uint32(42)) require.NotNil(t, out.ProtocolMap) require.Equal(t, out.ProtocolMap["foo"], testproto.Protocol_PROTOCOL_TCP) require.Len(t, out.PrimitivesList, 2) require.Equal(t, out.PrimitivesList[0].Uint32Val, uint32(42)) require.Equal(t, out.PrimitivesList[1].Uint32Val, uint32(56)) require.Len(t, out.IntList, 2) require.Equal(t, out.IntList[1], int32(2)) } func TestPrimitiveWrappers(t *testing.T) { hcl := ` double_val = 1.234 float_val = 2.345 int32_val = 536870912 int64_val = 25769803776 uint32_val = 2148532224 uint64_val = 9223372041149743104 bool_val = true string_val = "foo" // This is base64 encoded "bar" bytes_val = "YmFy" ` var out testproto.Wrappers err := Unmarshal([]byte(hcl), &out) require.NoError(t, err) require.Equal(t, out.DoubleVal.Value, float64(1.234)) require.Equal(t, out.FloatVal.Value, float32(2.345)) require.Equal(t, out.Int32Val.Value, int32(536870912)) require.Equal(t, out.Int64Val.Value, int64(25769803776)) require.Equal(t, out.Uint32Val.Value, uint32(2148532224)) require.Equal(t, out.Uint64Val.Value, uint64(9223372041149743104)) require.Equal(t, out.BoolVal.Value, true) require.Equal(t, out.StringVal.Value, "foo") require.Equal(t, out.BytesVal.Value, []byte("bar")) } func TestNonDynamicWellKnown(t *testing.T) { hcl := ` empty_val = {} timestamp_val = "2023-02-27T12:34:56.789Z" duration_val = "12s" ` var out testproto.NonDynamicWellKnown err := Unmarshal([]byte(hcl), &out) require.NoError(t, err) require.NotNil(t, out.EmptyVal) require.NotNil(t, out.TimestampVal) require.Equal(t, out.TimestampVal.AsTime(), time.Date(2023, 2, 27, 12, 34, 56, 789000000, time.UTC)) require.NotNil(t, out.DurationVal) require.Equal(t, out.DurationVal.AsDuration(), time.Second*12) } func TestInvalidTimestamp(t *testing.T) { if testing.Short() { t.Skip("too slow for testing.Short") } t.Parallel() cases := map[string]struct { hcl string expectXDS bool }{ "invalid": { hcl: ` timestamp_val = "Sat Jun 12 2023 14:59:57 GMT+0200" `, }, "range error": { hcl: ` timestamp_val = "2023-02-27T25:34:56.789Z" `, }, } for name, tc := range cases { tc := tc var out testproto.NonDynamicWellKnown t.Run(name, func(t *testing.T) { err := Unmarshal([]byte(tc.hcl), &out) require.Error(t, err) require.Nil(t, out.TimestampVal) require.ErrorContains(t, err, "error parsing timestamp") }) } } func TestInvalidDuration(t *testing.T) { hcl := ` duration_val = "abc" ` var out testproto.NonDynamicWellKnown err := Unmarshal([]byte(hcl), &out) require.ErrorContains(t, err, "error parsing string duration:") require.Nil(t, out.DurationVal) } func TestOneOf(t *testing.T) { hcl1 := ` int32_val = 3 ` hcl2 := ` primitives { int32_val = 3 } ` hcl3 := ` int32_val = 3 primitives { int32_val = 4 } ` var out testproto.OneOf err := Unmarshal([]byte(hcl1), &out) require.NoError(t, err) require.Equal(t, out.GetInt32Val(), int32(3)) err = Unmarshal([]byte(hcl2), &out) require.NoError(t, err) primitives := out.GetPrimitives() require.NotNil(t, primitives) require.Equal(t, primitives.Int32Val, int32(3)) err = Unmarshal([]byte(hcl3), &out) require.Error(t, err) } func TestAny(t *testing.T) { hcl := ` any_val { type_url = "hashicorp.consul.internal.protohcl.testproto.Primitives" uint32_val = 42 } any_list = [ { type_url = "hashicorp.consul.internal.protohcl.testproto.Primitives" uint32_val = 123 }, { type_url = "hashicorp.consul.internal.protohcl.testproto.Wrappers" uint32_val = 321 } ] ` var out testproto.DynamicWellKnown err := Unmarshal([]byte(hcl), &out) require.NoError(t, err) require.NotNil(t, out.AnyVal) require.Equal(t, out.AnyVal.TypeUrl, "hashicorp.consul.internal.protohcl.testproto.Primitives") raw, err := anypb.UnmarshalNew(out.AnyVal, proto.UnmarshalOptions{}) require.NoError(t, err) require.NotNil(t, raw) primitives, ok := raw.(*testproto.Primitives) require.True(t, ok) require.Equal(t, primitives.Uint32Val, uint32(42)) } func TestAnyTypeDynamicWellKnown(t *testing.T) { hcl := ` any_val { type_url = "hashicorp.consul.internal.protohcl.testproto.DynamicWellKnown" any_val { type_url = "hashicorp.consul.internal.protohcl.testproto.Primitives" uint32_val = 42 } } ` var out testproto.DynamicWellKnown err := Unmarshal([]byte(hcl), &out) require.NoError(t, err) require.NotNil(t, out.AnyVal) require.Equal(t, out.AnyVal.TypeUrl, "hashicorp.consul.internal.protohcl.testproto.DynamicWellKnown") raw, err := anypb.UnmarshalNew(out.AnyVal, proto.UnmarshalOptions{}) require.NoError(t, err) require.NotNil(t, raw) anyVal, ok := raw.(*testproto.DynamicWellKnown) require.True(t, ok) res, err := anypb.UnmarshalNew(anyVal.AnyVal, proto.UnmarshalOptions{}) require.NoError(t, err) require.NotNil(t, res) primitives, ok := res.(*testproto.Primitives) require.True(t, ok) require.Equal(t, primitives.Uint32Val, uint32(42)) } func TestAnyTypeNestedAndCollections(t *testing.T) { hcl := ` any_val { type_url = "hashicorp.consul.internal.protohcl.testproto.NestedAndCollections" primitives { uint32_val = 42 } } ` var out testproto.DynamicWellKnown err := Unmarshal([]byte(hcl), &out) require.NoError(t, err) require.NotNil(t, out.AnyVal) require.Equal(t, out.AnyVal.TypeUrl, "hashicorp.consul.internal.protohcl.testproto.NestedAndCollections") raw, err := anypb.UnmarshalNew(out.AnyVal, proto.UnmarshalOptions{}) require.NoError(t, err) require.NotNil(t, raw) nestedCollections, ok := raw.(*testproto.NestedAndCollections) require.True(t, ok) require.NotNil(t, nestedCollections.Primitives) require.Equal(t, nestedCollections.Primitives.Uint32Val, uint32(42)) } func TestAnyTypeErrors(t *testing.T) { type testCase struct { description string hcl string error string } testCases := []testCase{ { description: "type_url is expected", hcl: ` any_val { uint32_val = 42 } `, error: "type_url field is required to decode Any", }, { description: "type_url is unknown", hcl: ` any_val { type_url = "hashicorp.consul.internal.protohcl.testproto.Integer" uint32_val = 42 } `, error: "error looking up type information for hashicorp.consul.internal.protohcl.testproto.Integer", }, { description: "unknown field", hcl: ` any_val { type_url = "hashicorp.consul.internal.protohcl.testproto.Primitives" int_val = 42 } `, error: "Unsupported argument; An argument named \"int_val\" is not expected here", }, } for _, tc := range testCases { tc := tc t.Run(tc.description, func(t *testing.T) { t.Parallel() var out testproto.DynamicWellKnown err := Unmarshal([]byte(tc.hcl), &out) require.Error(t, err) require.Contains(t, err.Error(), tc.error) }) } } func TestStruct(t *testing.T) { hcl := ` struct_val = { "null"= null "bool"= true "foo" = "bar" "baz" = 1.234 "nested" = { "foo" = 12, "bar" = "something" } } ` var out testproto.DynamicWellKnown err := Unmarshal([]byte(hcl), &out) require.NoError(t, err) require.NotNil(t, out.StructVal) valMap := out.StructVal.AsMap() jsonVal, err := json.Marshal(valMap) require.NoError(t, err) expected := `{ "null": null, "bool": true, "foo": "bar", "baz": 1.234, "nested": { "foo": 12, "bar": "something" } } ` require.JSONEq(t, expected, string(jsonVal)) } func TestStructList(t *testing.T) { hcl := ` struct_val = { "list_int" = [ 1, 2, 3, ] "list_string": [ "abc", "def" ] "list_bool": [ true, false ] "list_maps" = [ { "arrr" = "matey" }, { "hoist" = "the colors" } ] "list_list" = [ [ "hello", "world", null ] ] } ` var out testproto.DynamicWellKnown err := Unmarshal([]byte(hcl), &out) require.NoError(t, err) require.NotNil(t, out.StructVal) valMap := out.StructVal.AsMap() jsonVal, err := json.Marshal(valMap) require.NoError(t, err) expected := `{ "list_int": [ 1, 2, 3 ], "list_string": [ "abc", "def" ], "list_bool": [ true, false ], "list_maps": [ { "arrr": "matey" }, { "hoist": "the colors" } ], "list_list": [ [ "hello", "world", null ] ] } ` require.JSONEq(t, expected, string(jsonVal)) } func TestFunctionExecution(t *testing.T) { hcl := ` primitives = primitive_defaults() ` var out testproto.NestedAndCollections var ( testType = cty.Capsule("type", reflect.TypeOf(testproto.Primitives{})) test = function.New(&function.Spec{ Params: []function.Parameter{}, Type: function.StaticReturnType(testType), Impl: func(args []cty.Value, _ cty.Type) (cty.Value, error) { t := &testproto.Primitives{ StringVal: "test", Int32Val: 10, BoolVal: false, } return cty.CapsuleVal(testType, t), nil }, }) ) err := UnmarshalOptions{ Functions: map[string]function.Function{"primitive_defaults": test}, }.Unmarshal([]byte(hcl), &out) require.NoError(t, err) require.NotNil(t, out.Primitives) require.Equal(t, out.Primitives.StringVal, "test") require.Equal(t, out.Primitives.Int32Val, int32(10)) require.Equal(t, out.Primitives.BoolVal, false) } func TestSkipFields(t *testing.T) { u := UnmarshalOptions{} hcl := ` any_val { type_url = "hashicorp.consul.internal.protohcl.testproto.Primitives" uint32_val = 10 }` file, diags := hclparse.NewParser().ParseHCL([]byte(hcl), "") require.False(t, diags.HasErrors()) decoder := u.bodyDecoder(file.Body) decoder = decoder.SkipFields("type_url") decoder = decoder.SkipFields("type_url", "uint32_val") expected := map[string]struct{}{ "type_url": {}, "uint32_val": {}, } require.Contains(t, fmt.Sprintf("%v", decoder), fmt.Sprintf("%v", expected)) }