diff --git a/.secrets.baseline b/.secrets.baseline index 6949fcf414..5a03324b6e 100644 --- a/.secrets.baseline +++ b/.secrets.baseline @@ -66,6 +66,10 @@ { "path": "detect_secrets.filters.allowlist.is_line_allowlisted" }, + { + "path": "detect_secrets.filters.common.is_baseline_file", + "filename": ".secrets.baseline" + }, { "path": "detect_secrets.filters.common.is_ignored_due_to_verification_policies", "min_level": 2 @@ -123,28 +127,28 @@ "filename": "utils/config/service_configuration_test.go", "hashed_secret": "ddcec2f503a5d58f432a0beee3fb9544fa581f54", "is_verified": false, - "line_number": 35 + "line_number": 37 }, { "type": "Secret Keyword", "filename": "utils/config/service_configuration_test.go", "hashed_secret": "7ca1cc114e7e5f955880bb96a5bf391b4dc20ab6", "is_verified": false, - "line_number": 533 + "line_number": 535 }, { "type": "Secret Keyword", "filename": "utils/config/service_configuration_test.go", "hashed_secret": "11519c144be4850d95b34220a40030cbd5a36b57", "is_verified": false, - "line_number": 628 + "line_number": 630 }, { "type": "Secret Keyword", "filename": "utils/config/service_configuration_test.go", "hashed_secret": "15fae91d8fa7f2c531c1cf3ddc745e1f4473c02d", "is_verified": false, - "line_number": 635 + "line_number": 637 } ], "utils/filesystem/filehash_test.go": [ @@ -272,5 +276,5 @@ } ] }, - "generated_at": "2025-08-08T23:03:50Z" + "generated_at": "2025-09-26T12:33:39Z" } diff --git a/changes/20250925204435.feature b/changes/20250925204435.feature new file mode 100644 index 0000000000..5a106dd3c5 --- /dev/null +++ b/changes/20250925204435.feature @@ -0,0 +1 @@ +:gear: `[config]` Add tests for service configurations with embedded structs tagged with `mapstructure:",squash"` diff --git a/utils/config/fixtures/env-test.env b/utils/config/fixtures/env-test.env new file mode 100644 index 0000000000..eee71001a1 --- /dev/null +++ b/utils/config/fixtures/env-test.env @@ -0,0 +1,15 @@ +# 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) +WITH_NO_TAG_TESTBASECFG_EMBEDDED1="embedded 1" +WITH_NO_TAG_TESTBASECFG_EMBEDDED2="embedded 2" +WITH_NO_TAG_NON_EMBEDDED1="non-embedded 1" +WITH_NO_TAG_NON_EMBEDDED2="non-embedded 2" +# These env vars will map to a struct the embeds another struct with a mapstructure tag set +WITH_TAG_EMBEDDED_STRUCT_EMBEDDED1="embedded 1" +WITH_TAG_EMBEDDED_STRUCT_EMBEDDED2="embedded 2" +WITH_TAG_NON_EMBEDDED1="non-embedded 1" +WITH_TAG_NON_EMBEDDED2="non-embedded 2" +# These env vars will map to a struct the embeds another struct with the ",squash" mapstructure tag set +WITH_SQUASH_TAG_EMBEDDED1="embedded 1" +WITH_SQUASH_TAG_EMBEDDED2="embedded 2" +WITH_SQUASH_TAG_NON_EMBEDDED1="non-embedded 1" +WITH_SQUASH_TAG_NON_EMBEDDED2="non-embedded 2" \ No newline at end of file diff --git a/utils/config/fixtures/flat-config-test.json b/utils/config/fixtures/flat-config-test.json new file mode 100644 index 0000000000..dd0b9f82d5 --- /dev/null +++ b/utils/config/fixtures/flat-config-test.json @@ -0,0 +1,5 @@ +{ + "embedded1": "embedded 1", + "embedded2": "embedded 2", + "non_embedded1": "non-embedded 1" +} \ No newline at end of file diff --git a/utils/config/fixtures/nested-config-test.json b/utils/config/fixtures/nested-config-test.json new file mode 100644 index 0000000000..d46efa0ec5 --- /dev/null +++ b/utils/config/fixtures/nested-config-test.json @@ -0,0 +1,11 @@ +{ + "testbasecfg": { + "embedded1": "embedded 1", + "embedded2": "embedded 2" + }, + "embedded_struct": { + "embedded1": "embedded 1", + "embedded2": "embedded 2" + }, + "non_embedded1": "non-embedded 1" +} \ No newline at end of file diff --git a/utils/config/service_configuration_test.go b/utils/config/service_configuration_test.go index b0566cb6f5..5df642d2d8 100644 --- a/utils/config/service_configuration_test.go +++ b/utils/config/service_configuration_test.go @@ -8,6 +8,7 @@ import ( "context" "errors" "fmt" + "io/fs" "math/rand" "os" "path/filepath" @@ -16,6 +17,7 @@ import ( "github.com/go-faker/faker/v4" validation "github.com/go-ozzo/ozzo-validation/v4" + "github.com/joho/godotenv" "github.com/spf13/pflag" "github.com/spf13/viper" "github.com/stretchr/testify/assert" @@ -776,3 +778,225 @@ func TestServiceConfigurationLoadFromEnvironment(t *testing.T) { assert.True(t, configTest.TestConfig.Flag) assert.False(t, configTest.TestConfig2.Flag) } + +type TestBaseCfg struct { + Embedded1 string `mapstructure:"embedded1"` + Embedded2 string `mapstructure:"embedded2"` +} + +func (cfg *TestBaseCfg) Validate() error { + return validation.ValidateStruct(cfg, + validation.Field(&cfg.Embedded1, validation.Required), + validation.Field(&cfg.Embedded2, validation.Required), + ) +} + +type TestCfgWithEmbeddedCfg struct { + TestBaseCfg + NonEmbedded1 string `mapstructure:"non_embedded1"` + NonEmbedded2 string `mapstructure:"non_embedded2"` +} + +func (cfg *TestCfgWithEmbeddedCfg) Validate() error { + // Validate Embedded Structs + err := ValidateEmbedded(cfg) + if err != nil { + return err + } + + return validation.ValidateStruct(cfg, + validation.Field(&cfg.NonEmbedded1, validation.Required), + ) +} + +type TestCfgWithEmbeddedCfgWithTag struct { + TestBaseCfg `mapstructure:"embedded_struct"` + NonEmbedded1 string `mapstructure:"non_embedded1"` +} + +func (cfg *TestCfgWithEmbeddedCfgWithTag) Validate() error { + // Validate Embedded Structs + err := ValidateEmbedded(cfg) + if err != nil { + return err + } + + return validation.ValidateStruct(cfg, + validation.Field(&cfg.NonEmbedded1, validation.Required), + ) +} + +type TestCfgWithEmbeddedCfgWithSquashTag struct { + TestBaseCfg `mapstructure:",squash"` + NonEmbedded1 string `mapstructure:"non_embedded1"` +} + +func (cfg *TestCfgWithEmbeddedCfgWithSquashTag) Validate() error { + // Validate Embedded Structs + err := ValidateEmbedded(cfg) + if err != nil { + return err + } + + return validation.ValidateStruct(cfg, + validation.Field(&cfg.NonEmbedded1, validation.Required), + ) +} + +// Config values loaded from file should be correctly mapped onto a struct that embeds another struct with no mapstructure tag set +func TestEmbeddedServiceConfigurationWithNoTagLoadFromFile(t *testing.T) { + os.Clearenv() + session := viper.New() + testEmbedded := TestCfgWithEmbeddedCfg{} + err := LoadFromEnvironment(session, "", &testEmbedded, + &TestCfgWithEmbeddedCfg{}, filepath.Join(".", "fixtures", "nested-config-test.json")) + require.NoError(t, err) + assert.NotEmpty(t, testEmbedded.NonEmbedded1) + assert.Equal(t, "non-embedded 1", testEmbedded.NonEmbedded1) + assert.Empty(t, testEmbedded.NonEmbedded2) + assert.NotEmpty(t, testEmbedded.Embedded1) + assert.Equal(t, "embedded 1", testEmbedded.Embedded1) + assert.NotEmpty(t, testEmbedded.Embedded2) + assert.Equal(t, "embedded 2", testEmbedded.Embedded2) +} + +// Nested config values loaded from file should be correctly mapped onto a struct that embeds another struct with a mapstructure tag set +func TestEmbeddedServiceConfigurationWithTagLoadFromFile(t *testing.T) { + os.Clearenv() + session := viper.New() + testEmbeddedWithTag := TestCfgWithEmbeddedCfgWithTag{} + err := LoadFromEnvironment(session, "", &testEmbeddedWithTag, + &TestCfgWithEmbeddedCfgWithTag{}, filepath.Join(".", "fixtures", "nested-config-test.json")) + require.NoError(t, err) + assert.NotEmpty(t, testEmbeddedWithTag.NonEmbedded1) + assert.Equal(t, "non-embedded 1", testEmbeddedWithTag.NonEmbedded1) + assert.NotEmpty(t, testEmbeddedWithTag.Embedded1) + assert.Equal(t, "embedded 1", testEmbeddedWithTag.Embedded1) + assert.NotEmpty(t, testEmbeddedWithTag.Embedded2) + assert.Equal(t, "embedded 2", testEmbeddedWithTag.Embedded2) +} + +// Flat config values loaded from file should be correctly mapped onto a struct that embeds another struct with the ",squash" mapstructure tag set +func TestEmbeddedServiceConfigurationWithSquashTagLoadFromFile(t *testing.T) { + os.Clearenv() + session := viper.New() + testEmbeddedWithSquashTag := TestCfgWithEmbeddedCfgWithSquashTag{} + err := LoadFromEnvironment(session, "", &testEmbeddedWithSquashTag, + &TestCfgWithEmbeddedCfgWithSquashTag{}, filepath.Join(".", "fixtures", "flat-config-test.json")) + require.NoError(t, err) + assert.NotEmpty(t, testEmbeddedWithSquashTag.NonEmbedded1) + assert.Equal(t, "non-embedded 1", testEmbeddedWithSquashTag.NonEmbedded1) + assert.NotEmpty(t, testEmbeddedWithSquashTag.Embedded1) + assert.Equal(t, "embedded 1", testEmbeddedWithSquashTag.Embedded1) + assert.NotEmpty(t, testEmbeddedWithSquashTag.Embedded2) + assert.Equal(t, "embedded 2", testEmbeddedWithSquashTag.Embedded2) +} + +// Nested config values loaded from file should not be correctly mapped onto a struct that embeds another struct with the ",squash" mapstructure tag set +func TestEmbeddedServiceConfigurationWithSquashTagLoadFromNestedFile(t *testing.T) { + os.Clearenv() + session := viper.New() + testEmbeddedWithSquashTag := TestCfgWithEmbeddedCfgWithSquashTag{} + err := LoadFromEnvironment(session, "", &testEmbeddedWithSquashTag, + &TestCfgWithEmbeddedCfgWithSquashTag{}, filepath.Join(".", "fixtures", "nested-config-test.json")) + require.Error(t, err) + assert.NotEmpty(t, testEmbeddedWithSquashTag.NonEmbedded1) + assert.Equal(t, "non-embedded 1", testEmbeddedWithSquashTag.NonEmbedded1) + assert.Empty(t, testEmbeddedWithSquashTag.Embedded1) + assert.NotEqual(t, "embedded 1", testEmbeddedWithSquashTag.Embedded1) + assert.NotEqual(t, "embedded 2", testEmbeddedWithSquashTag.Embedded2) +} + +// Nested config values loaded from file should be correctly mapped onto a struct that embeds another struct and should maintain any defaults not overwritten +func TestEmbeddedServiceConfigurationLoadFromFileWithDefaults(t *testing.T) { + os.Clearenv() + session := viper.New() + testEmbedded := TestCfgWithEmbeddedCfg{} + defaults := &TestCfgWithEmbeddedCfg{ + TestBaseCfg: TestBaseCfg{ + Embedded1: "a", + Embedded2: "b", + }, + NonEmbedded1: "c", + NonEmbedded2: "d", + } + err := LoadFromEnvironment(session, "", &testEmbedded, + defaults, filepath.Join(".", "fixtures", "nested-config-test.json")) + require.NoError(t, err) + assert.NotEmpty(t, testEmbedded.NonEmbedded1) + assert.Equal(t, "non-embedded 1", testEmbedded.NonEmbedded1) + assert.NotEmpty(t, testEmbedded.NonEmbedded2) + assert.Equal(t, "d", testEmbedded.NonEmbedded2) + assert.NotEmpty(t, testEmbedded.Embedded1) + assert.Equal(t, "embedded 1", testEmbedded.Embedded1) + assert.NotEmpty(t, testEmbedded.Embedded2) + assert.Equal(t, "embedded 2", testEmbedded.Embedded2) +} + +// Config values loaded from env vars should be correctly mapped onto a struct that embeds another struct with no mapstructure tag set +func TestEmbeddedServiceConfigurationWithNoTagLoadFromEnvironment(t *testing.T) { + os.Clearenv() + session := viper.New() + testEmbedded := TestCfgWithEmbeddedCfg{} + err := loadEnvIntoEnvironment(t, filepath.Join(".", "fixtures", "env-test.env")) + require.NoError(t, err) + + err = LoadFromEnvironment(session, "WITH_NO_TAG", &testEmbedded, + &TestCfgWithEmbeddedCfg{}, "") + require.NoError(t, err) + assert.NotEmpty(t, testEmbedded.NonEmbedded1) + assert.Equal(t, "non-embedded 1", testEmbedded.NonEmbedded1) + assert.NotEmpty(t, testEmbedded.Embedded1) + assert.Equal(t, "embedded 1", testEmbedded.Embedded1) + assert.NotEmpty(t, testEmbedded.Embedded2) + assert.Equal(t, "embedded 2", testEmbedded.Embedded2) +} + +// Config values loaded from env vars should be correctly mapped onto a struct that embeds another struct with a mapstructure tag set +func TestEmbeddedServiceConfigurationWithTagLoadFromEnvironment(t *testing.T) { + os.Clearenv() + session := viper.New() + testEmbeddedWithTag := TestCfgWithEmbeddedCfgWithTag{} + err := loadEnvIntoEnvironment(t, filepath.Join(".", "fixtures", "env-test.env")) + require.NoError(t, err) + + err = LoadFromEnvironment(session, "WITH_TAG", &testEmbeddedWithTag, + &TestCfgWithEmbeddedCfgWithTag{}, "") + require.NoError(t, err) + assert.NotEmpty(t, testEmbeddedWithTag.NonEmbedded1) + assert.Equal(t, "non-embedded 1", testEmbeddedWithTag.NonEmbedded1) + assert.NotEmpty(t, testEmbeddedWithTag.Embedded1) + assert.Equal(t, "embedded 1", testEmbeddedWithTag.Embedded1) + assert.NotEmpty(t, testEmbeddedWithTag.Embedded2) + assert.Equal(t, "embedded 2", testEmbeddedWithTag.Embedded2) +} + +// Flat config values loaded from env vars should be correctly mapped onto a struct that embeds another struct with the ",squash" mapstructure tag set +func TestEmbeddedServiceConfigurationWithSquashTagLoadFromEnvironment(t *testing.T) { + os.Clearenv() + session := viper.New() + testEmbeddedWithSquashTag := TestCfgWithEmbeddedCfgWithSquashTag{} + err := loadEnvIntoEnvironment(t, filepath.Join(".", "fixtures", "env-test.env")) + require.NoError(t, err) + + err = LoadFromEnvironment(session, "WITH_SQUASH_TAG", &testEmbeddedWithSquashTag, + &TestCfgWithEmbeddedCfgWithSquashTag{}, "") + require.NoError(t, err) + assert.NotEmpty(t, testEmbeddedWithSquashTag.NonEmbedded1) + assert.Equal(t, "non-embedded 1", testEmbeddedWithSquashTag.NonEmbedded1) + assert.NotEmpty(t, testEmbeddedWithSquashTag.Embedded1) + assert.Equal(t, "embedded 1", testEmbeddedWithSquashTag.Embedded1) + assert.NotEmpty(t, testEmbeddedWithSquashTag.Embedded2) + assert.Equal(t, "embedded 2", testEmbeddedWithSquashTag.Embedded2) +} + +func loadEnvIntoEnvironment(t *testing.T, envPath string) (err error) { + t.Helper() + _, err = fs.Stat(os.DirFS("."), envPath) + require.NoError(t, err) + + err = godotenv.Load(envPath) + require.NoError(t, err) + + return +} diff --git a/utils/serialization/maps/map_test.go b/utils/serialization/maps/map_test.go index 9389dee138..a63c83ab2c 100644 --- a/utils/serialization/maps/map_test.go +++ b/utils/serialization/maps/map_test.go @@ -100,12 +100,38 @@ type TestStruct2WithTime struct { Time time.Time `mapstructure:"some_time"` Duration time.Duration `mapstructure:"some_duration"` } + type TestStruct3WithTime struct { Time time.Time `mapstructure:"some_time"` Duration time.Duration `mapstructure:"some_duration"` Struct TestStruct2WithTime } +type TestStruct4 struct { + Field1 string `mapstructure:"field_1"` + Field2 string `mapstructure:"field_2"` +} + +type TestStruct5WithEmbeddedStruct struct { + TestStruct4 + Field3 string `mapstructure:"field_3"` +} + +type TestStruct5WithEmbeddedStructAndSquashTag struct { + TestStruct4 `mapstructure:",squash"` + Field3 string `mapstructure:"field_3"` +} + +type TestStruct6WithEmbeddedStruct struct { + TestStruct5WithEmbeddedStruct + Field4 string `mapstructure:"field_4"` +} + +type TestStruct6WithEmbeddedStructAndSquashTag struct { + TestStruct5WithEmbeddedStructAndSquashTag `mapstructure:",squash"` + Field4 string `mapstructure:"field_4"` +} + func TestToMap(t *testing.T) { t.Run("generic", func(t *testing.T) { testStruct := TestStruct1{} @@ -160,5 +186,56 @@ func TestToMap(t *testing.T) { errortest.AssertError(t, FromMapToPointer[any](testMap, nil), commonerrors.ErrInvalid, commonerrors.ErrUndefined) errortest.AssertError(t, FromMapToPointer[*TestStruct3WithTime](testMap, nil), commonerrors.ErrInvalid, commonerrors.ErrUndefined) }) + t.Run("embedded struct mapping with `mapstructure:\",squash\"`", func(t *testing.T) { + testStruct := TestStruct6WithEmbeddedStructAndSquashTag{} + require.NoError(t, faker.FakeData(&testStruct)) + + m := make(map[string]string) + m["field_1"] = faker.Word() + m["field_2"] = faker.Word() + m["field_3"] = faker.Word() + m["field_4"] = faker.Word() + + // Should correctly set embedded struct fields from map m, even on nested embedded structs + err := FromMap[TestStruct6WithEmbeddedStructAndSquashTag](m, &testStruct) + require.NoError(t, err) + assert.Equal(t, testStruct.Field1, m["field_1"]) + assert.Equal(t, testStruct.Field2, m["field_2"]) + assert.Equal(t, testStruct.Field3, m["field_3"]) + assert.Equal(t, testStruct.Field4, m["field_4"]) + + structMap, err := ToMap[TestStruct6WithEmbeddedStructAndSquashTag](&testStruct) + require.NoError(t, err) + assert.Equal(t, m, structMap) + + newStruct := TestStruct6WithEmbeddedStructAndSquashTag{} + require.NoError(t, FromMap[TestStruct6WithEmbeddedStructAndSquashTag](structMap, &newStruct)) + assert.Equal(t, testStruct, newStruct) + }) + t.Run("embedded struct mapping without `mapstructure:\",squash\"`", func(t *testing.T) { + testStruct := TestStruct6WithEmbeddedStruct{} + require.NoError(t, faker.FakeData(&testStruct)) + m := make(map[string]string) + m["field_1"] = faker.Word() + m["field_2"] = faker.Word() + m["field_3"] = faker.Word() + m["field_4"] = faker.Word() + + // Should only set the parent struct fields from map m and ignore any embedded struct fields + err := FromMap[TestStruct6WithEmbeddedStruct](m, &testStruct) + require.NoError(t, err) + assert.NotEqual(t, testStruct.Field1, m["field_1"]) + assert.NotEqual(t, testStruct.Field2, m["field_2"]) + assert.NotEqual(t, testStruct.Field3, m["field_3"]) + assert.Equal(t, testStruct.Field4, m["field_4"]) + + structMap, err := ToMap[TestStruct6WithEmbeddedStruct](&testStruct) + require.NoError(t, err) + assert.NotEqual(t, m, structMap) + + newStruct := TestStruct6WithEmbeddedStruct{} + require.NoError(t, FromMap[TestStruct6WithEmbeddedStruct](structMap, &newStruct)) + assert.Equal(t, testStruct, newStruct) + }) }