125 lines
4.2 KiB
Go
125 lines
4.2 KiB
Go
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)
|
|
}
|
|
}
|
|
}
|