1478 lines
36 KiB
Go
1478 lines
36 KiB
Go
package env
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"net/http"
|
|
"net/url"
|
|
"os"
|
|
"path/filepath"
|
|
"reflect"
|
|
"runtime"
|
|
"strconv"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/matryer/is"
|
|
)
|
|
|
|
type unmarshaler struct {
|
|
time.Duration
|
|
}
|
|
|
|
// TextUnmarshaler implements encoding.TextUnmarshaler.
|
|
func (d *unmarshaler) UnmarshalText(data []byte) (err error) {
|
|
if len(data) != 0 {
|
|
d.Duration, err = time.ParseDuration(string(data))
|
|
} else {
|
|
d.Duration = 0
|
|
}
|
|
return err
|
|
}
|
|
|
|
// nolint: maligned
|
|
type Config struct {
|
|
String string `env:"STRING"`
|
|
StringPtr *string `env:"STRING"`
|
|
Strings []string `env:"STRINGS"`
|
|
StringPtrs []*string `env:"STRINGS"`
|
|
|
|
Bool bool `env:"BOOL"`
|
|
BoolPtr *bool `env:"BOOL"`
|
|
Bools []bool `env:"BOOLS"`
|
|
BoolPtrs []*bool `env:"BOOLS"`
|
|
|
|
Int int `env:"INT"`
|
|
IntPtr *int `env:"INT"`
|
|
Ints []int `env:"INTS"`
|
|
IntPtrs []*int `env:"INTS"`
|
|
|
|
Int8 int8 `env:"INT8"`
|
|
Int8Ptr *int8 `env:"INT8"`
|
|
Int8s []int8 `env:"INT8S"`
|
|
Int8Ptrs []*int8 `env:"INT8S"`
|
|
|
|
Int16 int16 `env:"INT16"`
|
|
Int16Ptr *int16 `env:"INT16"`
|
|
Int16s []int16 `env:"INT16S"`
|
|
Int16Ptrs []*int16 `env:"INT16S"`
|
|
|
|
Int32 int32 `env:"INT32"`
|
|
Int32Ptr *int32 `env:"INT32"`
|
|
Int32s []int32 `env:"INT32S"`
|
|
Int32Ptrs []*int32 `env:"INT32S"`
|
|
|
|
Int64 int64 `env:"INT64"`
|
|
Int64Ptr *int64 `env:"INT64"`
|
|
Int64s []int64 `env:"INT64S"`
|
|
Int64Ptrs []*int64 `env:"INT64S"`
|
|
|
|
Uint uint `env:"UINT"`
|
|
UintPtr *uint `env:"UINT"`
|
|
Uints []uint `env:"UINTS"`
|
|
UintPtrs []*uint `env:"UINTS"`
|
|
|
|
Uint8 uint8 `env:"UINT8"`
|
|
Uint8Ptr *uint8 `env:"UINT8"`
|
|
Uint8s []uint8 `env:"UINT8S"`
|
|
Uint8Ptrs []*uint8 `env:"UINT8S"`
|
|
|
|
Uint16 uint16 `env:"UINT16"`
|
|
Uint16Ptr *uint16 `env:"UINT16"`
|
|
Uint16s []uint16 `env:"UINT16S"`
|
|
Uint16Ptrs []*uint16 `env:"UINT16S"`
|
|
|
|
Uint32 uint32 `env:"UINT32"`
|
|
Uint32Ptr *uint32 `env:"UINT32"`
|
|
Uint32s []uint32 `env:"UINT32S"`
|
|
Uint32Ptrs []*uint32 `env:"UINT32S"`
|
|
|
|
Uint64 uint64 `env:"UINT64"`
|
|
Uint64Ptr *uint64 `env:"UINT64"`
|
|
Uint64s []uint64 `env:"UINT64S"`
|
|
Uint64Ptrs []*uint64 `env:"UINT64S"`
|
|
|
|
Float32 float32 `env:"FLOAT32"`
|
|
Float32Ptr *float32 `env:"FLOAT32"`
|
|
Float32s []float32 `env:"FLOAT32S"`
|
|
Float32Ptrs []*float32 `env:"FLOAT32S"`
|
|
|
|
Float64 float64 `env:"FLOAT64"`
|
|
Float64Ptr *float64 `env:"FLOAT64"`
|
|
Float64s []float64 `env:"FLOAT64S"`
|
|
Float64Ptrs []*float64 `env:"FLOAT64S"`
|
|
|
|
Duration time.Duration `env:"DURATION"`
|
|
Durations []time.Duration `env:"DURATIONS"`
|
|
DurationPtr *time.Duration `env:"DURATION"`
|
|
DurationPtrs []*time.Duration `env:"DURATIONS"`
|
|
|
|
Unmarshaler unmarshaler `env:"UNMARSHALER"`
|
|
UnmarshalerPtr *unmarshaler `env:"UNMARSHALER"`
|
|
Unmarshalers []unmarshaler `env:"UNMARSHALERS"`
|
|
UnmarshalerPtrs []*unmarshaler `env:"UNMARSHALERS"`
|
|
|
|
URL url.URL `env:"URL"`
|
|
URLPtr *url.URL `env:"URL"`
|
|
URLs []url.URL `env:"URLS"`
|
|
URLPtrs []*url.URL `env:"URLS"`
|
|
|
|
StringWithdefault string `env:"DATABASE_URL" envDefault:"postgres://localhost:5432/db"`
|
|
|
|
CustomSeparator []string `env:"SEPSTRINGS" envSeparator:":"`
|
|
|
|
NonDefined struct {
|
|
String string `env:"NONDEFINED_STR"`
|
|
}
|
|
|
|
NotAnEnv string
|
|
unexported string `env:"FOO"`
|
|
}
|
|
|
|
type ParentStruct struct {
|
|
InnerStruct *InnerStruct
|
|
unexported *InnerStruct
|
|
Ignored *http.Client
|
|
}
|
|
|
|
type InnerStruct struct {
|
|
Inner string `env:"innervar"`
|
|
Number uint `env:"innernum"`
|
|
}
|
|
|
|
type ForNestedStruct struct {
|
|
NestedStruct
|
|
}
|
|
|
|
type NestedStruct struct {
|
|
NestedVar string `env:"nestedvar"`
|
|
}
|
|
|
|
func TestParsesEnv(t *testing.T) {
|
|
is := is.New(t)
|
|
|
|
defer os.Clearenv()
|
|
|
|
tos := func(v interface{}) string {
|
|
return fmt.Sprintf("%v", v)
|
|
}
|
|
|
|
toss := func(v ...interface{}) string {
|
|
ss := []string{}
|
|
for _, s := range v {
|
|
ss = append(ss, tos(s))
|
|
}
|
|
return strings.Join(ss, ",")
|
|
}
|
|
|
|
str1 := "str1"
|
|
str2 := "str2"
|
|
os.Setenv("STRING", str1)
|
|
os.Setenv("STRINGS", toss(str1, str2))
|
|
|
|
bool1 := true
|
|
bool2 := false
|
|
os.Setenv("BOOL", tos(bool1))
|
|
os.Setenv("BOOLS", toss(bool1, bool2))
|
|
|
|
int1 := -1
|
|
int2 := 2
|
|
os.Setenv("INT", tos(int1))
|
|
os.Setenv("INTS", toss(int1, int2))
|
|
|
|
var int81 int8 = -2
|
|
var int82 int8 = 5
|
|
os.Setenv("INT8", tos(int81))
|
|
os.Setenv("INT8S", toss(int81, int82))
|
|
|
|
var int161 int16 = -24
|
|
var int162 int16 = 15
|
|
os.Setenv("INT16", tos(int161))
|
|
os.Setenv("INT16S", toss(int161, int162))
|
|
|
|
var int321 int32 = -14
|
|
var int322 int32 = 154
|
|
os.Setenv("INT32", tos(int321))
|
|
os.Setenv("INT32S", toss(int321, int322))
|
|
|
|
var int641 int64 = -12
|
|
var int642 int64 = 150
|
|
os.Setenv("INT64", tos(int641))
|
|
os.Setenv("INT64S", toss(int641, int642))
|
|
|
|
var uint1 uint = 1
|
|
var uint2 uint = 2
|
|
os.Setenv("UINT", tos(uint1))
|
|
os.Setenv("UINTS", toss(uint1, uint2))
|
|
|
|
var uint81 uint8 = 15
|
|
var uint82 uint8 = 51
|
|
os.Setenv("UINT8", tos(uint81))
|
|
os.Setenv("UINT8S", toss(uint81, uint82))
|
|
|
|
var uint161 uint16 = 532
|
|
var uint162 uint16 = 123
|
|
os.Setenv("UINT16", tos(uint161))
|
|
os.Setenv("UINT16S", toss(uint161, uint162))
|
|
|
|
var uint321 uint32 = 93
|
|
var uint322 uint32 = 14
|
|
os.Setenv("UINT32", tos(uint321))
|
|
os.Setenv("UINT32S", toss(uint321, uint322))
|
|
|
|
var uint641 uint64 = 5
|
|
var uint642 uint64 = 43
|
|
os.Setenv("UINT64", tos(uint641))
|
|
os.Setenv("UINT64S", toss(uint641, uint642))
|
|
|
|
var float321 float32 = 9.3
|
|
var float322 float32 = 1.1
|
|
os.Setenv("FLOAT32", tos(float321))
|
|
os.Setenv("FLOAT32S", toss(float321, float322))
|
|
|
|
float641 := 1.53
|
|
float642 := 0.5
|
|
os.Setenv("FLOAT64", tos(float641))
|
|
os.Setenv("FLOAT64S", toss(float641, float642))
|
|
|
|
duration1 := time.Second
|
|
duration2 := time.Second * 4
|
|
os.Setenv("DURATION", tos(duration1))
|
|
os.Setenv("DURATIONS", toss(duration1, duration2))
|
|
|
|
unmarshaler1 := unmarshaler{time.Minute}
|
|
unmarshaler2 := unmarshaler{time.Millisecond * 1232}
|
|
os.Setenv("UNMARSHALER", tos(unmarshaler1.Duration))
|
|
os.Setenv("UNMARSHALERS", toss(unmarshaler1.Duration, unmarshaler2.Duration))
|
|
|
|
url1 := "https://goreleaser.com"
|
|
url2 := "https://caarlos0.dev"
|
|
os.Setenv("URL", tos(url1))
|
|
os.Setenv("URLS", toss(url1, url2))
|
|
|
|
os.Setenv("SEPSTRINGS", strings.Join([]string{str1, str2}, ":"))
|
|
|
|
nonDefinedStr := "nonDefinedStr"
|
|
os.Setenv("NONDEFINED_STR", nonDefinedStr)
|
|
|
|
cfg := Config{}
|
|
is.NoErr(Parse(&cfg))
|
|
|
|
is.Equal(str1, cfg.String)
|
|
is.Equal(&str1, cfg.StringPtr)
|
|
is.Equal(str1, cfg.Strings[0])
|
|
is.Equal(str2, cfg.Strings[1])
|
|
is.Equal(&str1, cfg.StringPtrs[0])
|
|
is.Equal(&str2, cfg.StringPtrs[1])
|
|
|
|
is.Equal(bool1, cfg.Bool)
|
|
is.Equal(&bool1, cfg.BoolPtr)
|
|
is.Equal(bool1, cfg.Bools[0])
|
|
is.Equal(bool2, cfg.Bools[1])
|
|
is.Equal(&bool1, cfg.BoolPtrs[0])
|
|
is.Equal(&bool2, cfg.BoolPtrs[1])
|
|
|
|
is.Equal(int1, cfg.Int)
|
|
is.Equal(&int1, cfg.IntPtr)
|
|
is.Equal(int1, cfg.Ints[0])
|
|
is.Equal(int2, cfg.Ints[1])
|
|
is.Equal(&int1, cfg.IntPtrs[0])
|
|
is.Equal(&int2, cfg.IntPtrs[1])
|
|
|
|
is.Equal(int81, cfg.Int8)
|
|
is.Equal(&int81, cfg.Int8Ptr)
|
|
is.Equal(int81, cfg.Int8s[0])
|
|
is.Equal(int82, cfg.Int8s[1])
|
|
is.Equal(&int81, cfg.Int8Ptrs[0])
|
|
is.Equal(&int82, cfg.Int8Ptrs[1])
|
|
|
|
is.Equal(int161, cfg.Int16)
|
|
is.Equal(&int161, cfg.Int16Ptr)
|
|
is.Equal(int161, cfg.Int16s[0])
|
|
is.Equal(int162, cfg.Int16s[1])
|
|
is.Equal(&int161, cfg.Int16Ptrs[0])
|
|
is.Equal(&int162, cfg.Int16Ptrs[1])
|
|
|
|
is.Equal(int321, cfg.Int32)
|
|
is.Equal(&int321, cfg.Int32Ptr)
|
|
is.Equal(int321, cfg.Int32s[0])
|
|
is.Equal(int322, cfg.Int32s[1])
|
|
is.Equal(&int321, cfg.Int32Ptrs[0])
|
|
is.Equal(&int322, cfg.Int32Ptrs[1])
|
|
|
|
is.Equal(int641, cfg.Int64)
|
|
is.Equal(&int641, cfg.Int64Ptr)
|
|
is.Equal(int641, cfg.Int64s[0])
|
|
is.Equal(int642, cfg.Int64s[1])
|
|
is.Equal(&int641, cfg.Int64Ptrs[0])
|
|
is.Equal(&int642, cfg.Int64Ptrs[1])
|
|
|
|
is.Equal(uint1, cfg.Uint)
|
|
is.Equal(&uint1, cfg.UintPtr)
|
|
is.Equal(uint1, cfg.Uints[0])
|
|
is.Equal(uint2, cfg.Uints[1])
|
|
is.Equal(&uint1, cfg.UintPtrs[0])
|
|
is.Equal(&uint2, cfg.UintPtrs[1])
|
|
|
|
is.Equal(uint81, cfg.Uint8)
|
|
is.Equal(&uint81, cfg.Uint8Ptr)
|
|
is.Equal(uint81, cfg.Uint8s[0])
|
|
is.Equal(uint82, cfg.Uint8s[1])
|
|
is.Equal(&uint81, cfg.Uint8Ptrs[0])
|
|
is.Equal(&uint82, cfg.Uint8Ptrs[1])
|
|
|
|
is.Equal(uint161, cfg.Uint16)
|
|
is.Equal(&uint161, cfg.Uint16Ptr)
|
|
is.Equal(uint161, cfg.Uint16s[0])
|
|
is.Equal(uint162, cfg.Uint16s[1])
|
|
is.Equal(&uint161, cfg.Uint16Ptrs[0])
|
|
is.Equal(&uint162, cfg.Uint16Ptrs[1])
|
|
|
|
is.Equal(uint321, cfg.Uint32)
|
|
is.Equal(&uint321, cfg.Uint32Ptr)
|
|
is.Equal(uint321, cfg.Uint32s[0])
|
|
is.Equal(uint322, cfg.Uint32s[1])
|
|
is.Equal(&uint321, cfg.Uint32Ptrs[0])
|
|
is.Equal(&uint322, cfg.Uint32Ptrs[1])
|
|
|
|
is.Equal(uint641, cfg.Uint64)
|
|
is.Equal(&uint641, cfg.Uint64Ptr)
|
|
is.Equal(uint641, cfg.Uint64s[0])
|
|
is.Equal(uint642, cfg.Uint64s[1])
|
|
is.Equal(&uint641, cfg.Uint64Ptrs[0])
|
|
is.Equal(&uint642, cfg.Uint64Ptrs[1])
|
|
|
|
is.Equal(float321, cfg.Float32)
|
|
is.Equal(&float321, cfg.Float32Ptr)
|
|
is.Equal(float321, cfg.Float32s[0])
|
|
is.Equal(float322, cfg.Float32s[1])
|
|
is.Equal(&float321, cfg.Float32Ptrs[0])
|
|
is.Equal(&float322, cfg.Float32Ptrs[1])
|
|
|
|
is.Equal(float641, cfg.Float64)
|
|
is.Equal(&float641, cfg.Float64Ptr)
|
|
is.Equal(float641, cfg.Float64s[0])
|
|
is.Equal(float642, cfg.Float64s[1])
|
|
is.Equal(&float641, cfg.Float64Ptrs[0])
|
|
is.Equal(&float642, cfg.Float64Ptrs[1])
|
|
|
|
is.Equal(duration1, cfg.Duration)
|
|
is.Equal(&duration1, cfg.DurationPtr)
|
|
is.Equal(duration1, cfg.Durations[0])
|
|
is.Equal(duration2, cfg.Durations[1])
|
|
is.Equal(&duration1, cfg.DurationPtrs[0])
|
|
is.Equal(&duration2, cfg.DurationPtrs[1])
|
|
|
|
is.Equal(unmarshaler1, cfg.Unmarshaler)
|
|
is.Equal(&unmarshaler1, cfg.UnmarshalerPtr)
|
|
is.Equal(unmarshaler1, cfg.Unmarshalers[0])
|
|
is.Equal(unmarshaler2, cfg.Unmarshalers[1])
|
|
is.Equal(&unmarshaler1, cfg.UnmarshalerPtrs[0])
|
|
is.Equal(&unmarshaler2, cfg.UnmarshalerPtrs[1])
|
|
|
|
is.Equal(url1, cfg.URL.String())
|
|
is.Equal(url1, cfg.URLPtr.String())
|
|
is.Equal(url1, cfg.URLs[0].String())
|
|
is.Equal(url2, cfg.URLs[1].String())
|
|
is.Equal(url1, cfg.URLPtrs[0].String())
|
|
is.Equal(url2, cfg.URLPtrs[1].String())
|
|
|
|
is.Equal("postgres://localhost:5432/db", cfg.StringWithdefault)
|
|
is.Equal(nonDefinedStr, cfg.NonDefined.String)
|
|
|
|
is.Equal(str1, cfg.CustomSeparator[0])
|
|
is.Equal(str2, cfg.CustomSeparator[1])
|
|
|
|
is.Equal(cfg.NotAnEnv, "")
|
|
|
|
is.Equal(cfg.unexported, "")
|
|
}
|
|
|
|
func TestSetEnvAndTagOptsChain(t *testing.T) {
|
|
is := is.New(t)
|
|
|
|
defer os.Clearenv()
|
|
type config struct {
|
|
Key1 string `mytag:"KEY1,required"`
|
|
Key2 int `mytag:"KEY2,required"`
|
|
}
|
|
envs := map[string]string{
|
|
"KEY1": "VALUE1",
|
|
"KEY2": "3",
|
|
}
|
|
|
|
cfg := config{}
|
|
is.NoErr(Parse(&cfg, Options{TagName: "mytag"}, Options{Environment: envs}))
|
|
is.Equal("VALUE1", cfg.Key1)
|
|
is.Equal(3, cfg.Key2)
|
|
}
|
|
|
|
func TestJSONTag(t *testing.T) {
|
|
is := is.New(t)
|
|
|
|
defer os.Clearenv()
|
|
type config struct {
|
|
Key1 string `json:"KEY1"`
|
|
Key2 int `json:"KEY2"`
|
|
}
|
|
|
|
os.Setenv("KEY1", "VALUE7")
|
|
os.Setenv("KEY2", "5")
|
|
|
|
cfg := config{}
|
|
is.NoErr(Parse(&cfg, Options{TagName: "json"}))
|
|
is.Equal("VALUE7", cfg.Key1)
|
|
is.Equal(5, cfg.Key2)
|
|
}
|
|
|
|
func TestParsesEnvInner(t *testing.T) {
|
|
is := is.New(t)
|
|
|
|
os.Setenv("innervar", "someinnervalue")
|
|
os.Setenv("innernum", "8")
|
|
defer os.Clearenv()
|
|
cfg := ParentStruct{
|
|
InnerStruct: &InnerStruct{},
|
|
unexported: &InnerStruct{},
|
|
}
|
|
is.NoErr(Parse(&cfg))
|
|
is.Equal("someinnervalue", cfg.InnerStruct.Inner)
|
|
is.Equal(uint(8), cfg.InnerStruct.Number)
|
|
}
|
|
|
|
func TestParsesEnvInnerFails(t *testing.T) {
|
|
defer os.Clearenv()
|
|
type config struct {
|
|
Foo struct {
|
|
Number int `env:"NUMBER"`
|
|
}
|
|
}
|
|
os.Setenv("NUMBER", "not-a-number")
|
|
isErrorWithMessage(t, Parse(&config{}), `env: parse error on field "Number" of type "int": strconv.ParseInt: parsing "not-a-number": invalid syntax`)
|
|
}
|
|
|
|
func TestParsesEnvInnerNil(t *testing.T) {
|
|
is := is.New(t)
|
|
|
|
os.Setenv("innervar", "someinnervalue")
|
|
defer os.Clearenv()
|
|
cfg := ParentStruct{}
|
|
is.NoErr(Parse(&cfg))
|
|
}
|
|
|
|
func TestParsesEnvInnerInvalid(t *testing.T) {
|
|
os.Setenv("innernum", "-547")
|
|
defer os.Clearenv()
|
|
cfg := ParentStruct{
|
|
InnerStruct: &InnerStruct{},
|
|
}
|
|
isErrorWithMessage(t, Parse(&cfg), `env: parse error on field "Number" of type "uint": strconv.ParseUint: parsing "-547": invalid syntax`)
|
|
}
|
|
|
|
func TestParsesEnvNested(t *testing.T) {
|
|
is := is.New(t)
|
|
|
|
os.Setenv("nestedvar", "somenestedvalue")
|
|
defer os.Clearenv()
|
|
var cfg ForNestedStruct
|
|
is.NoErr(Parse(&cfg))
|
|
is.Equal("somenestedvalue", cfg.NestedVar)
|
|
}
|
|
|
|
func TestEmptyVars(t *testing.T) {
|
|
is := is.New(t)
|
|
|
|
os.Clearenv()
|
|
cfg := Config{}
|
|
is.NoErr(Parse(&cfg))
|
|
is.Equal("", cfg.String)
|
|
is.Equal(false, cfg.Bool)
|
|
is.Equal(0, cfg.Int)
|
|
is.Equal(uint(0), cfg.Uint)
|
|
is.Equal(uint64(0), cfg.Uint64)
|
|
is.Equal(int64(0), cfg.Int64)
|
|
is.Equal(0, len(cfg.Strings))
|
|
is.Equal(0, len(cfg.CustomSeparator))
|
|
is.Equal(0, len(cfg.Ints))
|
|
is.Equal(0, len(cfg.Bools))
|
|
}
|
|
|
|
func TestPassAnInvalidPtr(t *testing.T) {
|
|
var thisShouldBreak int
|
|
isErrorWithMessage(t, Parse(&thisShouldBreak), "env: expected a pointer to a Struct")
|
|
}
|
|
|
|
func TestPassReference(t *testing.T) {
|
|
cfg := Config{}
|
|
isErrorWithMessage(t, Parse(cfg), "env: expected a pointer to a Struct")
|
|
}
|
|
|
|
func TestInvalidBool(t *testing.T) {
|
|
os.Setenv("BOOL", "should-be-a-bool")
|
|
defer os.Clearenv()
|
|
isErrorWithMessage(t, Parse(&Config{}), `env: parse error on field "Bool" of type "bool": strconv.ParseBool: parsing "should-be-a-bool": invalid syntax`)
|
|
}
|
|
|
|
func TestInvalidInt(t *testing.T) {
|
|
os.Setenv("INT", "should-be-an-int")
|
|
defer os.Clearenv()
|
|
isErrorWithMessage(t, Parse(&Config{}), `env: parse error on field "Int" of type "int": strconv.ParseInt: parsing "should-be-an-int": invalid syntax`)
|
|
}
|
|
|
|
func TestInvalidUint(t *testing.T) {
|
|
os.Setenv("UINT", "-44")
|
|
defer os.Clearenv()
|
|
isErrorWithMessage(t, Parse(&Config{}), `env: parse error on field "Uint" of type "uint": strconv.ParseUint: parsing "-44": invalid syntax`)
|
|
}
|
|
|
|
func TestInvalidFloat32(t *testing.T) {
|
|
os.Setenv("FLOAT32", "AAA")
|
|
defer os.Clearenv()
|
|
|
|
isErrorWithMessage(t, Parse(&Config{}), `env: parse error on field "Float32" of type "float32": strconv.ParseFloat: parsing "AAA": invalid syntax`)
|
|
}
|
|
|
|
func TestInvalidFloat64(t *testing.T) {
|
|
os.Setenv("FLOAT64", "AAA")
|
|
defer os.Clearenv()
|
|
isErrorWithMessage(t, Parse(&Config{}), `env: parse error on field "Float64" of type "float64": strconv.ParseFloat: parsing "AAA": invalid syntax`)
|
|
}
|
|
|
|
func TestInvalidUint64(t *testing.T) {
|
|
os.Setenv("UINT64", "AAA")
|
|
defer os.Clearenv()
|
|
isErrorWithMessage(t, Parse(&Config{}), `env: parse error on field "Uint64" of type "uint64": strconv.ParseUint: parsing "AAA": invalid syntax`)
|
|
}
|
|
|
|
func TestInvalidInt64(t *testing.T) {
|
|
os.Setenv("INT64", "AAA")
|
|
defer os.Clearenv()
|
|
isErrorWithMessage(t, Parse(&Config{}), `env: parse error on field "Int64" of type "int64": strconv.ParseInt: parsing "AAA": invalid syntax`)
|
|
}
|
|
|
|
func TestInvalidInt64Slice(t *testing.T) {
|
|
os.Setenv("BADINTS", "A,2,3")
|
|
defer os.Clearenv()
|
|
type config struct {
|
|
BadFloats []int64 `env:"BADINTS"`
|
|
}
|
|
isErrorWithMessage(t, Parse(&config{}), `env: parse error on field "BadFloats" of type "[]int64": strconv.ParseInt: parsing "A": invalid syntax`)
|
|
}
|
|
|
|
func TestInvalidUInt64Slice(t *testing.T) {
|
|
os.Setenv("BADINTS", "A,2,3")
|
|
defer os.Clearenv()
|
|
type config struct {
|
|
BadFloats []uint64 `env:"BADINTS"`
|
|
}
|
|
isErrorWithMessage(t, Parse(&config{}), `env: parse error on field "BadFloats" of type "[]uint64": strconv.ParseUint: parsing "A": invalid syntax`)
|
|
}
|
|
|
|
func TestInvalidFloat32Slice(t *testing.T) {
|
|
os.Setenv("BADFLOATS", "A,2.0,3.0")
|
|
defer os.Clearenv()
|
|
type config struct {
|
|
BadFloats []float32 `env:"BADFLOATS"`
|
|
}
|
|
isErrorWithMessage(t, Parse(&config{}), `env: parse error on field "BadFloats" of type "[]float32": strconv.ParseFloat: parsing "A": invalid syntax`)
|
|
}
|
|
|
|
func TestInvalidFloat64Slice(t *testing.T) {
|
|
os.Setenv("BADFLOATS", "A,2.0,3.0")
|
|
defer os.Clearenv()
|
|
type config struct {
|
|
BadFloats []float64 `env:"BADFLOATS"`
|
|
}
|
|
isErrorWithMessage(t, Parse(&config{}), `env: parse error on field "BadFloats" of type "[]float64": strconv.ParseFloat: parsing "A": invalid syntax`)
|
|
}
|
|
|
|
func TestInvalidBoolsSlice(t *testing.T) {
|
|
os.Setenv("BADBOOLS", "t,f,TRUE,faaaalse")
|
|
defer os.Clearenv()
|
|
type config struct {
|
|
BadBools []bool `env:"BADBOOLS"`
|
|
}
|
|
isErrorWithMessage(t, Parse(&config{}), `env: parse error on field "BadBools" of type "[]bool": strconv.ParseBool: parsing "faaaalse": invalid syntax`)
|
|
}
|
|
|
|
func TestInvalidDuration(t *testing.T) {
|
|
os.Setenv("DURATION", "should-be-a-valid-duration")
|
|
defer os.Clearenv()
|
|
isErrorWithMessage(t, Parse(&Config{}), `env: parse error on field "Duration" of type "time.Duration": unable to parse duration: time: invalid duration "should-be-a-valid-duration"`)
|
|
}
|
|
|
|
func TestInvalidDurations(t *testing.T) {
|
|
os.Setenv("DURATIONS", "1s,contains-an-invalid-duration,3s")
|
|
defer os.Clearenv()
|
|
isErrorWithMessage(t, Parse(&Config{}), `env: parse error on field "Durations" of type "[]time.Duration": unable to parse duration: time: invalid duration "contains-an-invalid-duration"`)
|
|
}
|
|
|
|
func TestParseStructWithoutEnvTag(t *testing.T) {
|
|
is := is.New(t)
|
|
|
|
cfg := Config{}
|
|
is.NoErr(Parse(&cfg))
|
|
is.Equal(cfg.NotAnEnv, "")
|
|
}
|
|
|
|
func TestParseStructWithInvalidFieldKind(t *testing.T) {
|
|
type config struct {
|
|
WontWorkByte byte `env:"BLAH"`
|
|
}
|
|
os.Setenv("BLAH", "a")
|
|
isErrorWithMessage(t, Parse(&config{}), `env: parse error on field "WontWorkByte" of type "uint8": strconv.ParseUint: parsing "a": invalid syntax`)
|
|
}
|
|
|
|
func TestUnsupportedSliceType(t *testing.T) {
|
|
type config struct {
|
|
WontWork []map[int]int `env:"WONTWORK"`
|
|
}
|
|
|
|
os.Setenv("WONTWORK", "1,2,3")
|
|
defer os.Clearenv()
|
|
|
|
isErrorWithMessage(t, Parse(&config{}), `env: no parser found for field "WontWork" of type "[]map[int]int"`)
|
|
}
|
|
|
|
func TestBadSeparator(t *testing.T) {
|
|
type config struct {
|
|
WontWork []int `env:"WONTWORK" envSeparator:":"`
|
|
}
|
|
|
|
os.Setenv("WONTWORK", "1,2,3,4")
|
|
defer os.Clearenv()
|
|
|
|
isErrorWithMessage(t, Parse(&config{}), `env: parse error on field "WontWork" of type "[]int": strconv.ParseInt: parsing "1,2,3,4": invalid syntax`)
|
|
}
|
|
|
|
func TestNoErrorRequiredSet(t *testing.T) {
|
|
is := is.New(t)
|
|
|
|
type config struct {
|
|
IsRequired string `env:"IS_REQUIRED,required"`
|
|
}
|
|
|
|
cfg := &config{}
|
|
|
|
os.Setenv("IS_REQUIRED", "")
|
|
defer os.Clearenv()
|
|
is.NoErr(Parse(cfg))
|
|
is.Equal("", cfg.IsRequired)
|
|
}
|
|
|
|
func TestHook(t *testing.T) {
|
|
is := is.New(t)
|
|
|
|
type config struct {
|
|
Something string `env:"SOMETHING" envDefault:"important"`
|
|
Another string `env:"ANOTHER"`
|
|
}
|
|
|
|
cfg := &config{}
|
|
|
|
os.Setenv("ANOTHER", "1")
|
|
defer os.Clearenv()
|
|
|
|
type onSetArgs struct {
|
|
tag string
|
|
key interface{}
|
|
isDefault bool
|
|
}
|
|
|
|
var onSetCalled []onSetArgs
|
|
|
|
is.NoErr(Parse(cfg, Options{
|
|
OnSet: func(tag string, value interface{}, isDefault bool) {
|
|
onSetCalled = append(onSetCalled, onSetArgs{tag, value, isDefault})
|
|
},
|
|
}))
|
|
is.Equal("important", cfg.Something)
|
|
is.Equal("1", cfg.Another)
|
|
is.Equal(2, len(onSetCalled))
|
|
is.Equal(onSetArgs{"SOMETHING", "important", true}, onSetCalled[0])
|
|
is.Equal(onSetArgs{"ANOTHER", "1", false}, onSetCalled[1])
|
|
}
|
|
|
|
func TestErrorRequiredWithDefault(t *testing.T) {
|
|
is := is.New(t)
|
|
|
|
type config struct {
|
|
IsRequired string `env:"IS_REQUIRED,required" envDefault:"important"`
|
|
}
|
|
|
|
cfg := &config{}
|
|
|
|
os.Setenv("IS_REQUIRED", "")
|
|
defer os.Clearenv()
|
|
is.NoErr(Parse(cfg))
|
|
is.Equal("", cfg.IsRequired)
|
|
}
|
|
|
|
func TestErrorRequiredNotSet(t *testing.T) {
|
|
type config struct {
|
|
IsRequired string `env:"IS_REQUIRED,required"`
|
|
}
|
|
isErrorWithMessage(t, Parse(&config{}), `env: required environment variable "IS_REQUIRED" is not set`)
|
|
}
|
|
|
|
func TestNoErrorNotEmptySet(t *testing.T) {
|
|
is := is.New(t)
|
|
os.Setenv("IS_REQUIRED", "1")
|
|
defer os.Clearenv()
|
|
type config struct {
|
|
IsRequired string `env:"IS_REQUIRED,notEmpty"`
|
|
}
|
|
is.NoErr(Parse(&config{}))
|
|
}
|
|
|
|
func TestNoErrorRequiredAndNotEmptySet(t *testing.T) {
|
|
is := is.New(t)
|
|
os.Setenv("IS_REQUIRED", "1")
|
|
defer os.Clearenv()
|
|
type config struct {
|
|
IsRequired string `env:"IS_REQUIRED,required,notEmpty"`
|
|
}
|
|
is.NoErr(Parse(&config{}))
|
|
}
|
|
|
|
func TestErrorNotEmptySet(t *testing.T) {
|
|
os.Setenv("IS_REQUIRED", "")
|
|
defer os.Clearenv()
|
|
type config struct {
|
|
IsRequired string `env:"IS_REQUIRED,notEmpty"`
|
|
}
|
|
isErrorWithMessage(t, Parse(&config{}), `env: environment variable "IS_REQUIRED" should not be empty`)
|
|
}
|
|
|
|
func TestErrorRequiredAndNotEmptySet(t *testing.T) {
|
|
os.Setenv("IS_REQUIRED", "")
|
|
defer os.Clearenv()
|
|
type config struct {
|
|
IsRequired string `env:"IS_REQUIRED,notEmpty,required"`
|
|
}
|
|
isErrorWithMessage(t, Parse(&config{}), `env: environment variable "IS_REQUIRED" should not be empty`)
|
|
}
|
|
|
|
func TestErrorRequiredNotSetWithDefault(t *testing.T) {
|
|
is := is.New(t)
|
|
|
|
type config struct {
|
|
IsRequired string `env:"IS_REQUIRED,required" envDefault:"important"`
|
|
}
|
|
|
|
cfg := &config{}
|
|
|
|
is.NoErr(Parse(cfg))
|
|
is.Equal("important", cfg.IsRequired)
|
|
}
|
|
|
|
func TestParseExpandOption(t *testing.T) {
|
|
is := is.New(t)
|
|
|
|
type config struct {
|
|
Host string `env:"HOST" envDefault:"localhost"`
|
|
Port int `env:"PORT" envDefault:"3000" envExpand:"True"`
|
|
SecretKey string `env:"SECRET_KEY" envExpand:"True"`
|
|
ExpandKey string `env:"EXPAND_KEY"`
|
|
CompoundKey string `env:"HOST_PORT" envDefault:"${HOST}:${PORT}" envExpand:"True"`
|
|
Default string `env:"DEFAULT" envDefault:"def1" envExpand:"True"`
|
|
}
|
|
defer os.Clearenv()
|
|
|
|
os.Setenv("HOST", "localhost")
|
|
os.Setenv("PORT", "3000")
|
|
os.Setenv("EXPAND_KEY", "qwerty12345")
|
|
os.Setenv("SECRET_KEY", "${EXPAND_KEY}")
|
|
|
|
cfg := config{}
|
|
err := Parse(&cfg)
|
|
|
|
is.NoErr(err)
|
|
is.Equal("localhost", cfg.Host)
|
|
is.Equal(3000, cfg.Port)
|
|
is.Equal("qwerty12345", cfg.SecretKey)
|
|
is.Equal("qwerty12345", cfg.ExpandKey)
|
|
is.Equal("localhost:3000", cfg.CompoundKey)
|
|
is.Equal("def1", cfg.Default)
|
|
}
|
|
|
|
func TestParseUnsetRequireOptions(t *testing.T) {
|
|
is := is.New(t)
|
|
|
|
type config struct {
|
|
Password string `env:"PASSWORD,unset,required"`
|
|
}
|
|
defer os.Clearenv()
|
|
cfg := config{}
|
|
|
|
isErrorWithMessage(t, Parse(&cfg), `env: required environment variable "PASSWORD" is not set`)
|
|
os.Setenv("PASSWORD", "superSecret")
|
|
is.NoErr(Parse(&cfg))
|
|
|
|
is.Equal("superSecret", cfg.Password)
|
|
unset, exists := os.LookupEnv("PASSWORD")
|
|
is.Equal("", unset)
|
|
is.Equal(false, exists)
|
|
}
|
|
|
|
func TestCustomParser(t *testing.T) {
|
|
is := is.New(t)
|
|
|
|
type foo struct {
|
|
name string
|
|
}
|
|
|
|
type bar struct {
|
|
Name string `env:"OTHER"`
|
|
Foo *foo `env:"BLAH"`
|
|
}
|
|
|
|
type config struct {
|
|
Var foo `env:"VAR"`
|
|
Foo *foo `env:"BLAH"`
|
|
Other *bar
|
|
}
|
|
|
|
os.Setenv("VAR", "test")
|
|
defer os.Unsetenv("VAR")
|
|
os.Setenv("OTHER", "test2")
|
|
defer os.Unsetenv("OTHER")
|
|
os.Setenv("BLAH", "test3")
|
|
defer os.Unsetenv("BLAH")
|
|
|
|
cfg := &config{
|
|
Other: &bar{},
|
|
}
|
|
err := ParseWithFuncs(cfg, map[reflect.Type]ParserFunc{
|
|
reflect.TypeOf(foo{}): func(v string) (interface{}, error) {
|
|
return foo{name: v}, nil
|
|
},
|
|
})
|
|
|
|
is.NoErr(err)
|
|
is.Equal(cfg.Var.name, "test")
|
|
is.Equal(cfg.Foo.name, "test3")
|
|
is.Equal(cfg.Other.Name, "test2")
|
|
is.Equal(cfg.Other.Foo.name, "test3")
|
|
}
|
|
|
|
func TestParseWithFuncsNoPtr(t *testing.T) {
|
|
type foo struct{}
|
|
isErrorWithMessage(t, ParseWithFuncs(foo{}, nil), "env: expected a pointer to a Struct")
|
|
}
|
|
|
|
func TestParseWithFuncsInvalidType(t *testing.T) {
|
|
var c int
|
|
isErrorWithMessage(t, ParseWithFuncs(&c, nil), "env: expected a pointer to a Struct")
|
|
}
|
|
|
|
func TestCustomParserError(t *testing.T) {
|
|
type foo struct {
|
|
name string
|
|
}
|
|
|
|
customParserFunc := func(v string) (interface{}, error) {
|
|
return nil, errors.New("something broke")
|
|
}
|
|
|
|
t.Run("single", func(t *testing.T) {
|
|
is := is.New(t)
|
|
|
|
type config struct {
|
|
Var foo `env:"VAR"`
|
|
}
|
|
|
|
os.Setenv("VAR", "single")
|
|
cfg := &config{}
|
|
err := ParseWithFuncs(cfg, map[reflect.Type]ParserFunc{
|
|
reflect.TypeOf(foo{}): customParserFunc,
|
|
})
|
|
|
|
is.Equal(cfg.Var.name, "")
|
|
isErrorWithMessage(t, err, `env: parse error on field "Var" of type "env.foo": something broke`)
|
|
})
|
|
|
|
t.Run("slice", func(t *testing.T) {
|
|
is := is.New(t)
|
|
|
|
type config struct {
|
|
Var []foo `env:"VAR2"`
|
|
}
|
|
os.Setenv("VAR2", "slice,slace")
|
|
|
|
cfg := &config{}
|
|
err := ParseWithFuncs(cfg, map[reflect.Type]ParserFunc{
|
|
reflect.TypeOf(foo{}): customParserFunc,
|
|
})
|
|
|
|
is.Equal(cfg.Var, nil)
|
|
isErrorWithMessage(t, err, `env: parse error on field "Var" of type "[]env.foo": something broke`)
|
|
})
|
|
}
|
|
|
|
func TestCustomParserBasicType(t *testing.T) {
|
|
is := is.New(t)
|
|
|
|
type ConstT int32
|
|
|
|
type config struct {
|
|
Const ConstT `env:"CONST_"`
|
|
}
|
|
|
|
exp := ConstT(123)
|
|
os.Setenv("CONST_", fmt.Sprintf("%d", exp))
|
|
|
|
customParserFunc := func(v string) (interface{}, error) {
|
|
i, err := strconv.Atoi(v)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
r := ConstT(i)
|
|
return r, nil
|
|
}
|
|
|
|
cfg := &config{}
|
|
err := ParseWithFuncs(cfg, map[reflect.Type]ParserFunc{
|
|
reflect.TypeOf(ConstT(0)): customParserFunc,
|
|
})
|
|
|
|
is.NoErr(err)
|
|
is.Equal(exp, cfg.Const)
|
|
}
|
|
|
|
func TestCustomParserUint64Alias(t *testing.T) {
|
|
is := is.New(t)
|
|
|
|
type T uint64
|
|
|
|
var one T = 1
|
|
|
|
type config struct {
|
|
Val T `env:"" envDefault:"1x"`
|
|
}
|
|
|
|
parserCalled := false
|
|
|
|
tParser := func(value string) (interface{}, error) {
|
|
parserCalled = true
|
|
trimmed := strings.TrimSuffix(value, "x")
|
|
i, err := strconv.Atoi(trimmed)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return T(i), nil
|
|
}
|
|
|
|
cfg := config{}
|
|
|
|
err := ParseWithFuncs(&cfg, map[reflect.Type]ParserFunc{
|
|
reflect.TypeOf(one): tParser,
|
|
})
|
|
|
|
is.True(parserCalled) // tParser should have been called
|
|
is.NoErr(err)
|
|
is.Equal(T(1), cfg.Val)
|
|
}
|
|
|
|
func TestTypeCustomParserBasicInvalid(t *testing.T) {
|
|
is := is.New(t)
|
|
|
|
type ConstT int32
|
|
|
|
type config struct {
|
|
Const ConstT `env:"CONST_"`
|
|
}
|
|
|
|
os.Setenv("CONST_", "foobar")
|
|
|
|
customParserFunc := func(_ string) (interface{}, error) {
|
|
return nil, errors.New("random error")
|
|
}
|
|
|
|
cfg := &config{}
|
|
err := ParseWithFuncs(cfg, map[reflect.Type]ParserFunc{
|
|
reflect.TypeOf(ConstT(0)): customParserFunc,
|
|
})
|
|
|
|
is.Equal(cfg.Const, ConstT(0))
|
|
isErrorWithMessage(t, err, `env: parse error on field "Const" of type "env.ConstT": random error`)
|
|
}
|
|
|
|
func TestCustomParserNotCalledForNonAlias(t *testing.T) {
|
|
is := is.New(t)
|
|
|
|
type T uint64
|
|
type U uint64
|
|
|
|
type config struct {
|
|
Val uint64 `env:"" envDefault:"33"`
|
|
Other U `env:"OTHER" envDefault:"44"`
|
|
}
|
|
|
|
tParserCalled := false
|
|
|
|
tParser := func(value string) (interface{}, error) {
|
|
tParserCalled = true
|
|
return T(99), nil
|
|
}
|
|
|
|
cfg := config{}
|
|
|
|
err := ParseWithFuncs(&cfg, map[reflect.Type]ParserFunc{
|
|
reflect.TypeOf(T(0)): tParser,
|
|
})
|
|
|
|
is.True(!tParserCalled) // tParser should not have been called
|
|
is.NoErr(err)
|
|
is.Equal(uint64(33), cfg.Val)
|
|
is.Equal(U(44), cfg.Other)
|
|
}
|
|
|
|
func TestCustomParserBasicUnsupported(t *testing.T) {
|
|
is := is.New(t)
|
|
|
|
type ConstT struct {
|
|
A int
|
|
}
|
|
|
|
type config struct {
|
|
Const ConstT `env:"CONST_"`
|
|
}
|
|
|
|
os.Setenv("CONST_", "42")
|
|
|
|
cfg := &config{}
|
|
err := Parse(cfg)
|
|
|
|
is.Equal(cfg.Const, ConstT{0})
|
|
isErrorWithMessage(t, err, `env: no parser found for field "Const" of type "env.ConstT"`)
|
|
}
|
|
|
|
func TestUnsupportedStructType(t *testing.T) {
|
|
type config struct {
|
|
Foo http.Client `env:"FOO"`
|
|
}
|
|
os.Setenv("FOO", "foo")
|
|
defer os.Clearenv()
|
|
isErrorWithMessage(t, Parse(&config{}), `env: no parser found for field "Foo" of type "http.Client"`)
|
|
}
|
|
|
|
func TestEmptyOption(t *testing.T) {
|
|
is := is.New(t)
|
|
|
|
type config struct {
|
|
Var string `env:"VAR,"`
|
|
}
|
|
|
|
cfg := &config{}
|
|
|
|
os.Setenv("VAR", "")
|
|
defer os.Clearenv()
|
|
is.NoErr(Parse(cfg))
|
|
is.Equal("", cfg.Var)
|
|
}
|
|
|
|
func TestErrorOptionNotRecognized(t *testing.T) {
|
|
type config struct {
|
|
Var string `env:"VAR,not_supported!"`
|
|
}
|
|
isErrorWithMessage(t, Parse(&config{}), `env: tag option "not_supported!" not supported`)
|
|
}
|
|
|
|
func TestTextUnmarshalerError(t *testing.T) {
|
|
type config struct {
|
|
Unmarshaler unmarshaler `env:"UNMARSHALER"`
|
|
}
|
|
os.Setenv("UNMARSHALER", "invalid")
|
|
isErrorWithMessage(t, Parse(&config{}), `env: parse error on field "Unmarshaler" of type "env.unmarshaler": time: invalid duration "invalid"`)
|
|
}
|
|
|
|
func TestTextUnmarshalersError(t *testing.T) {
|
|
type config struct {
|
|
Unmarshalers []unmarshaler `env:"UNMARSHALERS"`
|
|
}
|
|
os.Setenv("UNMARSHALERS", "1s,invalid")
|
|
isErrorWithMessage(t, Parse(&config{}), `env: parse error on field "Unmarshalers" of type "[]env.unmarshaler": time: invalid duration "invalid"`)
|
|
}
|
|
|
|
func TestParseURL(t *testing.T) {
|
|
is := is.New(t)
|
|
|
|
type config struct {
|
|
ExampleURL url.URL `env:"EXAMPLE_URL" envDefault:"https://google.com"`
|
|
}
|
|
var cfg config
|
|
is.NoErr(Parse(&cfg))
|
|
is.Equal("https://google.com", cfg.ExampleURL.String())
|
|
}
|
|
|
|
func TestParseInvalidURL(t *testing.T) {
|
|
type config struct {
|
|
ExampleURL url.URL `env:"EXAMPLE_URL_2"`
|
|
}
|
|
os.Setenv("EXAMPLE_URL_2", "nope://s s/")
|
|
|
|
isErrorWithMessage(t, Parse(&config{}), `env: parse error on field "ExampleURL" of type "url.URL": unable to parse URL: parse "nope://s s/": invalid character " " in host name`)
|
|
}
|
|
|
|
func ExampleParse() {
|
|
type inner struct {
|
|
Foo string `env:"FOO" envDefault:"foobar"`
|
|
}
|
|
type config struct {
|
|
Home string `env:"HOME,required"`
|
|
Port int `env:"PORT" envDefault:"3000"`
|
|
IsProduction bool `env:"PRODUCTION"`
|
|
Inner inner
|
|
}
|
|
os.Setenv("HOME", "/tmp/fakehome")
|
|
var cfg config
|
|
if err := Parse(&cfg); err != nil {
|
|
fmt.Println("failed:", err)
|
|
}
|
|
fmt.Printf("%+v", cfg)
|
|
// Output: {Home:/tmp/fakehome Port:3000 IsProduction:false Inner:{Foo:foobar}}
|
|
}
|
|
|
|
func ExampleParse_onSet() {
|
|
type config struct {
|
|
Home string `env:"HOME,required"`
|
|
Port int `env:"PORT" envDefault:"3000"`
|
|
IsProduction bool `env:"PRODUCTION"`
|
|
}
|
|
os.Setenv("HOME", "/tmp/fakehome")
|
|
var cfg config
|
|
if err := Parse(&cfg, Options{
|
|
OnSet: func(tag string, value interface{}, isDefault bool) {
|
|
fmt.Printf("Set %s to %v (default? %v)\n", tag, value, isDefault)
|
|
},
|
|
}); err != nil {
|
|
fmt.Println("failed:", err)
|
|
}
|
|
fmt.Printf("%+v", cfg)
|
|
// Output: Set HOME to /tmp/fakehome (default? false)
|
|
// Set PORT to 3000 (default? true)
|
|
// Set PRODUCTION to (default? false)
|
|
// {Home:/tmp/fakehome Port:3000 IsProduction:false}
|
|
}
|
|
|
|
func ExampleParse_defaults() {
|
|
type config struct {
|
|
A string `env:"FOO" envDefault:"foo"`
|
|
B string `env:"FOO"`
|
|
}
|
|
|
|
// env FOO is not set
|
|
|
|
cfg := config{
|
|
A: "A",
|
|
B: "B",
|
|
}
|
|
if err := Parse(&cfg); err != nil {
|
|
fmt.Println("failed:", err)
|
|
}
|
|
fmt.Printf("%+v", cfg)
|
|
// Output: {A:foo B:B}
|
|
}
|
|
|
|
func TestIgnoresUnexported(t *testing.T) {
|
|
is := is.New(t)
|
|
|
|
type unexportedConfig struct {
|
|
home string `env:"HOME"`
|
|
Home2 string `env:"HOME"`
|
|
}
|
|
cfg := unexportedConfig{}
|
|
|
|
os.Setenv("HOME", "/tmp/fakehome")
|
|
is.NoErr(Parse(&cfg))
|
|
is.Equal(cfg.home, "")
|
|
is.Equal("/tmp/fakehome", cfg.Home2)
|
|
}
|
|
|
|
type LogLevel int8
|
|
|
|
func (l *LogLevel) UnmarshalText(text []byte) error {
|
|
txt := string(text)
|
|
switch txt {
|
|
case "debug":
|
|
*l = DebugLevel
|
|
case "info":
|
|
*l = InfoLevel
|
|
default:
|
|
return fmt.Errorf("unknown level: %q", txt)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
const (
|
|
DebugLevel LogLevel = iota - 1
|
|
InfoLevel
|
|
)
|
|
|
|
func TestPrecedenceUnmarshalText(t *testing.T) {
|
|
is := is.New(t)
|
|
|
|
os.Setenv("LOG_LEVEL", "debug")
|
|
os.Setenv("LOG_LEVELS", "debug,info")
|
|
defer os.Unsetenv("LOG_LEVEL")
|
|
defer os.Unsetenv("LOG_LEVELS")
|
|
|
|
type config struct {
|
|
LogLevel LogLevel `env:"LOG_LEVEL"`
|
|
LogLevels []LogLevel `env:"LOG_LEVELS"`
|
|
}
|
|
var cfg config
|
|
|
|
is.NoErr(Parse(&cfg))
|
|
is.Equal(DebugLevel, cfg.LogLevel)
|
|
is.Equal([]LogLevel{DebugLevel, InfoLevel}, cfg.LogLevels)
|
|
}
|
|
|
|
func ExampleParseWithFuncs() {
|
|
type thing struct {
|
|
desc string
|
|
}
|
|
|
|
type conf struct {
|
|
Thing thing `env:"THING"`
|
|
}
|
|
|
|
os.Setenv("THING", "my thing")
|
|
|
|
c := conf{}
|
|
|
|
err := ParseWithFuncs(&c, map[reflect.Type]ParserFunc{
|
|
reflect.TypeOf(thing{}): func(v string) (interface{}, error) {
|
|
return thing{desc: v}, nil
|
|
},
|
|
})
|
|
if err != nil {
|
|
fmt.Println(err)
|
|
}
|
|
fmt.Println(c.Thing.desc)
|
|
// Output:
|
|
// my thing
|
|
}
|
|
|
|
func TestFile(t *testing.T) {
|
|
is := is.New(t)
|
|
|
|
type config struct {
|
|
SecretKey string `env:"SECRET_KEY,file"`
|
|
}
|
|
|
|
dir := t.TempDir()
|
|
file := filepath.Join(dir, "sec_key")
|
|
is.NoErr(os.WriteFile(file, []byte("secret"), 0o660))
|
|
|
|
defer os.Clearenv()
|
|
os.Setenv("SECRET_KEY", file)
|
|
|
|
cfg := config{}
|
|
is.NoErr(Parse(&cfg))
|
|
is.Equal("secret", cfg.SecretKey)
|
|
}
|
|
|
|
func TestFileNoParam(t *testing.T) {
|
|
is := is.New(t)
|
|
|
|
type config struct {
|
|
SecretKey string `env:"SECRET_KEY,file"`
|
|
}
|
|
defer os.Clearenv()
|
|
|
|
cfg := config{}
|
|
is.NoErr(Parse(&cfg))
|
|
}
|
|
|
|
func TestFileNoParamRequired(t *testing.T) {
|
|
type config struct {
|
|
SecretKey string `env:"SECRET_KEY,file,required"`
|
|
}
|
|
isErrorWithMessage(t, Parse(&config{}), `env: required environment variable "SECRET_KEY" is not set`)
|
|
}
|
|
|
|
func TestFileBadFile(t *testing.T) {
|
|
type config struct {
|
|
SecretKey string `env:"SECRET_KEY,file"`
|
|
}
|
|
|
|
filename := "not-a-real-file"
|
|
defer os.Clearenv()
|
|
os.Setenv("SECRET_KEY", filename)
|
|
|
|
oserr := "no such file or directory"
|
|
if runtime.GOOS == "windows" {
|
|
oserr = "The system cannot find the file specified."
|
|
}
|
|
isErrorWithMessage(t, Parse(&config{}), fmt.Sprintf(`env: could not load content of file "%s" from variable SECRET_KEY: open %s: %s`, filename, filename, oserr))
|
|
}
|
|
|
|
func TestFileWithDefault(t *testing.T) {
|
|
is := is.New(t)
|
|
|
|
type config struct {
|
|
SecretKey string `env:"SECRET_KEY,file" envDefault:"${FILE}" envExpand:"true"`
|
|
}
|
|
defer os.Clearenv()
|
|
|
|
dir := t.TempDir()
|
|
file := filepath.Join(dir, "sec_key")
|
|
is.NoErr(os.WriteFile(file, []byte("secret"), 0o660))
|
|
|
|
defer os.Clearenv()
|
|
os.Setenv("FILE", file)
|
|
|
|
cfg := config{}
|
|
is.NoErr(Parse(&cfg))
|
|
is.Equal("secret", cfg.SecretKey)
|
|
}
|
|
|
|
func TestCustomSliceType(t *testing.T) {
|
|
is := is.New(t)
|
|
|
|
type customslice []byte
|
|
|
|
type config struct {
|
|
SecretKey customslice `env:"SECRET_KEY"`
|
|
}
|
|
|
|
parsecustomsclice := func(value string) (interface{}, error) {
|
|
return customslice(value), nil
|
|
}
|
|
|
|
defer os.Clearenv()
|
|
os.Setenv("SECRET_KEY", "somesecretkey")
|
|
|
|
var cfg config
|
|
is.NoErr(ParseWithFuncs(&cfg, map[reflect.Type]ParserFunc{reflect.TypeOf(customslice{}): parsecustomsclice}))
|
|
}
|
|
|
|
func TestBlankKey(t *testing.T) {
|
|
is := is.New(t)
|
|
|
|
type testStruct struct {
|
|
Blank string
|
|
BlankWithTag string `env:""`
|
|
}
|
|
|
|
val := testStruct{}
|
|
|
|
defer os.Clearenv()
|
|
os.Setenv("", "You should not see this")
|
|
|
|
is.NoErr(Parse(&val))
|
|
is.Equal("", val.Blank)
|
|
is.Equal("", val.BlankWithTag)
|
|
}
|
|
|
|
type MyTime time.Time
|
|
|
|
func (t *MyTime) UnmarshalText(text []byte) error {
|
|
tt, err := time.Parse("2006-01-02", string(text))
|
|
*t = MyTime(tt)
|
|
return err
|
|
}
|
|
|
|
func TestCustomTimeParser(t *testing.T) {
|
|
is := is.New(t)
|
|
|
|
type config struct {
|
|
SomeTime MyTime `env:"SOME_TIME"`
|
|
}
|
|
|
|
os.Setenv("SOME_TIME", "2021-05-06")
|
|
defer os.Unsetenv("SOME_TIME")
|
|
|
|
var cfg config
|
|
is.NoErr(Parse(&cfg))
|
|
is.Equal(2021, time.Time(cfg.SomeTime).Year())
|
|
is.Equal(time.Month(5), time.Time(cfg.SomeTime).Month())
|
|
is.Equal(6, time.Time(cfg.SomeTime).Day())
|
|
}
|
|
|
|
func TestRequiredIfNoDefOption(t *testing.T) {
|
|
type Tree struct {
|
|
Fruit string `env:"FRUIT"`
|
|
}
|
|
type config struct {
|
|
Name string `env:"NAME"`
|
|
Genre string `env:"GENRE" envDefault:"Unknown"`
|
|
Tree
|
|
}
|
|
var cfg config
|
|
|
|
t.Run("missing", func(t *testing.T) {
|
|
isErrorWithMessage(t, Parse(&cfg, Options{RequiredIfNoDef: true}), `env: required environment variable "NAME" is not set`)
|
|
os.Setenv("NAME", "John")
|
|
t.Cleanup(os.Clearenv)
|
|
isErrorWithMessage(t, Parse(&cfg, Options{RequiredIfNoDef: true}), `env: required environment variable "FRUIT" is not set`)
|
|
})
|
|
|
|
t.Run("all set", func(t *testing.T) {
|
|
os.Setenv("NAME", "John")
|
|
os.Setenv("FRUIT", "Apple")
|
|
t.Cleanup(os.Clearenv)
|
|
|
|
// should not trigger an error for the missing 'GENRE' env because it has a default value.
|
|
is.New(t).NoErr(Parse(&cfg, Options{RequiredIfNoDef: true}))
|
|
})
|
|
}
|
|
|
|
func TestPrefix(t *testing.T) {
|
|
is := is.New(t)
|
|
type Config struct {
|
|
Home string `env:"HOME"`
|
|
}
|
|
type ComplexConfig struct {
|
|
Foo Config `envPrefix:"FOO_"`
|
|
Bar Config `envPrefix:"BAR_"`
|
|
Clean Config
|
|
}
|
|
cfg := ComplexConfig{}
|
|
err := Parse(&cfg, Options{Environment: map[string]string{"FOO_HOME": "/foo", "BAR_HOME": "/bar", "HOME": "/clean"}})
|
|
is.NoErr(err)
|
|
is.Equal("/foo", cfg.Foo.Home)
|
|
is.Equal("/bar", cfg.Bar.Home)
|
|
is.Equal("/clean", cfg.Clean.Home)
|
|
}
|
|
|
|
func TestComplePrefix(t *testing.T) {
|
|
is := is.New(t)
|
|
type Config struct {
|
|
Home string `env:"HOME"`
|
|
}
|
|
type ComplexConfig struct {
|
|
Foo Config `envPrefix:"FOO_"`
|
|
Clean Config
|
|
Bar Config `envPrefix:"BAR_"`
|
|
Blah string `env:"BLAH"`
|
|
}
|
|
cfg := ComplexConfig{}
|
|
err := Parse(&cfg, Options{
|
|
Prefix: "T_",
|
|
Environment: map[string]string{
|
|
"T_FOO_HOME": "/foo",
|
|
"T_BAR_HOME": "/bar",
|
|
"T_BLAH": "blahhh",
|
|
"T_HOME": "/clean",
|
|
},
|
|
})
|
|
is.NoErr(err)
|
|
is.Equal("/foo", cfg.Foo.Home)
|
|
is.Equal("/bar", cfg.Bar.Home)
|
|
is.Equal("/clean", cfg.Clean.Home)
|
|
is.Equal("blahhh", cfg.Blah)
|
|
}
|
|
|
|
func isErrorWithMessage(tb testing.TB, err error, msg string) {
|
|
tb.Helper()
|
|
|
|
is := is.New(tb)
|
|
is.True(err != nil) // should have failed
|
|
is.Equal(err.Error(), msg) // should have the expected message
|
|
}
|