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