Files
job-search/internal/config/section_test.go
T

304 lines
8.2 KiB
Go

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"` //nolint:unused // checks unexported fields are ignored
}
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)
}
if got := m["dur"]; got != "1m30s" {
t.Errorf("m[dur] = %#v; want the duration as a string", 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{Dur: 90 * time.Second})
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)
}
}
if got := m["p.dur"]; got != "1m30s" {
t.Errorf("m[p.dur] = %#v; want the duration as a string", got)
}
}
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", "1h30m0s"}, // durations are given out as strings
{"inner.name", "nested", "nested"},
// values of the field's type are used as-is
{"int", 5, 5},
{"on", true, true},
{"dur", 3 * time.Second, "3s"},
{"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)
}
}