consul/lib/decode/decode_test.go

441 lines
9.8 KiB
Go
Raw Permalink Normal View History

// Copyright (c) HashiCorp, Inc.
[COMPLIANCE] License changes (#18443) * Adding explicit MPL license for sub-package This directory and its subdirectories (packages) contain files licensed with the MPLv2 `LICENSE` file in this directory and are intentionally licensed separately from the BSL `LICENSE` file at the root of this repository. * Adding explicit MPL license for sub-package This directory and its subdirectories (packages) contain files licensed with the MPLv2 `LICENSE` file in this directory and are intentionally licensed separately from the BSL `LICENSE` file at the root of this repository. * Updating the license from MPL to Business Source License Going forward, this project will be licensed under the Business Source License v1.1. Please see our blog post for more details at <Blog URL>, FAQ at www.hashicorp.com/licensing-faq, and details of the license at www.hashicorp.com/bsl. * add missing license headers * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 --------- Co-authored-by: hashicorp-copywrite[bot] <110428419+hashicorp-copywrite[bot]@users.noreply.github.com>
2023-08-11 13:12:13 +00:00
// 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)
})
}
}