Unit tests added for internal/config
This commit is contained in:
1 parent
09644d0691
commit
69620e5f7d
4 files changed
+631
No files matched your search
@@ -0,0 +1,169 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"log/slog"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// initDir makes a temp working directory holding a .env file (if env is not
|
||||
// nil) and a config.yaml (if config is not empty), and returns its path.
|
||||
func initDir(t *testing.T, env []string, config string) string {
|
||||
t.Helper()
|
||||
resetViper(t)
|
||||
dir := t.TempDir()
|
||||
t.Chdir(dir)
|
||||
if env != nil {
|
||||
// godotenv sets variables without cleaning up after the test.
|
||||
for _, kv := range env {
|
||||
key, _, _ := strings.Cut(kv, "=")
|
||||
t.Cleanup(func() { os.Unsetenv(key) })
|
||||
}
|
||||
writeFile(t, filepath.Join(dir, ".env"), strings.Join(env, "\n")+"\n")
|
||||
}
|
||||
if config != "" {
|
||||
writeFile(t, filepath.Join(dir, "config.yaml"), config)
|
||||
}
|
||||
return dir
|
||||
}
|
||||
|
||||
func writeFile(t *testing.T, path, contents string) {
|
||||
t.Helper()
|
||||
if err := os.WriteFile(path, []byte(contents), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
const testConfigYAML = `logging:
|
||||
level: info
|
||||
sources:
|
||||
fts_mock:
|
||||
enabled: true
|
||||
interval: 30s
|
||||
`
|
||||
|
||||
func TestInitLoadsEnvAndConfigFile(t *testing.T) {
|
||||
initDir(t, []string{"JOBSEARCH_TEST_ENV=from-dotenv"}, testConfigYAML)
|
||||
captureLogs(t, slog.LevelError)
|
||||
|
||||
var conf JobSearchConfig
|
||||
if err := Init("", &conf); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got := os.Getenv("JOBSEARCH_TEST_ENV"); got != "from-dotenv" {
|
||||
t.Errorf("JOBSEARCH_TEST_ENV = %q; want it loaded from .env", got)
|
||||
}
|
||||
if conf.Logging.Level != "info" {
|
||||
t.Errorf("Logging.Level = %q; want info", conf.Logging.Level)
|
||||
}
|
||||
if !conf.Sources.FTSMock.Enabled || conf.Sources.FTSMock.Interval != 30*time.Second {
|
||||
t.Errorf("FTSMock = %+v; want enabled every 30s", conf.Sources.FTSMock)
|
||||
}
|
||||
// Keys missing from the file keep their defaults.
|
||||
if conf.Sources.FindATender.Interval != 10*time.Minute {
|
||||
t.Errorf("FindATender.Interval = %v; want default 10m", conf.Sources.FindATender.Interval)
|
||||
}
|
||||
}
|
||||
|
||||
func TestInitExplicitConfigFile(t *testing.T) {
|
||||
dir := initDir(t, []string{}, "logging:\n level: info\n")
|
||||
other := filepath.Join(dir, "other.yaml")
|
||||
writeFile(t, other, "logging:\n level: error\n")
|
||||
captureLogs(t, slog.LevelError)
|
||||
|
||||
var conf JobSearchConfig
|
||||
if err := Init(other, &conf); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if conf.Logging.Level != "error" {
|
||||
t.Errorf("Logging.Level = %q; want error from %s", conf.Logging.Level, other)
|
||||
}
|
||||
}
|
||||
|
||||
func TestInitNoConfigFile(t *testing.T) {
|
||||
initDir(t, []string{}, "")
|
||||
logs := captureLogs(t, slog.LevelWarn)
|
||||
|
||||
var conf JobSearchConfig
|
||||
if err := Init("", &conf); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !strings.Contains(logs.String(), "No config file found") {
|
||||
t.Errorf("no warning logged: %s", logs)
|
||||
}
|
||||
if conf.Logging.Level != "warn" {
|
||||
t.Errorf("Logging.Level = %q; want default warn", conf.Logging.Level)
|
||||
}
|
||||
}
|
||||
|
||||
// The -debug and -quiet levels set before Init win over the config file.
|
||||
func TestInitKeepsCommandLineLevel(t *testing.T) {
|
||||
for _, level := range []string{"debug", "error"} {
|
||||
t.Run(level, func(t *testing.T) {
|
||||
initDir(t, []string{}, "logging:\n level: info\n")
|
||||
logs := captureLogs(t, slog.LevelDebug)
|
||||
|
||||
conf := JobSearchConfig{Logging: JobSearchConfigLogging{Level: level}}
|
||||
if err := Init("", &conf); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if conf.Logging.Level != level {
|
||||
t.Errorf("Logging.Level = %q; want %s kept", conf.Logging.Level, level)
|
||||
}
|
||||
// At debug, the environment and loaded config are logged.
|
||||
gotDebug := strings.Contains(logs.String(), "Loaded config:")
|
||||
if gotDebug != (level == "debug") {
|
||||
t.Errorf("debug output logged = %v at level %s: %s", gotDebug, level, logs)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestInitErrors(t *testing.T) {
|
||||
t.Run("no .env file", func(t *testing.T) {
|
||||
initDir(t, nil, testConfigYAML)
|
||||
var conf JobSearchConfig
|
||||
if err := Init("", &conf); err == nil || !strings.Contains(err.Error(), ".env") {
|
||||
t.Fatalf("err = %v; want an invalid .env file error", err)
|
||||
}
|
||||
})
|
||||
t.Run("invalid config file", func(t *testing.T) {
|
||||
initDir(t, []string{}, "logging: [unclosed\n")
|
||||
captureLogs(t, slog.LevelError)
|
||||
var conf JobSearchConfig
|
||||
if err := Init("", &conf); err == nil {
|
||||
t.Fatal("expected an error for invalid YAML")
|
||||
}
|
||||
})
|
||||
t.Run("missing explicit config file", func(t *testing.T) {
|
||||
dir := initDir(t, []string{}, "")
|
||||
captureLogs(t, slog.LevelError)
|
||||
var conf JobSearchConfig
|
||||
if err := Init(filepath.Join(dir, "missing.yaml"), &conf); err == nil {
|
||||
t.Fatal("expected an error for a missing config file")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Environment variables override the config file, with nested keys joined
|
||||
// by underscores.
|
||||
func TestInitEnvOverride(t *testing.T) {
|
||||
initDir(t, []string{}, testConfigYAML)
|
||||
captureLogs(t, slog.LevelError)
|
||||
t.Setenv("JOBSEARCH_LOGGING_LEVEL", "error")
|
||||
t.Setenv("JOBSEARCH_SOURCES_FTS_MOCK_INTERVAL", "45s")
|
||||
|
||||
var conf JobSearchConfig
|
||||
if err := Init("", &conf); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if conf.Logging.Level != "error" {
|
||||
t.Errorf("Logging.Level = %q; want error from the environment", conf.Logging.Level)
|
||||
}
|
||||
if conf.Sources.FTSMock.Interval != 45*time.Second {
|
||||
t.Errorf("FTSMock.Interval = %v; want 45s from the environment", conf.Sources.FTSMock.Interval)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,163 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
// useConfigFile points Viper at a new YAML config file holding contents.
|
||||
func useConfigFile(t *testing.T, contents string) string {
|
||||
t.Helper()
|
||||
resetViper(t)
|
||||
path := filepath.Join(t.TempDir(), "config.yaml")
|
||||
if err := os.WriteFile(path, []byte(contents), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
viper.SetConfigFile(path)
|
||||
if err := viper.ReadInConfig(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return path
|
||||
}
|
||||
|
||||
// readConfigFile reads path with a fresh Viper.
|
||||
func readConfigFile(t *testing.T, path string) *viper.Viper {
|
||||
t.Helper()
|
||||
v := viper.New()
|
||||
v.SetConfigFile(path)
|
||||
if err := v.ReadInConfig(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
func TestStoreValue(t *testing.T) {
|
||||
tests := []struct {
|
||||
key, value string
|
||||
want any
|
||||
kind reflect.Kind
|
||||
}{
|
||||
{"logging.level", "debug", "debug", reflect.String},
|
||||
{"sources.sleeper.enabled", "true", true, reflect.Bool},
|
||||
{"sources.fts_mock.api_root", "http://mock", "http://mock", reflect.String},
|
||||
{"sources.fts_mock.interval", "30s", "30s", reflect.String},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.key, func(t *testing.T) {
|
||||
path := useConfigFile(t, "logging:\n level: warn\n")
|
||||
conf := defaultConfig(t)
|
||||
|
||||
got, kind, err := StoreValue(&conf, tt.key, tt.value)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got != tt.want || kind != tt.kind {
|
||||
t.Errorf("got %#v <%s>; want %#v <%s>", got, kind, tt.want, tt.kind)
|
||||
}
|
||||
if v := readConfigFile(t, path).Get(tt.key); v != tt.want {
|
||||
t.Errorf("file has %s = %#v; want %#v", tt.key, v, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestStoreValueKeepsOtherKeys(t *testing.T) {
|
||||
path := useConfigFile(t, "logging:\n level: warn\n file: /tmp/js.log\n")
|
||||
conf := defaultConfig(t)
|
||||
if _, _, err := StoreValue(&conf, "logging.level", "error"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
v := readConfigFile(t, path)
|
||||
if got := v.GetString("logging.file"); got != "/tmp/js.log" {
|
||||
t.Errorf("logging.file = %q; want it kept", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestStoreValueErrors(t *testing.T) {
|
||||
tests := []struct {
|
||||
name, key, value, wantErr string
|
||||
}{
|
||||
{"unknown key", "nope", "x", ""},
|
||||
{"section key", "sources", "x", ""},
|
||||
{"bad bool", "sources.sleeper.enabled", "maybe", "expected <bool>"},
|
||||
{"invalid log level", "logging.level", "shouty", "invalid log level"},
|
||||
{"bad duration", "sources.fts_mock.interval", "eventually", "duration"},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
path := useConfigFile(t, "logging:\n level: warn\n")
|
||||
before, _ := os.ReadFile(path)
|
||||
conf := defaultConfig(t)
|
||||
|
||||
_, _, err := StoreValue(&conf, tt.key, tt.value)
|
||||
if err == nil {
|
||||
t.Fatal("expected an error")
|
||||
}
|
||||
if !strings.Contains(err.Error(), tt.wantErr) {
|
||||
t.Errorf("err = %v; want it to mention %q", err, tt.wantErr)
|
||||
}
|
||||
if after, _ := os.ReadFile(path); string(after) != string(before) {
|
||||
t.Errorf("config file changed:\n%s", after)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestStoreValueNoConfigFile(t *testing.T) {
|
||||
resetViper(t)
|
||||
conf := defaultConfig(t)
|
||||
if _, _, err := StoreValue(&conf, "logging.level", "debug"); err == nil {
|
||||
t.Fatal("expected an error with no config file to write")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPurgeValue(t *testing.T) {
|
||||
path := useConfigFile(t, "logging:\n level: debug\n file: /tmp/js.log\n")
|
||||
conf := defaultConfig(t)
|
||||
|
||||
if err := PurgeValue(&conf, "logging.file"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
v := readConfigFile(t, path)
|
||||
if v.IsSet("logging.file") {
|
||||
t.Errorf("logging.file still set: %v", v.Get("logging.file"))
|
||||
}
|
||||
if got := v.GetString("logging.level"); got != "debug" {
|
||||
t.Errorf("logging.level = %q; want it kept", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPurgeValueErrors(t *testing.T) {
|
||||
path := useConfigFile(t, "logging:\n level: debug\n")
|
||||
before, _ := os.ReadFile(path)
|
||||
conf := defaultConfig(t)
|
||||
|
||||
if err := PurgeValue(&conf, "nope"); err == nil {
|
||||
t.Error("expected an error for an unknown key")
|
||||
}
|
||||
if after, _ := os.ReadFile(path); string(after) != string(before) {
|
||||
t.Errorf("config file changed:\n%s", after)
|
||||
}
|
||||
|
||||
resetViper(t)
|
||||
if err := PurgeValue(&conf, "logging.level"); err == nil {
|
||||
t.Error("expected an error with no config file to write")
|
||||
}
|
||||
}
|
||||
|
||||
func TestStoreValueUpdatesConf(t *testing.T) {
|
||||
useConfigFile(t, "logging:\n level: warn\n")
|
||||
conf := defaultConfig(t)
|
||||
if _, _, err := StoreValue(&conf, "sources.fts_mock.interval", "30s"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if conf.Sources.FTSMock.Interval != 30*time.Second {
|
||||
t.Errorf("FTSMock.Interval = %v; want 30s", conf.Sources.FTSMock.Interval)
|
||||
}
|
||||
}
|
||||
@@ -142,6 +142,8 @@ func mapOfValue(section reflect.Value) map[string]any {
|
||||
fv := section.FieldByIndex(f.index)
|
||||
if f.section {
|
||||
m[f.key] = mapOfValue(fv)
|
||||
} else if f.typ == durationType {
|
||||
m[f.key] = fv.Interface().(time.Duration).String()
|
||||
} else {
|
||||
m[f.key] = fv.Interface()
|
||||
}
|
||||
|
||||
@@ -0,0 +1,297 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Test sections exercising every field shape section.go supports.
|
||||
type testInner struct {
|
||||
Name string `cfg:"name" default:"inner"`
|
||||
}
|
||||
|
||||
type testEmbedded struct {
|
||||
On bool `cfg:"on" default:"true"`
|
||||
}
|
||||
|
||||
type testSection struct {
|
||||
testEmbedded
|
||||
Str string `cfg:"str" default:"hello"`
|
||||
Int int `cfg:"int" default:"-42"`
|
||||
Int8 int8 `cfg:"int8"`
|
||||
Uint uint `cfg:"uint" default:"7"`
|
||||
Uint16 uint16 `cfg:"uint16"`
|
||||
Float float64 `cfg:"float" default:"1.5"`
|
||||
Float32 float32 `cfg:"float32"`
|
||||
Dur time.Duration `cfg:"dur" default:"90s"`
|
||||
When time.Time `cfg:"when"`
|
||||
Slice []string `cfg:"slice"`
|
||||
Inner testInner `cfg:"inner"`
|
||||
NoTag string
|
||||
Skipped string `cfg:"-"`
|
||||
Empty string `cfg:""`
|
||||
unexport string `cfg:"unexported"`
|
||||
}
|
||||
|
||||
type testValidated struct {
|
||||
Value string `cfg:"value"`
|
||||
}
|
||||
|
||||
var errRejected = errors.New("rejected")
|
||||
|
||||
func (c *testValidated) validate(key string, value any) error {
|
||||
if key == "value" && value.(string) == "bad" {
|
||||
return errRejected
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestKeysOf(t *testing.T) {
|
||||
got := keysOf(&testSection{})
|
||||
want := []string{"on", "str", "int", "int8", "uint", "uint16", "float",
|
||||
"float32", "dur", "when", "slice", "inner"}
|
||||
if !reflect.DeepEqual(got, want) {
|
||||
t.Errorf("keysOf = %v; want %v", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFieldsOfSkipsUntagged(t *testing.T) {
|
||||
for _, f := range fieldsOf(reflect.TypeOf(testSection{})) {
|
||||
switch f.key {
|
||||
case "", "-", "unexported":
|
||||
t.Errorf("fieldsOf included field with key %q", f.key)
|
||||
}
|
||||
if f.key == "when" && f.section {
|
||||
t.Error("time.Time field treated as a section")
|
||||
}
|
||||
if f.key == "inner" && !f.section {
|
||||
t.Error("struct field not treated as a section")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestMapOf(t *testing.T) {
|
||||
var s testSection
|
||||
if err := applyDefaults(&s); err != nil {
|
||||
t.Fatalf("applyDefaults: %v", err)
|
||||
}
|
||||
m := mapOf(s)
|
||||
if got := m["str"]; got != "hello" {
|
||||
t.Errorf("m[str] = %#v", got)
|
||||
}
|
||||
if got := m["on"]; got != true {
|
||||
t.Errorf("m[on] (promoted) = %#v", got)
|
||||
}
|
||||
inner, ok := m["inner"].(map[string]any)
|
||||
if !ok {
|
||||
t.Fatalf("m[inner] = %#v; want nested map", m["inner"])
|
||||
}
|
||||
if inner["name"] != "inner" {
|
||||
t.Errorf("m[inner][name] = %#v", inner["name"])
|
||||
}
|
||||
if _, ok := m["inner.name"]; ok {
|
||||
t.Error("mapOf produced a flattened key")
|
||||
}
|
||||
// Pointer and value give the same result.
|
||||
if !reflect.DeepEqual(m, mapOf(&s)) {
|
||||
t.Error("mapOf(&s) != mapOf(s)")
|
||||
}
|
||||
}
|
||||
|
||||
func TestFlattenOfPrefix(t *testing.T) {
|
||||
m := flattenOf("p", testSection{})
|
||||
for _, k := range []string{"p.str", "p.on", "p.inner.name"} {
|
||||
if _, ok := m[k]; !ok {
|
||||
t.Errorf("flattenOf missing %q in %v", k, m)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestStructValuePanics(t *testing.T) {
|
||||
defer func() {
|
||||
if recover() == nil {
|
||||
t.Error("structValue(int) did not panic")
|
||||
}
|
||||
}()
|
||||
structValue(42)
|
||||
}
|
||||
|
||||
func TestApplyDefaults(t *testing.T) {
|
||||
var s testSection
|
||||
if err := applyDefaults(&s); err != nil {
|
||||
t.Fatalf("applyDefaults: %v", err)
|
||||
}
|
||||
want := testSection{
|
||||
testEmbedded: testEmbedded{On: true},
|
||||
Str: "hello",
|
||||
Int: -42,
|
||||
Uint: 7,
|
||||
Float: 1.5,
|
||||
Dur: 90 * time.Second,
|
||||
Inner: testInner{Name: "inner"},
|
||||
}
|
||||
if !reflect.DeepEqual(s, want) {
|
||||
t.Errorf("applyDefaults = %+v; want %+v", s, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestApplyDefaultsInvalid(t *testing.T) {
|
||||
var bad struct {
|
||||
N int `cfg:"n" default:"nope"`
|
||||
}
|
||||
err := applyDefaults(&bad)
|
||||
if err == nil || !strings.Contains(err.Error(), "invalid default for n") {
|
||||
t.Errorf("applyDefaults = %v; want invalid default error", err)
|
||||
}
|
||||
|
||||
var badNested struct {
|
||||
Sub struct {
|
||||
D time.Duration `cfg:"d" default:"forever"`
|
||||
} `cfg:"sub"`
|
||||
}
|
||||
err = applyDefaults(&badNested)
|
||||
if err == nil || !strings.Contains(err.Error(), "sub.d") {
|
||||
t.Errorf("applyDefaults nested = %v; want error naming sub.d", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTypeOf(t *testing.T) {
|
||||
tests := []struct {
|
||||
key string
|
||||
want reflect.Kind
|
||||
}{
|
||||
{"str", reflect.String},
|
||||
{"int", reflect.Int},
|
||||
{"uint", reflect.Uint},
|
||||
{"float", reflect.Float64},
|
||||
{"on", reflect.Bool},
|
||||
{"dur", reflect.String}, // durations are read as strings
|
||||
{"when", reflect.Struct},
|
||||
{"slice", reflect.Slice},
|
||||
{"inner.name", reflect.String},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
got, err := typeOf(testSection{}, tt.key)
|
||||
if err != nil || got != tt.want {
|
||||
t.Errorf("typeOf(%q) = %v, %v; want %v", tt.key, got, err, tt.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnknownKeys(t *testing.T) {
|
||||
keys := []string{
|
||||
"missing",
|
||||
"inner", // section without sub-key
|
||||
"str.sub", // value with a sub-key
|
||||
"inner.missing", // unknown nested key
|
||||
"NoTag", // untagged field
|
||||
"unexported",
|
||||
"",
|
||||
}
|
||||
var s testSection
|
||||
for _, key := range keys {
|
||||
if k, err := typeOf(s, key); !errors.Is(err, errUnknownKey) || k != reflect.Invalid {
|
||||
t.Errorf("typeOf(%q) = %v, %v; want Invalid, errUnknownKey", key, k, err)
|
||||
}
|
||||
if v, err := valueOf(s, key); !errors.Is(err, errUnknownKey) || v != nil {
|
||||
t.Errorf("valueOf(%q) = %v, %v; want nil, errUnknownKey", key, v, err)
|
||||
}
|
||||
if err := setValue(&s, key, "x"); !errors.Is(err, errUnknownKey) {
|
||||
t.Errorf("setValue(%q) = %v; want errUnknownKey", key, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetValue(t *testing.T) {
|
||||
tests := []struct {
|
||||
key string
|
||||
value any
|
||||
want any
|
||||
}{
|
||||
// strings are parsed
|
||||
{"str", "world", "world"},
|
||||
{"on", "false", false},
|
||||
{"int", "123", 123},
|
||||
{"int8", "-128", int8(-128)},
|
||||
{"uint", "9", uint(9)},
|
||||
{"uint16", "65535", uint16(65535)},
|
||||
{"float", "2.25", 2.25},
|
||||
{"float32", "0.5", float32(0.5)},
|
||||
{"dur", "1h30m", 90 * time.Minute},
|
||||
{"inner.name", "nested", "nested"},
|
||||
// values of the field's type are used as-is
|
||||
{"int", 5, 5},
|
||||
{"on", true, true},
|
||||
{"dur", 3 * time.Second, 3 * time.Second},
|
||||
{"slice", []string{"a", "b"}, []string{"a", "b"}},
|
||||
{"when", time.Unix(0, 0).UTC(), time.Unix(0, 0).UTC()},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
var s testSection
|
||||
if err := setValue(&s, tt.key, tt.value); err != nil {
|
||||
t.Errorf("setValue(%q, %#v): %v", tt.key, tt.value, err)
|
||||
continue
|
||||
}
|
||||
got, err := valueOf(s, tt.key)
|
||||
if err != nil || !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("after setValue(%q, %#v): valueOf = %#v, %v; want %#v",
|
||||
tt.key, tt.value, got, err, tt.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetValueErrors(t *testing.T) {
|
||||
tests := []struct {
|
||||
key string
|
||||
value any
|
||||
want string
|
||||
}{
|
||||
{"int", "abc", "unable to convert"},
|
||||
{"int8", "200", "unable to convert"}, // out of range
|
||||
{"uint", "-1", "unable to convert"},
|
||||
{"float", "x", "unable to convert"},
|
||||
{"on", "maybe", "unable to convert"},
|
||||
{"dur", "soon", "unable to convert to duration"},
|
||||
{"slice", "a,b", "unsupported type string"},
|
||||
{"when", "2024-01-01", "unsupported type string"},
|
||||
{"int", 1.5, "unsupported type float64"},
|
||||
{"int", int64(1), "unsupported type int64"},
|
||||
{"str", nil, "unsupported type <nil>"},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
var s testSection
|
||||
before := s
|
||||
err := setValue(&s, tt.key, tt.value)
|
||||
if err == nil || !strings.Contains(err.Error(), tt.want) {
|
||||
t.Errorf("setValue(%q, %#v) = %v; want error containing %q",
|
||||
tt.key, tt.value, err, tt.want)
|
||||
}
|
||||
if !reflect.DeepEqual(s, before) {
|
||||
t.Errorf("setValue(%q, %#v) modified the section on error", tt.key, tt.value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetValueValidator(t *testing.T) {
|
||||
var s testValidated
|
||||
if err := setValue(&s, "value", "good"); err != nil || s.Value != "good" {
|
||||
t.Errorf("setValue(good) = %v, Value = %q", err, s.Value)
|
||||
}
|
||||
if err := setValue(&s, "value", "bad"); !errors.Is(err, errRejected) {
|
||||
t.Errorf("setValue(bad) = %v; want errRejected", err)
|
||||
}
|
||||
if s.Value != "good" {
|
||||
t.Errorf("rejected value was stored: %q", s.Value)
|
||||
}
|
||||
|
||||
// The validator of the section holding the field is used for nested keys.
|
||||
var outer struct {
|
||||
V testValidated `cfg:"v"`
|
||||
}
|
||||
if err := setValue(&outer, "v.value", "bad"); !errors.Is(err, errRejected) {
|
||||
t.Errorf("nested setValue(bad) = %v; want errRejected", err)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user