mirror of https://github.com/hashicorp/consul
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
610 lines
13 KiB
610 lines
13 KiB
// 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 TestNestedAndCollections_AttributeSyntax(t *testing.T) { |
|
hcl := ` |
|
primitives { |
|
uint32_val = 42 |
|
} |
|
|
|
primitives_map = { |
|
"foo" = { |
|
uint32_val = 42 |
|
} |
|
} |
|
|
|
protocol_map = { |
|
"foo" = "PROTOCOL_TCP" |
|
} |
|
|
|
primitives_list = [ |
|
{ |
|
uint32_val = 42 |
|
}, |
|
{ |
|
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)) |
|
}
|
|
|