Skip to content

Commit 75261b9

Browse files
authored
⚙️ [config] Add tests for service configurations with embedded structs tagged with mapstructure:",squash" (#715)
<!-- Copyright (C) 2020-2022 Arm Limited or its affiliates and Contributors. All rights reserved. SPDX-License-Identifier: Apache-2.0 --> ### Description This PR adds tests to ensure that service configurations with embedded struct fields that are tagged with `mapstructure:",squash"` will be flattened and mapped correctly. It tests configuration mapping from both environment variables as well as from file. ### Test Coverage <!-- Please put an `x` in the correct box e.g. `[x]` to indicate the testing coverage of this change. --> - [X] This change is covered by existing or additional automated tests. - [ ] Manual testing has been performed (and evidence provided) as automated testing was not feasible. - [ ] Additional tests are not required for this change (e.g. documentation update).
1 parent 1c4cd20 commit 75261b9

File tree

7 files changed

+342
-5
lines changed

7 files changed

+342
-5
lines changed

.secrets.baseline

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,10 @@
6666
{
6767
"path": "detect_secrets.filters.allowlist.is_line_allowlisted"
6868
},
69+
{
70+
"path": "detect_secrets.filters.common.is_baseline_file",
71+
"filename": ".secrets.baseline"
72+
},
6973
{
7074
"path": "detect_secrets.filters.common.is_ignored_due_to_verification_policies",
7175
"min_level": 2
@@ -123,28 +127,28 @@
123127
"filename": "utils/config/service_configuration_test.go",
124128
"hashed_secret": "ddcec2f503a5d58f432a0beee3fb9544fa581f54",
125129
"is_verified": false,
126-
"line_number": 35
130+
"line_number": 37
127131
},
128132
{
129133
"type": "Secret Keyword",
130134
"filename": "utils/config/service_configuration_test.go",
131135
"hashed_secret": "7ca1cc114e7e5f955880bb96a5bf391b4dc20ab6",
132136
"is_verified": false,
133-
"line_number": 533
137+
"line_number": 535
134138
},
135139
{
136140
"type": "Secret Keyword",
137141
"filename": "utils/config/service_configuration_test.go",
138142
"hashed_secret": "11519c144be4850d95b34220a40030cbd5a36b57",
139143
"is_verified": false,
140-
"line_number": 628
144+
"line_number": 630
141145
},
142146
{
143147
"type": "Secret Keyword",
144148
"filename": "utils/config/service_configuration_test.go",
145149
"hashed_secret": "15fae91d8fa7f2c531c1cf3ddc745e1f4473c02d",
146150
"is_verified": false,
147-
"line_number": 635
151+
"line_number": 637
148152
}
149153
],
150154
"utils/filesystem/filehash_test.go": [
@@ -272,5 +276,5 @@
272276
}
273277
]
274278
},
275-
"generated_at": "2025-08-08T23:03:50Z"
279+
"generated_at": "2025-09-26T12:33:39Z"
276280
}

changes/20250925204435.feature

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
:gear: `[config]` Add tests for service configurations with embedded structs tagged with `mapstructure:",squash"`

utils/config/fixtures/env-test.env

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
# These env vars will map to a struct the embeds another struct with no mapstructure tag set (it will expect its struct name to be part of the env var)
2+
WITH_NO_TAG_TESTBASECFG_EMBEDDED1="embedded 1"
3+
WITH_NO_TAG_TESTBASECFG_EMBEDDED2="embedded 2"
4+
WITH_NO_TAG_NON_EMBEDDED1="non-embedded 1"
5+
WITH_NO_TAG_NON_EMBEDDED2="non-embedded 2"
6+
# These env vars will map to a struct the embeds another struct with a mapstructure tag set
7+
WITH_TAG_EMBEDDED_STRUCT_EMBEDDED1="embedded 1"
8+
WITH_TAG_EMBEDDED_STRUCT_EMBEDDED2="embedded 2"
9+
WITH_TAG_NON_EMBEDDED1="non-embedded 1"
10+
WITH_TAG_NON_EMBEDDED2="non-embedded 2"
11+
# These env vars will map to a struct the embeds another struct with the ",squash" mapstructure tag set
12+
WITH_SQUASH_TAG_EMBEDDED1="embedded 1"
13+
WITH_SQUASH_TAG_EMBEDDED2="embedded 2"
14+
WITH_SQUASH_TAG_NON_EMBEDDED1="non-embedded 1"
15+
WITH_SQUASH_TAG_NON_EMBEDDED2="non-embedded 2"
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"embedded1": "embedded 1",
3+
"embedded2": "embedded 2",
4+
"non_embedded1": "non-embedded 1"
5+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{
2+
"testbasecfg": {
3+
"embedded1": "embedded 1",
4+
"embedded2": "embedded 2"
5+
},
6+
"embedded_struct": {
7+
"embedded1": "embedded 1",
8+
"embedded2": "embedded 2"
9+
},
10+
"non_embedded1": "non-embedded 1"
11+
}

utils/config/service_configuration_test.go

Lines changed: 224 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"context"
99
"errors"
1010
"fmt"
11+
"io/fs"
1112
"math/rand"
1213
"os"
1314
"path/filepath"
@@ -16,6 +17,7 @@ import (
1617

1718
"github.com/go-faker/faker/v4"
1819
validation "github.com/go-ozzo/ozzo-validation/v4"
20+
"github.com/joho/godotenv"
1921
"github.com/spf13/pflag"
2022
"github.com/spf13/viper"
2123
"github.com/stretchr/testify/assert"
@@ -776,3 +778,225 @@ func TestServiceConfigurationLoadFromEnvironment(t *testing.T) {
776778
assert.True(t, configTest.TestConfig.Flag)
777779
assert.False(t, configTest.TestConfig2.Flag)
778780
}
781+
782+
type TestBaseCfg struct {
783+
Embedded1 string `mapstructure:"embedded1"`
784+
Embedded2 string `mapstructure:"embedded2"`
785+
}
786+
787+
func (cfg *TestBaseCfg) Validate() error {
788+
return validation.ValidateStruct(cfg,
789+
validation.Field(&cfg.Embedded1, validation.Required),
790+
validation.Field(&cfg.Embedded2, validation.Required),
791+
)
792+
}
793+
794+
type TestCfgWithEmbeddedCfg struct {
795+
TestBaseCfg
796+
NonEmbedded1 string `mapstructure:"non_embedded1"`
797+
NonEmbedded2 string `mapstructure:"non_embedded2"`
798+
}
799+
800+
func (cfg *TestCfgWithEmbeddedCfg) Validate() error {
801+
// Validate Embedded Structs
802+
err := ValidateEmbedded(cfg)
803+
if err != nil {
804+
return err
805+
}
806+
807+
return validation.ValidateStruct(cfg,
808+
validation.Field(&cfg.NonEmbedded1, validation.Required),
809+
)
810+
}
811+
812+
type TestCfgWithEmbeddedCfgWithTag struct {
813+
TestBaseCfg `mapstructure:"embedded_struct"`
814+
NonEmbedded1 string `mapstructure:"non_embedded1"`
815+
}
816+
817+
func (cfg *TestCfgWithEmbeddedCfgWithTag) Validate() error {
818+
// Validate Embedded Structs
819+
err := ValidateEmbedded(cfg)
820+
if err != nil {
821+
return err
822+
}
823+
824+
return validation.ValidateStruct(cfg,
825+
validation.Field(&cfg.NonEmbedded1, validation.Required),
826+
)
827+
}
828+
829+
type TestCfgWithEmbeddedCfgWithSquashTag struct {
830+
TestBaseCfg `mapstructure:",squash"`
831+
NonEmbedded1 string `mapstructure:"non_embedded1"`
832+
}
833+
834+
func (cfg *TestCfgWithEmbeddedCfgWithSquashTag) Validate() error {
835+
// Validate Embedded Structs
836+
err := ValidateEmbedded(cfg)
837+
if err != nil {
838+
return err
839+
}
840+
841+
return validation.ValidateStruct(cfg,
842+
validation.Field(&cfg.NonEmbedded1, validation.Required),
843+
)
844+
}
845+
846+
// Config values loaded from file should be correctly mapped onto a struct that embeds another struct with no mapstructure tag set
847+
func TestEmbeddedServiceConfigurationWithNoTagLoadFromFile(t *testing.T) {
848+
os.Clearenv()
849+
session := viper.New()
850+
testEmbedded := TestCfgWithEmbeddedCfg{}
851+
err := LoadFromEnvironment(session, "", &testEmbedded,
852+
&TestCfgWithEmbeddedCfg{}, filepath.Join(".", "fixtures", "nested-config-test.json"))
853+
require.NoError(t, err)
854+
assert.NotEmpty(t, testEmbedded.NonEmbedded1)
855+
assert.Equal(t, "non-embedded 1", testEmbedded.NonEmbedded1)
856+
assert.Empty(t, testEmbedded.NonEmbedded2)
857+
assert.NotEmpty(t, testEmbedded.Embedded1)
858+
assert.Equal(t, "embedded 1", testEmbedded.Embedded1)
859+
assert.NotEmpty(t, testEmbedded.Embedded2)
860+
assert.Equal(t, "embedded 2", testEmbedded.Embedded2)
861+
}
862+
863+
// Nested config values loaded from file should be correctly mapped onto a struct that embeds another struct with a mapstructure tag set
864+
func TestEmbeddedServiceConfigurationWithTagLoadFromFile(t *testing.T) {
865+
os.Clearenv()
866+
session := viper.New()
867+
testEmbeddedWithTag := TestCfgWithEmbeddedCfgWithTag{}
868+
err := LoadFromEnvironment(session, "", &testEmbeddedWithTag,
869+
&TestCfgWithEmbeddedCfgWithTag{}, filepath.Join(".", "fixtures", "nested-config-test.json"))
870+
require.NoError(t, err)
871+
assert.NotEmpty(t, testEmbeddedWithTag.NonEmbedded1)
872+
assert.Equal(t, "non-embedded 1", testEmbeddedWithTag.NonEmbedded1)
873+
assert.NotEmpty(t, testEmbeddedWithTag.Embedded1)
874+
assert.Equal(t, "embedded 1", testEmbeddedWithTag.Embedded1)
875+
assert.NotEmpty(t, testEmbeddedWithTag.Embedded2)
876+
assert.Equal(t, "embedded 2", testEmbeddedWithTag.Embedded2)
877+
}
878+
879+
// Flat config values loaded from file should be correctly mapped onto a struct that embeds another struct with the ",squash" mapstructure tag set
880+
func TestEmbeddedServiceConfigurationWithSquashTagLoadFromFile(t *testing.T) {
881+
os.Clearenv()
882+
session := viper.New()
883+
testEmbeddedWithSquashTag := TestCfgWithEmbeddedCfgWithSquashTag{}
884+
err := LoadFromEnvironment(session, "", &testEmbeddedWithSquashTag,
885+
&TestCfgWithEmbeddedCfgWithSquashTag{}, filepath.Join(".", "fixtures", "flat-config-test.json"))
886+
require.NoError(t, err)
887+
assert.NotEmpty(t, testEmbeddedWithSquashTag.NonEmbedded1)
888+
assert.Equal(t, "non-embedded 1", testEmbeddedWithSquashTag.NonEmbedded1)
889+
assert.NotEmpty(t, testEmbeddedWithSquashTag.Embedded1)
890+
assert.Equal(t, "embedded 1", testEmbeddedWithSquashTag.Embedded1)
891+
assert.NotEmpty(t, testEmbeddedWithSquashTag.Embedded2)
892+
assert.Equal(t, "embedded 2", testEmbeddedWithSquashTag.Embedded2)
893+
}
894+
895+
// Nested config values loaded from file should not be correctly mapped onto a struct that embeds another struct with the ",squash" mapstructure tag set
896+
func TestEmbeddedServiceConfigurationWithSquashTagLoadFromNestedFile(t *testing.T) {
897+
os.Clearenv()
898+
session := viper.New()
899+
testEmbeddedWithSquashTag := TestCfgWithEmbeddedCfgWithSquashTag{}
900+
err := LoadFromEnvironment(session, "", &testEmbeddedWithSquashTag,
901+
&TestCfgWithEmbeddedCfgWithSquashTag{}, filepath.Join(".", "fixtures", "nested-config-test.json"))
902+
require.Error(t, err)
903+
assert.NotEmpty(t, testEmbeddedWithSquashTag.NonEmbedded1)
904+
assert.Equal(t, "non-embedded 1", testEmbeddedWithSquashTag.NonEmbedded1)
905+
assert.Empty(t, testEmbeddedWithSquashTag.Embedded1)
906+
assert.NotEqual(t, "embedded 1", testEmbeddedWithSquashTag.Embedded1)
907+
assert.NotEqual(t, "embedded 2", testEmbeddedWithSquashTag.Embedded2)
908+
}
909+
910+
// Nested config values loaded from file should be correctly mapped onto a struct that embeds another struct and should maintain any defaults not overwritten
911+
func TestEmbeddedServiceConfigurationLoadFromFileWithDefaults(t *testing.T) {
912+
os.Clearenv()
913+
session := viper.New()
914+
testEmbedded := TestCfgWithEmbeddedCfg{}
915+
defaults := &TestCfgWithEmbeddedCfg{
916+
TestBaseCfg: TestBaseCfg{
917+
Embedded1: "a",
918+
Embedded2: "b",
919+
},
920+
NonEmbedded1: "c",
921+
NonEmbedded2: "d",
922+
}
923+
err := LoadFromEnvironment(session, "", &testEmbedded,
924+
defaults, filepath.Join(".", "fixtures", "nested-config-test.json"))
925+
require.NoError(t, err)
926+
assert.NotEmpty(t, testEmbedded.NonEmbedded1)
927+
assert.Equal(t, "non-embedded 1", testEmbedded.NonEmbedded1)
928+
assert.NotEmpty(t, testEmbedded.NonEmbedded2)
929+
assert.Equal(t, "d", testEmbedded.NonEmbedded2)
930+
assert.NotEmpty(t, testEmbedded.Embedded1)
931+
assert.Equal(t, "embedded 1", testEmbedded.Embedded1)
932+
assert.NotEmpty(t, testEmbedded.Embedded2)
933+
assert.Equal(t, "embedded 2", testEmbedded.Embedded2)
934+
}
935+
936+
// Config values loaded from env vars should be correctly mapped onto a struct that embeds another struct with no mapstructure tag set
937+
func TestEmbeddedServiceConfigurationWithNoTagLoadFromEnvironment(t *testing.T) {
938+
os.Clearenv()
939+
session := viper.New()
940+
testEmbedded := TestCfgWithEmbeddedCfg{}
941+
err := loadEnvIntoEnvironment(t, filepath.Join(".", "fixtures", "env-test.env"))
942+
require.NoError(t, err)
943+
944+
err = LoadFromEnvironment(session, "WITH_NO_TAG", &testEmbedded,
945+
&TestCfgWithEmbeddedCfg{}, "")
946+
require.NoError(t, err)
947+
assert.NotEmpty(t, testEmbedded.NonEmbedded1)
948+
assert.Equal(t, "non-embedded 1", testEmbedded.NonEmbedded1)
949+
assert.NotEmpty(t, testEmbedded.Embedded1)
950+
assert.Equal(t, "embedded 1", testEmbedded.Embedded1)
951+
assert.NotEmpty(t, testEmbedded.Embedded2)
952+
assert.Equal(t, "embedded 2", testEmbedded.Embedded2)
953+
}
954+
955+
// Config values loaded from env vars should be correctly mapped onto a struct that embeds another struct with a mapstructure tag set
956+
func TestEmbeddedServiceConfigurationWithTagLoadFromEnvironment(t *testing.T) {
957+
os.Clearenv()
958+
session := viper.New()
959+
testEmbeddedWithTag := TestCfgWithEmbeddedCfgWithTag{}
960+
err := loadEnvIntoEnvironment(t, filepath.Join(".", "fixtures", "env-test.env"))
961+
require.NoError(t, err)
962+
963+
err = LoadFromEnvironment(session, "WITH_TAG", &testEmbeddedWithTag,
964+
&TestCfgWithEmbeddedCfgWithTag{}, "")
965+
require.NoError(t, err)
966+
assert.NotEmpty(t, testEmbeddedWithTag.NonEmbedded1)
967+
assert.Equal(t, "non-embedded 1", testEmbeddedWithTag.NonEmbedded1)
968+
assert.NotEmpty(t, testEmbeddedWithTag.Embedded1)
969+
assert.Equal(t, "embedded 1", testEmbeddedWithTag.Embedded1)
970+
assert.NotEmpty(t, testEmbeddedWithTag.Embedded2)
971+
assert.Equal(t, "embedded 2", testEmbeddedWithTag.Embedded2)
972+
}
973+
974+
// Flat config values loaded from env vars should be correctly mapped onto a struct that embeds another struct with the ",squash" mapstructure tag set
975+
func TestEmbeddedServiceConfigurationWithSquashTagLoadFromEnvironment(t *testing.T) {
976+
os.Clearenv()
977+
session := viper.New()
978+
testEmbeddedWithSquashTag := TestCfgWithEmbeddedCfgWithSquashTag{}
979+
err := loadEnvIntoEnvironment(t, filepath.Join(".", "fixtures", "env-test.env"))
980+
require.NoError(t, err)
981+
982+
err = LoadFromEnvironment(session, "WITH_SQUASH_TAG", &testEmbeddedWithSquashTag,
983+
&TestCfgWithEmbeddedCfgWithSquashTag{}, "")
984+
require.NoError(t, err)
985+
assert.NotEmpty(t, testEmbeddedWithSquashTag.NonEmbedded1)
986+
assert.Equal(t, "non-embedded 1", testEmbeddedWithSquashTag.NonEmbedded1)
987+
assert.NotEmpty(t, testEmbeddedWithSquashTag.Embedded1)
988+
assert.Equal(t, "embedded 1", testEmbeddedWithSquashTag.Embedded1)
989+
assert.NotEmpty(t, testEmbeddedWithSquashTag.Embedded2)
990+
assert.Equal(t, "embedded 2", testEmbeddedWithSquashTag.Embedded2)
991+
}
992+
993+
func loadEnvIntoEnvironment(t *testing.T, envPath string) (err error) {
994+
t.Helper()
995+
_, err = fs.Stat(os.DirFS("."), envPath)
996+
require.NoError(t, err)
997+
998+
err = godotenv.Load(envPath)
999+
require.NoError(t, err)
1000+
1001+
return
1002+
}

0 commit comments

Comments
 (0)