consul/lib/decode/decode_test.go

441 lines
9.8 KiB
Go

// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package decode
import (
"fmt"
"reflect"
"testing"
"github.com/hashicorp/hcl"
"github.com/mitchellh/mapstructure"
"github.com/stretchr/testify/require"
)
func TestHookTranslateKeys(t *testing.T) {
var testcases = []struct {
name string
data interface{}
expected interface{}
}{
{
name: "target of type struct, with struct receiver",
data: map[string]interface{}{
"S": map[string]interface{}{
"None": "no translation",
"OldOne": "value1",
"oldtwo": "value2",
},
},
expected: Config{
S: TypeStruct{
One: "value1",
Two: "value2",
None: "no translation",
},
},
},
{
name: "target of type ptr, with struct receiver",
data: map[string]interface{}{
"PS": map[string]interface{}{
"None": "no translation",
"OldOne": "value1",
"oldtwo": "value2",
},
},
expected: Config{
PS: &TypeStruct{
One: "value1",
Two: "value2",
None: "no translation",
},
},
},
{
name: "target of type ptr, with ptr receiver",
data: map[string]interface{}{
"PTR": map[string]interface{}{
"None": "no translation",
"old_THREE": "value3",
"oldfour": "value4",
},
},
expected: Config{
PTR: &TypePtrToStruct{
Three: "value3",
Four: "value4",
None: "no translation",
},
},
},
{
name: "target of type ptr, with struct receiver",
data: map[string]interface{}{
"PTRS": map[string]interface{}{
"None": "no translation",
"old_THREE": "value3",
"old_four": "value4",
},
},
expected: Config{
PTRS: TypePtrToStruct{
Three: "value3",
Four: "value4",
None: "no translation",
},
},
},
{
name: "target of type map",
data: map[string]interface{}{
"Blob": map[string]interface{}{
"one": 1,
"two": 2,
},
},
expected: Config{
Blob: map[string]interface{}{
"one": 1,
"two": 2,
},
},
},
{
name: "value already exists for canonical key",
data: map[string]interface{}{
"PS": map[string]interface{}{
"OldOne": "value1",
"One": "original1",
"oldTWO": "value2",
"two": "original2",
},
},
expected: Config{
PS: &TypeStruct{
One: "original1",
Two: "original2",
},
},
},
}
for _, tc := range testcases {
t.Run(tc.name, func(t *testing.T) {
cfg := Config{}
md := new(mapstructure.Metadata)
decoder, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{
DecodeHook: HookTranslateKeys,
Metadata: md,
Result: &cfg,
})
require.NoError(t, err)
require.NoError(t, decoder.Decode(tc.data))
require.Equal(t, cfg, tc.expected, "decode metadata: %#v", md)
})
}
}
type Config struct {
S TypeStruct
PS *TypeStruct
PTR *TypePtrToStruct
PTRS TypePtrToStruct
Blob map[string]interface{}
}
type TypeStruct struct {
One string `alias:"oldone"`
Two string `alias:"oldtwo"`
None string
}
type TypePtrToStruct struct {
Three string `alias:"old_three"`
Four string `alias:"old_four,oldfour"`
None string
}
func TestHookTranslateKeys_TargetStructHasPointerReceiver(t *testing.T) {
target := &TypePtrToStruct{}
md := new(mapstructure.Metadata)
decoder, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{
DecodeHook: HookTranslateKeys,
Metadata: md,
Result: target,
})
require.NoError(t, err)
data := map[string]interface{}{
"None": "no translation",
"Old_Three": "value3",
"OldFour": "value4",
}
expected := &TypePtrToStruct{
None: "no translation",
Three: "value3",
Four: "value4",
}
require.NoError(t, decoder.Decode(data))
require.Equal(t, expected, target, "decode metadata: %#v", md)
}
func TestHookTranslateKeys_DoesNotModifySourceData(t *testing.T) {
raw := map[string]interface{}{
"S": map[string]interface{}{
"None": "no translation",
"OldOne": "value1",
"oldtwo": "value2",
},
}
cfg := Config{}
decoder, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{
DecodeHook: HookTranslateKeys,
Result: &cfg,
})
require.NoError(t, err)
require.NoError(t, decoder.Decode(raw))
expected := map[string]interface{}{
"S": map[string]interface{}{
"None": "no translation",
"OldOne": "value1",
"oldtwo": "value2",
},
}
require.Equal(t, raw, expected)
}
type translateExample struct {
FieldDefaultCanonical string `alias:"first"`
FieldWithMapstructureTag string `alias:"second" mapstructure:"field_with_mapstruct_tag"`
FieldWithMapstructureTagOmit string `mapstructure:"field_with_mapstruct_omit,omitempty" alias:"third"`
FieldWithEmptyTag string `mapstructure:"" alias:"forth"`
EmbeddedStruct `mapstructure:",squash"`
*PtrEmbeddedStruct `mapstructure:",squash"`
BadField string `mapstructure:",squash"`
}
type EmbeddedStruct struct {
NextField string `alias:"next"`
}
type PtrEmbeddedStruct struct {
OtherNextField string `alias:"othernext"`
}
func TestTranslationsForType(t *testing.T) {
to := reflect.TypeOf(translateExample{})
actual := translationsForType(to)
expected := map[string]string{
"first": "fielddefaultcanonical",
"second": "field_with_mapstruct_tag",
"third": "field_with_mapstruct_omit",
"forth": "fieldwithemptytag",
"next": "nextfield",
"othernext": "othernextfield",
}
require.Equal(t, expected, actual)
}
type nested struct {
O map[string]interface{}
Slice []Item
Item Item
OSlice []map[string]interface{}
Sub *nested
}
type Item struct {
Name string
}
func TestHookWeakDecodeFromSlice_DoesNotModifySliceTargets(t *testing.T) {
source := `
slice {
name = "first"
}
slice {
name = "second"
}
item {
name = "solo"
}
sub {
oslice {
something = "v1"
}
}
`
target := &nested{}
err := decodeHCLToMapStructure(source, target)
require.NoError(t, err)
expected := &nested{
Slice: []Item{{Name: "first"}, {Name: "second"}},
Item: Item{Name: "solo"},
Sub: &nested{
OSlice: []map[string]interface{}{
{"something": "v1"},
},
},
}
require.Equal(t, target, expected)
}
func decodeHCLToMapStructure(source string, target interface{}) error {
raw := map[string]interface{}{}
err := hcl.Decode(&raw, source)
if err != nil {
return err
}
md := new(mapstructure.Metadata)
decoder, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{
DecodeHook: HookWeakDecodeFromSlice,
Metadata: md,
Result: target,
})
if err != nil {
return err
}
return decoder.Decode(&raw)
}
func TestHookWeakDecodeFromSlice_DoesNotModifySliceTargetsFromSliceInterface(t *testing.T) {
raw := map[string]interface{}{
"slice": []interface{}{map[string]interface{}{"name": "first"}},
"item": []interface{}{map[string]interface{}{"name": "solo"}},
"sub": []interface{}{
map[string]interface{}{
"OSlice": []interface{}{
map[string]interface{}{"something": "v1"},
},
"item": []interface{}{map[string]interface{}{"name": "subitem"}},
},
},
}
target := &nested{}
decoder, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{
DecodeHook: HookWeakDecodeFromSlice,
Result: target,
})
require.NoError(t, err)
err = decoder.Decode(&raw)
require.NoError(t, err)
expected := &nested{
Slice: []Item{{Name: "first"}},
Item: Item{Name: "solo"},
Sub: &nested{
OSlice: []map[string]interface{}{
{"something": "v1"},
},
Item: Item{Name: "subitem"},
},
}
require.Equal(t, target, expected)
}
func TestHookWeakDecodeFromSlice_ErrorsWithMultipleNestedBlocks(t *testing.T) {
source := `
item {
name = "first"
}
item {
name = "second"
}
`
target := &nested{}
err := decodeHCLToMapStructure(source, target)
require.Error(t, err)
require.Contains(t, err.Error(), "'Item' expected a map, got 'slice'")
}
func TestHookWeakDecodeFromSlice_UnpacksNestedBlocks(t *testing.T) {
source := `
item {
name = "first"
}
`
target := &nested{}
err := decodeHCLToMapStructure(source, target)
require.NoError(t, err)
expected := &nested{
Item: Item{Name: "first"},
}
require.Equal(t, target, expected)
}
func TestHookWeakDecodeFromSlice_NestedOpaqueConfig(t *testing.T) {
source := `
service {
proxy {
config {
envoy_gateway_bind_addresses {
all-interfaces {
address = "0.0.0.0"
port = 8443
}
}
}
}
}`
target := map[string]interface{}{}
err := decodeHCLToMapStructure(source, &target)
require.NoError(t, err)
expected := map[string]interface{}{
"service": map[string]interface{}{
"proxy": map[string]interface{}{
"config": map[string]interface{}{
"envoy_gateway_bind_addresses": map[string]interface{}{
"all-interfaces": map[string]interface{}{
"address": "0.0.0.0",
"port": 8443,
},
},
},
},
},
}
require.Equal(t, target, expected)
}
func TestFieldTags(t *testing.T) {
type testCase struct {
tags string
expected mapstructureFieldTags
}
fn := func(t *testing.T, tc testCase) {
tag := fmt.Sprintf(`mapstructure:"%v"`, tc.tags)
field := reflect.StructField{
Tag: reflect.StructTag(tag),
Name: "Original",
}
actual := fieldTags(field)
require.Equal(t, tc.expected, actual)
}
var testCases = []testCase{
{tags: "", expected: mapstructureFieldTags{name: "Original"}},
{tags: "just-a-name", expected: mapstructureFieldTags{name: "just-a-name"}},
{tags: "name,squash", expected: mapstructureFieldTags{name: "name", squash: true}},
{tags: ",squash", expected: mapstructureFieldTags{name: "Original", squash: true}},
{tags: ",omitempty,squash", expected: mapstructureFieldTags{name: "Original", squash: true}},
{tags: "named,omitempty,squash", expected: mapstructureFieldTags{name: "named", squash: true}},
}
for _, tc := range testCases {
t.Run(tc.tags, func(t *testing.T) {
fn(t, tc)
})
}
}