package config_test import ( "errors" "os" "path/filepath" "strings" "testing" "github.com/spf13/viper" "sneak.berlin/go/simplexcalc/internal/config" ) // env builds a viper instance holding exactly the given keys, so a test // describes one environment without touching the process's. func env(kv map[string]string) *viper.Viper { v := viper.New() for k, val := range kv { v.Set(k, val) } return v } // TestAbsentValuesTakeDefaults pins the other half of the iron rule: a // value that is not set does get the default. Without this, a bug that // rejected everything would pass every test below. func TestAbsentValuesTakeDefaults(t *testing.T) { t.Parallel() for name, kv := range map[string]map[string]string{ "unset": nil, "whitespace only": { config.EnvDataDir: " ", config.EnvDebug: " ", config.EnvPort: " ", config.EnvAPITokenFile: "\t", }, } { t.Run(name, func(t *testing.T) { t.Parallel() c, err := config.Load(env(kv)) if err != nil { t.Fatalf("absent values must be valid, got: %v", err) } if c.DataDir != config.DefaultDataDir { t.Errorf("DataDir = %q, want %q", c.DataDir, config.DefaultDataDir) } if c.Debug { t.Error("Debug must default off") } if c.Port != config.DefaultPort { t.Errorf("Port = %d, want %d", c.Port, config.DefaultPort) } if c.APIToken != "" { t.Error("APIToken must default to none") } }) } } // TestSetButUnparseableAborts is the central contract of this package: // a value an operator plausibly types, and that does not parse, fails // startup rather than being replaced by the default. func TestSetButUnparseableAborts(t *testing.T) { t.Parallel() for _, raw := range []string{"yes", "on", "enabled", "2"} { t.Run(raw, func(t *testing.T) { t.Parallel() c, err := config.Load(env(map[string]string{config.EnvDebug: raw})) if err == nil { t.Fatalf("wanted a startup failure, got a Config: %+v", c) } if !errors.Is(err, config.ErrInvalidConfig) { t.Errorf("error does not wrap ErrInvalidConfig: %v", err) } if c != nil { t.Error("a failed load must return no Config at all") } }) } } // TestValidValuesAreUsed proves the parsers accept what they document. func TestValidValuesAreUsed(t *testing.T) { t.Parallel() c, err := config.Load(env(map[string]string{ config.EnvDataDir: "/var/lib/example", config.EnvDebug: "true", })) if err != nil { t.Fatalf("valid environment was rejected: %v", err) } if c.DataDir != "/var/lib/example" { t.Errorf("DataDir = %q, want /var/lib/example", c.DataDir) } if !c.Debug { t.Error("Debug = false, want true") } } // TestPort: PORT is a whole number from 1 to 65535, and anything else // aborts. func TestPort(t *testing.T) { t.Parallel() for raw, want := range map[string]int{"1": 1, "8081": 8081, "65535": 65535} { c, err := config.Load(env(map[string]string{config.EnvPort: raw})) if err != nil { t.Errorf("PORT=%q was rejected: %v", raw, err) continue } if c.Port != want { t.Errorf("PORT=%q: Port = %d, want %d", raw, c.Port, want) } } for _, raw := range []string{"0", "65536", "-1", "80.5", "8080x", "http"} { _, err := config.Load(env(map[string]string{config.EnvPort: raw})) if !errors.Is(err, config.ErrInvalidConfig) { t.Errorf("PORT=%q: error = %v, want ErrInvalidConfig", raw, err) } } } // TestAPITokenFile: the credential is the file's content without the // whitespace around it. A file that cannot be read, or holds too short // a credential, aborts, and the error never shows what the file holds. func TestAPITokenFile(t *testing.T) { t.Parallel() dir := t.TempDir() token := strings.Repeat("k", config.MinAPITokenLength) short := strings.Repeat("s", config.MinAPITokenLength-1) for name, content := range map[string]string{ "good": " " + token + "\n", "short": "\n" + short + " \n", } { err := os.WriteFile(filepath.Join(dir, name), []byte(content), 0o600) if err != nil { t.Fatal(err) } } load := func(name string) (*config.Config, error) { return config.Load(env(map[string]string{ config.EnvAPITokenFile: filepath.Join(dir, name), })) } c, err := load("good") if err != nil { t.Fatalf("a good file was rejected: %v", err) } if c.APIToken != token { t.Errorf("APIToken = %q, want %q", c.APIToken, token) } _, err = load("missing") if !errors.Is(err, config.ErrInvalidConfig) { t.Errorf("a missing file: error = %v, want ErrInvalidConfig", err) } _, err = load("short") if !errors.Is(err, config.ErrInvalidConfig) { t.Fatalf("a short credential: error = %v, want ErrInvalidConfig", err) } if strings.Contains(err.Error(), short) { t.Errorf("the error shows the file's content: %v", err) } }