mirror of
https://github.com/akvorado/akvorado.git
synced 2025-12-11 22:14:02 +01:00
When using `mapstructure:",squash"`, most structure-specific hook don't dive into the structure as they are provided with the parent structure. Add an helper to make them work on the embedded structure as well and use it for the generic "deprecated fields" hook, but also for the hook for the common Kafka configuration. This is a bit brittle. There are other use cases, but they may not need this change.
417 lines
9.0 KiB
Go
417 lines
9.0 KiB
Go
// SPDX-FileCopyrightText: 2022 Free Mobile
|
|
// SPDX-License-Identifier: AGPL-3.0-only
|
|
|
|
package helpers
|
|
|
|
import (
|
|
"errors"
|
|
"reflect"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/go-viper/mapstructure/v2"
|
|
)
|
|
|
|
func TestMapStructureMatchName(t *testing.T) {
|
|
cases := []struct {
|
|
pos Pos
|
|
mapKey string
|
|
fieldName string
|
|
expected bool
|
|
}{
|
|
{Mark(), "one", "one", true},
|
|
{Mark(), "one", "One", true},
|
|
{Mark(), "one-two", "OneTwo", true},
|
|
{Mark(), "onetwo", "OneTwo", true},
|
|
{Mark(), "One-Two", "OneTwo", true},
|
|
{Mark(), "two", "one", false},
|
|
}
|
|
for _, tc := range cases {
|
|
got := MapStructureMatchName(tc.mapKey, tc.fieldName)
|
|
if got && !tc.expected {
|
|
t.Errorf("%s%q == %q but expected !=", tc.pos, tc.mapKey, tc.fieldName)
|
|
} else if !got && tc.expected {
|
|
t.Errorf("%s%q != %q but expected ==", tc.pos, tc.mapKey, tc.fieldName)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestStringToSliceHookFunc(t *testing.T) {
|
|
type Configuration struct {
|
|
A []string
|
|
B []int
|
|
}
|
|
TestConfigurationDecode(t, ConfigurationDecodeCases{
|
|
{
|
|
Initial: func() interface{} { return Configuration{} },
|
|
Configuration: func() interface{} {
|
|
return gin.H{
|
|
"a": "blip,blop",
|
|
"b": "1,2,3,4",
|
|
}
|
|
},
|
|
Expected: Configuration{
|
|
A: []string{"blip", "blop"},
|
|
B: []int{1, 2, 3, 4},
|
|
},
|
|
},
|
|
})
|
|
}
|
|
|
|
func TestProtectedDecodeHook(t *testing.T) {
|
|
var configuration struct {
|
|
A string
|
|
B string
|
|
}
|
|
panicHook := func(from, _ reflect.Type, data interface{}) (interface{}, error) {
|
|
if from.Kind() == reflect.String {
|
|
panic(errors.New("noooo"))
|
|
}
|
|
return data, nil
|
|
}
|
|
decoder, err := mapstructure.NewDecoder(GetMapStructureDecoderConfig(&configuration, panicHook))
|
|
if err != nil {
|
|
t.Fatalf("NewDecoder() error:\n%+v", err)
|
|
}
|
|
err = decoder.Decode(gin.H{"A": "hello", "B": "bye"})
|
|
if err == nil {
|
|
t.Fatal("Decode() did not error")
|
|
} else {
|
|
got := strings.Split(err.Error(), "\n")
|
|
expected := []string{
|
|
`decoding failed due to the following error(s):`,
|
|
``,
|
|
`'A' internal error while parsing: noooo`,
|
|
`'B' internal error while parsing: noooo`,
|
|
}
|
|
if diff := Diff(got, expected); diff != "" {
|
|
t.Fatalf("Decode() error:\n%s", diff)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestDefaultValuesConfig(t *testing.T) {
|
|
type InnerConfiguration struct {
|
|
AA string
|
|
BB string
|
|
CC int
|
|
}
|
|
type OuterConfiguration struct {
|
|
DD []InnerConfiguration
|
|
}
|
|
RegisterMapstructureUnmarshallerHook(DefaultValuesUnmarshallerHook(InnerConfiguration{
|
|
BB: "hello",
|
|
CC: 10,
|
|
}))
|
|
TestConfigurationDecode(t, ConfigurationDecodeCases{
|
|
{
|
|
Initial: func() interface{} { return OuterConfiguration{} },
|
|
Configuration: func() interface{} {
|
|
return gin.H{
|
|
"dd": []gin.H{
|
|
{
|
|
"aa": "hello1",
|
|
"bb": "hello2",
|
|
"cc": 43,
|
|
},
|
|
{"cc": 44},
|
|
{"aa": "bye"},
|
|
},
|
|
}
|
|
},
|
|
Expected: OuterConfiguration{
|
|
DD: []InnerConfiguration{
|
|
{
|
|
AA: "hello1",
|
|
BB: "hello2",
|
|
CC: 43,
|
|
}, {
|
|
AA: "",
|
|
BB: "hello",
|
|
CC: 44,
|
|
}, {
|
|
AA: "bye",
|
|
BB: "hello",
|
|
CC: 10,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
})
|
|
}
|
|
|
|
func TestRenameConfig(t *testing.T) {
|
|
type Configuration struct {
|
|
UnchangedLabel string
|
|
NewLabel string
|
|
}
|
|
RegisterMapstructureUnmarshallerHook(RenameKeyUnmarshallerHook(Configuration{}, "OldLabel", "NewLabel"))
|
|
TestConfigurationDecode(t, ConfigurationDecodeCases{
|
|
{
|
|
Description: "no rename needed",
|
|
Initial: func() interface{} { return Configuration{} },
|
|
Configuration: func() interface{} {
|
|
return gin.H{
|
|
"unchanged-label": "hello",
|
|
"new-label": "bye",
|
|
}
|
|
},
|
|
Expected: Configuration{
|
|
UnchangedLabel: "hello",
|
|
NewLabel: "bye",
|
|
},
|
|
}, {
|
|
Description: "rename needed",
|
|
Initial: func() interface{} { return Configuration{} },
|
|
Configuration: func() interface{} {
|
|
return gin.H{
|
|
"unchanged-label": "hello",
|
|
"old-label": "bye",
|
|
}
|
|
},
|
|
Expected: Configuration{
|
|
UnchangedLabel: "hello",
|
|
NewLabel: "bye",
|
|
},
|
|
}, {
|
|
Description: "conflicts",
|
|
Initial: func() interface{} { return Configuration{} },
|
|
Configuration: func() interface{} {
|
|
return gin.H{
|
|
"unchanged-label": "hello",
|
|
"old-label": "bye",
|
|
"new-label": "whatt?",
|
|
}
|
|
},
|
|
Error: true,
|
|
},
|
|
})
|
|
}
|
|
|
|
func TestParametrizedConfig(t *testing.T) {
|
|
type InnerConfigurationType1 struct {
|
|
CC string
|
|
DD string
|
|
}
|
|
type InnerConfigurationType2 struct {
|
|
CC string
|
|
EE string
|
|
}
|
|
type OuterConfiguration struct {
|
|
AA string
|
|
BB string
|
|
Config interface{}
|
|
}
|
|
available := map[string](func() interface{}){
|
|
"type1": func() interface{} {
|
|
return InnerConfigurationType1{
|
|
CC: "cc1",
|
|
DD: "dd1",
|
|
}
|
|
},
|
|
"type2": func() interface{} {
|
|
return InnerConfigurationType2{
|
|
CC: "cc2",
|
|
EE: "ee2",
|
|
}
|
|
},
|
|
}
|
|
RegisterMapstructureUnmarshallerHook(ParametrizedConfigurationUnmarshallerHook(OuterConfiguration{}, available))
|
|
|
|
t.Run("unmarshal", func(t *testing.T) {
|
|
TestConfigurationDecode(t, ConfigurationDecodeCases{
|
|
{
|
|
Description: "type1",
|
|
Initial: func() interface{} { return OuterConfiguration{} },
|
|
Configuration: func() interface{} {
|
|
return gin.H{
|
|
"type": "type1",
|
|
"aa": "a1",
|
|
"bb": "b1",
|
|
"cc": "c1",
|
|
"dd": "d1",
|
|
}
|
|
},
|
|
Expected: OuterConfiguration{
|
|
AA: "a1",
|
|
BB: "b1",
|
|
Config: InnerConfigurationType1{
|
|
CC: "c1",
|
|
DD: "d1",
|
|
},
|
|
},
|
|
}, {
|
|
Description: "type2",
|
|
Initial: func() interface{} { return OuterConfiguration{} },
|
|
Configuration: func() interface{} {
|
|
return gin.H{
|
|
"type": "type2",
|
|
"aa": "a2",
|
|
"bb": "b2",
|
|
"cc": "c2",
|
|
"ee": "e2",
|
|
}
|
|
},
|
|
Expected: OuterConfiguration{
|
|
AA: "a2",
|
|
BB: "b2",
|
|
Config: InnerConfigurationType2{
|
|
CC: "c2",
|
|
EE: "e2",
|
|
},
|
|
},
|
|
}, {
|
|
Description: "unknown type",
|
|
Initial: func() interface{} { return OuterConfiguration{} },
|
|
Configuration: func() interface{} {
|
|
return gin.H{
|
|
"type": "type3",
|
|
"aa": "a2",
|
|
"bb": "b2",
|
|
"cc": "c2",
|
|
"ee": "e2",
|
|
}
|
|
},
|
|
Error: true,
|
|
},
|
|
})
|
|
})
|
|
t.Run("marshal", func(t *testing.T) {
|
|
config1 := OuterConfiguration{
|
|
AA: "a1",
|
|
BB: "b1",
|
|
Config: InnerConfigurationType1{
|
|
CC: "c1",
|
|
DD: "d1",
|
|
},
|
|
}
|
|
expected1 := gin.H{
|
|
"type": "type1",
|
|
"aa": "a1",
|
|
"bb": "b1",
|
|
"cc": "c1",
|
|
"dd": "d1",
|
|
}
|
|
got1, err := ParametrizedConfigurationMarshalYAML(config1, available)
|
|
if err != nil {
|
|
t.Fatalf("ParametrizedConfigurationMarshalYAML() error:\n%+v", err)
|
|
}
|
|
if diff := Diff(got1, expected1); diff != "" {
|
|
t.Fatalf("ParametrizedConfigurationMarshalYAML() (-got, +want):\n%s", diff)
|
|
}
|
|
|
|
config2 := OuterConfiguration{
|
|
AA: "a2",
|
|
BB: "b2",
|
|
Config: InnerConfigurationType2{
|
|
CC: "c2",
|
|
EE: "e2",
|
|
},
|
|
}
|
|
expected2 := gin.H{
|
|
"type": "type2",
|
|
"aa": "a2",
|
|
"bb": "b2",
|
|
"cc": "c2",
|
|
"ee": "e2",
|
|
}
|
|
got2, err := ParametrizedConfigurationMarshalYAML(config2, available)
|
|
if err != nil {
|
|
t.Fatalf("ParametrizedConfigurationMarshalYAML() error:\n%+v", err)
|
|
}
|
|
if diff := Diff(got2, expected2); diff != "" {
|
|
t.Fatalf("ParametrizedConfigurationMarshalYAML() (-got, +want):\n%s", diff)
|
|
}
|
|
|
|
config3 := OuterConfiguration{
|
|
AA: "a3",
|
|
BB: "b3",
|
|
Config: struct {
|
|
FF string
|
|
}{},
|
|
}
|
|
if _, err := ParametrizedConfigurationMarshalYAML(config3, available); err == nil {
|
|
t.Fatal("ParametrizedConfigurationMarshalYAML() did not error")
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestDeprecatedFields(t *testing.T) {
|
|
type Configuration struct {
|
|
A string
|
|
B string
|
|
}
|
|
RegisterMapstructureDeprecatedFields[Configuration]("C", "D")
|
|
TestConfigurationDecode(t, ConfigurationDecodeCases{
|
|
{
|
|
Initial: func() interface{} { return Configuration{} },
|
|
Configuration: func() interface{} {
|
|
return gin.H{
|
|
"a": "hello",
|
|
"b": "bye",
|
|
"c": "nooo",
|
|
"d": "yes",
|
|
}
|
|
},
|
|
Expected: Configuration{
|
|
A: "hello",
|
|
B: "bye",
|
|
},
|
|
}, {
|
|
Initial: func() interface{} { return Configuration{} },
|
|
Configuration: func() interface{} {
|
|
return gin.H{
|
|
"a": "hello",
|
|
"b": "bye",
|
|
"e": "nooo",
|
|
}
|
|
},
|
|
Error: true,
|
|
},
|
|
})
|
|
}
|
|
|
|
func TestDeprecatedFieldsInSquashedStructure(t *testing.T) {
|
|
type SubConfiguration struct {
|
|
A string
|
|
B string
|
|
}
|
|
type Configuration struct {
|
|
Sub SubConfiguration `mapstructure:",squash"`
|
|
E string
|
|
}
|
|
RegisterMapstructureDeprecatedFields[SubConfiguration]("C", "D")
|
|
TestConfigurationDecode(t, ConfigurationDecodeCases{
|
|
{
|
|
Initial: func() any { return Configuration{} },
|
|
Configuration: func() any {
|
|
return gin.H{
|
|
"a": "hello",
|
|
"b": "bye",
|
|
"c": "nooo",
|
|
"d": "yes",
|
|
"e": "maybe",
|
|
}
|
|
},
|
|
Expected: Configuration{
|
|
Sub: SubConfiguration{
|
|
A: "hello",
|
|
B: "bye",
|
|
},
|
|
E: "maybe",
|
|
},
|
|
}, {
|
|
Initial: func() any { return Configuration{} },
|
|
Configuration: func() any {
|
|
return gin.H{
|
|
"a": "hello",
|
|
"b": "bye",
|
|
"f": "nooo",
|
|
}
|
|
},
|
|
Error: true,
|
|
},
|
|
})
|
|
}
|