conf/conf_test.go
juancwu c4ebd80669 Add reflective struct-tag config loader
Implements conf.Load to populate tagged structs from a chain of Sources
(env, .env, YAML/JSON/TOML, custom). Supports default values, slice
separators, nested structs, pointer fields, and a Validator hook.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 20:35:51 +00:00

134 lines
2.8 KiB
Go

package conf
import (
"errors"
"strings"
"testing"
"time"
)
type allTypes struct {
S string `env:"S" default:"hi"`
B bool `env:"B" default:"true"`
I int `env:"I" default:"7"`
I64 int64 `env:"I64" default:"100"`
U uint32 `env:"U" default:"3"`
F float64 `env:"F" default:"1.5"`
D time.Duration `env:"D" default:"5s"`
BS []byte `env:"BS" default:"abc"`
List []string `env:"LIST" default:"a,b,c"`
Skip string `env:"-"`
None string
}
func TestLoadDefaults(t *testing.T) {
var c allTypes
if err := Load(&c); err != nil {
t.Fatal(err)
}
if c.S != "hi" || !c.B || c.I != 7 || c.I64 != 100 || c.U != 3 || c.F != 1.5 ||
c.D != 5*time.Second || string(c.BS) != "abc" ||
strings.Join(c.List, ",") != "a,b,c" {
t.Fatalf("defaults not applied: %+v", c)
}
}
func TestPrecedence(t *testing.T) {
high := MapSource(map[string]string{"S": "high"})
low := MapSource(map[string]string{"S": "low", "I": "42"})
var c allTypes
if err := Load(&c, high, low); err != nil {
t.Fatal(err)
}
if c.S != "high" {
t.Fatalf("S=%q want high", c.S)
}
if c.I != 42 {
t.Fatalf("I=%d want 42", c.I)
}
}
func TestPointerField(t *testing.T) {
type p struct {
X *int `env:"X"`
Y *int `env:"Y"`
}
var c p
if err := Load(&c, MapSource(map[string]string{"X": "9"})); err != nil {
t.Fatal(err)
}
if c.X == nil || *c.X != 9 {
t.Fatalf("X=%v", c.X)
}
if c.Y != nil {
t.Fatalf("Y should be nil, got %v", *c.Y)
}
}
func TestNestedStruct(t *testing.T) {
type Inner struct {
A string `env:"A" default:"x"`
}
type Outer struct {
Inner Inner
B string `env:"B" default:"y"`
}
var o Outer
if err := Load(&o); err != nil {
t.Fatal(err)
}
if o.Inner.A != "x" || o.B != "y" {
t.Fatalf("%+v", o)
}
}
type withValidate struct {
URL string `env:"URL"`
}
func (w *withValidate) Validate() error {
if w.URL == "" {
return errors.New("URL required")
}
return nil
}
func TestValidatorFires(t *testing.T) {
var w withValidate
err := Load(&w)
if err == nil || !strings.Contains(err.Error(), "URL required") {
t.Fatalf("want URL required error, got %v", err)
}
}
func TestParseError(t *testing.T) {
type bad struct {
N int `env:"N"`
}
var b bad
err := Load(&b, MapSource(map[string]string{"N": "notanumber"}))
if err == nil || !strings.Contains(err.Error(), "N") {
t.Fatalf("want field N error, got %v", err)
}
}
func TestRejectsNonPointer(t *testing.T) {
var c allTypes
if err := Load(c); err == nil {
t.Fatal("expected error for non-pointer")
}
}
func TestSepOverride(t *testing.T) {
type s struct {
L []string `env:"L" sep:"|"`
}
var x s
if err := Load(&x, MapSource(map[string]string{"L": "a|b|c"})); err != nil {
t.Fatal(err)
}
if strings.Join(x.L, ",") != "a,b,c" {
t.Fatalf("got %v", x.L)
}
}