package config import ( "errors" "os" "path/filepath" "reflect" "strings" "testing" "time" ) type projectNested struct { ReadTimeout time.Duration `json:"readTimeout"` Names []string `json:"names"` } type projectConfig struct { Nested *projectNested `json:"nested"` Enabled bool `json:"enabled"` Count int `json:"count" envDefault:"12"` Password string `json:"-" env:"ETAX_PASSWORD,secret,required,empty=reject"` Label string `json:"label" env:",empty=ignore"` } func (c projectConfig) Validate() error { if c.Count < 0 { return errors.New("count must be nonnegative") } return nil } func TestProjectConfigurationOrderAndValues(t *testing.T) { f := filepath.Join(t.TempDir(), "config.yaml") if err := os.WriteFile(f, []byte("count: 3\nenabled: true\nnested:\n readTimeout: 2s\n names: [file]\nlabel: keep\n"), 0600); err != nil { t.Fatal(err) } env := map[string]string{"ETAX_PASSWORD": " password ", "APP_ENABLED": "false", "APP_COUNT": "0", "APP_NESTED_READ_TIMEOUT": "7s", "APP_NESTED_NAMES": "[]", "APP_LABEL": ""} options := EnvOptions{Prefix: "APP_", Lookup: func(k string) (string, bool) { v, ok := env[k]; return v, ok }} cfg, err := LoadProject(f, projectConfig{}, options) if err != nil { t.Fatal(err) } if cfg.Enabled || cfg.Count != 0 || cfg.Password != " password " || cfg.Nested.ReadTimeout != 7*time.Second || cfg.Nested.Names == nil || len(cfg.Nested.Names) != 0 || cfg.Label != "keep" { t.Fatal("overlay mismatch") } delete(env, "APP_COUNT") cfg, err = LoadProject("", projectConfig{}, options) if err != nil || cfg.Count != 12 { t.Fatal(cfg.Count, err) } env["APP_COUNT"] = "-1" if _, err = LoadProject(f, projectConfig{}, options); err == nil { t.Fatal("project Validate not called") } } func TestProjectOverlayIsTransactionalAndRedacted(t *testing.T) { base := projectConfig{Nested: &projectNested{ReadTimeout: time.Second, Names: []string{"old"}}, Password: "original"} env := map[string]string{"APP_NESTED_READ_TIMEOUT": "2s", "APP_NESTED_NAMES": "[\"new\"]", "APP_COUNT": "secret-value"} _, err := OverlayEnv(base, EnvOptions{Prefix: "APP_", Lookup: func(k string) (string, bool) { v, ok := env[k]; return v, ok }}) if err == nil || strings.Contains(err.Error(), "secret-value") { t.Fatal(err) } if base.Nested.ReadTimeout != time.Second || !reflect.DeepEqual(base.Nested.Names, []string{"old"}) { t.Fatal("default pointer modified") } delete(env, "APP_COUNT") env["ETAX_PASSWORD"] = "" if _, err = OverlayEnv(base, EnvOptions{Prefix: "APP_", Lookup: func(k string) (string, bool) { v, ok := env[k]; return v, ok }}); err == nil { t.Fatal("empty secret accepted") } type required struct { Token string `env:"TOKEN,required"` } if _, err = OverlayEnv(required{}, EnvOptions{Lookup: func(string) (string, bool) { return "", false }}); err == nil { t.Fatal("required absent") } type duplicate struct { A string `env:"SAME"` B string `env:"SAME"` } if _, err = OverlayEnv(duplicate{}, EnvOptions{}); err == nil { t.Fatal("duplicate env binding accepted") } } func TestEnvironmentCollectionAndEmptySemantics(t *testing.T) { type sample struct { Values []time.Duration Flag bool Text string Optional *int } cases := []struct { raw string valid bool }{{`["1s","2s"]`, true}, {`["invalid-secret"]`, false}} for _, tc := range cases { cfg, err := OverlayEnv(sample{}, EnvOptions{Lookup: func(k string) (string, bool) { if k == "VALUES" { return tc.raw, true } return "", false }}) if (err == nil) != tc.valid { t.Fatal(err) } if tc.valid && !reflect.DeepEqual(cfg.Values, []time.Duration{time.Second, 2 * time.Second}) { t.Fatal(cfg) } if err != nil && strings.Contains(err.Error(), "secret") { t.Fatal(err) } } for _, mode := range []string{"allow", "ignore", "reject"} { cfg, err := OverlayEnv(sample{Text: "old"}, EnvOptions{Empty: mode, Lookup: func(k string) (string, bool) { return "", k == "TEXT" }}) if mode == "reject" { if err == nil { t.Fatal("empty not rejected") } } else if err != nil || (mode == "allow" && cfg.Text != "") || (mode == "ignore" && cfg.Text != "old") { t.Fatal(cfg, err) } } }