Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: config to override env vars #341

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 35 additions & 4 deletions providers/env/env.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,18 @@ type Env struct {
prefix string
delim string
cb func(key string, value string) (string, interface{})
opt *Opt
}

// Opt represents optional configuration passed to the provider.
type Opt struct {
// EnvironFunc is the function that feeds environment variables
// to the provider.
EnvironFunc func() []string
}

var defaultOpt = &Opt{
EnvironFunc: os.Environ,
}

// Provider returns an environment variables provider that returns
Expand All @@ -30,29 +42,48 @@ type Env struct {
// everything, strip prefixes and replace _ with . etc.
// If the callback returns an empty string, the variable will be
// ignored.
func Provider(prefix, delim string, cb func(s string) string) *Env {
//
// It takes an optional Opt argument containing a function to override
// the default source for environment variables, which can be useful
// for mocking and parallel unit tests.
func Provider(prefix, delim string, cb func(s string) string, opt ...*Opt) *Env {
e := &Env{
prefix: prefix,
delim: delim,
opt: defaultOpt,
}
if cb != nil {
e.cb = func(key string, value string) (string, interface{}) {
return cb(key), value
}
}
if len(opt) > 0 {
e.opt = opt[0]
}

return e
}

// ProviderWithValue works exactly the same as Provider except the callback
// takes a (key, value) with the variable name and value and allows you
// to modify both. This is useful for cases where you may want to return
// other types like a string slice instead of just a string.
func ProviderWithValue(prefix, delim string, cb func(key string, value string) (string, interface{})) *Env {
return &Env{
//
// It takes an optional Opt argument containing a function to override
// the default source for environment variables, which can be useful
// for mocking and parallel unit tests.
func ProviderWithValue(prefix, delim string, cb func(key string, value string) (string, interface{}), opt ...*Opt) *Env {
e := &Env{
prefix: prefix,
delim: delim,
cb: cb,
opt: defaultOpt,
}
if len(opt) > 0 {
e.opt = opt[0]
}

return e
}

// ReadBytes is not supported by the env provider.
Expand All @@ -65,7 +96,7 @@ func (e *Env) ReadBytes() ([]byte, error) {
func (e *Env) Read() (map[string]interface{}, error) {
// Collect the environment variable keys.
var keys []string
for _, k := range os.Environ() {
for _, k := range e.opt.EnvironFunc() {
if e.prefix != "" {
if strings.HasPrefix(k, e.prefix) {
keys = append(keys, k)
Expand Down
148 changes: 124 additions & 24 deletions providers/env/env_test.go
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
package env

import (
"github.com/stretchr/testify/assert"
"os"
"slices"
"strings"
"testing"

"github.com/stretchr/testify/assert"
)

func TestProvider(t *testing.T) {
mockEnviron := func() []string {
return []string{"TEST_FOO=bar"}
}

testCases := []struct {
name string
Expand All @@ -18,6 +22,7 @@ func TestProvider(t *testing.T) {
expKey string
expValue string
cb func(key string) string
opt *Opt
want *Env
}{
{
Expand All @@ -27,6 +32,7 @@ func TestProvider(t *testing.T) {
want: &Env{
prefix: "TESTVAR_",
delim: ".",
opt: defaultOpt,
},
},
{
Expand All @@ -43,6 +49,7 @@ func TestProvider(t *testing.T) {
want: &Env{
prefix: "TESTVAR_",
delim: ".",
opt: defaultOpt,
},
},
{
Expand All @@ -52,6 +59,7 @@ func TestProvider(t *testing.T) {
want: &Env{
prefix: "",
delim: ".",
opt: defaultOpt,
},
},
{
Expand All @@ -66,50 +74,87 @@ func TestProvider(t *testing.T) {
return strings.Replace(strings.ToUpper(key), "_", ".", -1)
},
},
{
name: "Custom opt",
prefix: "TEST_",
delim: ".",
opt: &Opt{
EnvironFunc: mockEnviron,
},
want: &Env{
prefix: "TEST_",
delim: ".",
opt: &Opt{
EnvironFunc: mockEnviron,
},
},
},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
gotProvider := Provider(tc.prefix, tc.delim, tc.cb)
if tc.cb == nil {
var gotProvider *Env
if tc.opt != nil {
gotProvider = Provider(tc.prefix, tc.delim, tc.cb, tc.opt)
} else {
gotProvider = Provider(tc.prefix, tc.delim, tc.cb)
}

if tc.cb == nil && tc.opt == nil {
assert.Equal(t, tc.want, gotProvider)
return
}

if tc.cb != nil {
k, v := gotProvider.cb(tc.key, tc.value)
assert.Equal(t, tc.expKey, k)
assert.Equal(t, tc.expValue, v)
}
if tc.opt != nil {
wantEnv := tc.want.opt.EnvironFunc()
gotEnv := gotProvider.opt.EnvironFunc()
slices.Sort(wantEnv)
slices.Sort(gotEnv)
if !slices.Equal(wantEnv, gotEnv) {
assert.Fail(t, "Env vars not equal (omitted from message for security)",
"Want len: %d\nGot len:%d", len(wantEnv), len(gotEnv))
}
}
})
}
}

func TestProviderWithValue(t *testing.T) {
mockEnviron := func() []string {
return []string{"TEST_FOO=bar"}
}

testCases := []struct {
name string
prefix string
delim string
cb func(key string, value string) (string, interface{})
nilCallback bool
want *Env
name string
prefix string
delim string
cb func(key string, value string) (string, interface{})
opt *Opt
want *Env
}{
{
name: "Nil cb",
prefix: "TEST_",
delim: ".",
nilCallback: true,
name: "Nil cb",
prefix: "TEST_",
delim: ".",
want: &Env{
prefix: "TEST_",
delim: ".",
opt: defaultOpt,
},
},
{
name: "Empty string nil cb",
prefix: "",
delim: ".",
nilCallback: true,
name: "Empty string nil cb",
prefix: "",
delim: ".",
want: &Env{
prefix: "",
delim: ".",
opt: defaultOpt,
},
},
{
Expand All @@ -125,6 +170,7 @@ func TestProviderWithValue(t *testing.T) {
cb: func(key string, value string) (string, interface{}) {
return key, value
},
opt: defaultOpt,
},
},
{
Expand All @@ -142,23 +188,58 @@ func TestProviderWithValue(t *testing.T) {
key = strings.Replace(strings.TrimPrefix(strings.ToLower(key), "test_"), "_", ".", -1)
return key, value
},
opt: defaultOpt,
},
},
{
name: "Custom opt",
prefix: "TEST_",
delim: ".",
opt: &Opt{
EnvironFunc: mockEnviron,
},
want: &Env{
prefix: "TEST_",
delim: ".",
opt: &Opt{
EnvironFunc: mockEnviron,
},
},
},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
got := ProviderWithValue(tc.prefix, tc.delim, tc.cb)
if tc.nilCallback {
assert.Equal(t, tc.want, got)
var got *Env
if tc.opt != nil {
got = ProviderWithValue(tc.prefix, tc.delim, tc.cb, tc.opt)
} else {
got = ProviderWithValue(tc.prefix, tc.delim, tc.cb)
}
if tc.cb == nil && tc.opt == nil {
assert.Equal(t, tc.want, got)
return
}

if tc.cb != nil {
keyGot, valGot := got.cb("test_key_env_1", "test_val")
keyWant, valWant := tc.want.cb("test_key_env_1", "test_val")
assert.Equal(t, tc.prefix, got.prefix)
assert.Equal(t, tc.delim, got.delim)
assert.Equal(t, keyWant, keyGot)
assert.Equal(t, valWant, valGot)
}
if tc.opt != nil {
wantEnv := tc.want.opt.EnvironFunc()
gotEnv := got.opt.EnvironFunc()
slices.Sort(wantEnv)
slices.Sort(gotEnv)
if !slices.Equal(wantEnv, gotEnv) {
assert.Fail(t, "Env vars not equal (omitted from message for security)",
"Want len: %d\nGot len:%d", len(wantEnv), len(gotEnv))
}
assert.Equal(t, tc.want.opt.EnvironFunc(), got.opt.EnvironFunc())
}
})
}
}
Expand All @@ -180,6 +261,7 @@ func TestRead(t *testing.T) {
expValue: "TEST_VAL",
env: &Env{
delim: ".",
opt: defaultOpt,
},
},
{
Expand All @@ -193,6 +275,7 @@ func TestRead(t *testing.T) {
cb: func(key string, value string) (string, interface{}) {
return strings.Replace(strings.ToLower(key), "_", ".", -1), value
},
opt: defaultOpt,
},
},
{
Expand All @@ -207,6 +290,7 @@ func TestRead(t *testing.T) {
cb: func(key string, value string) (string, interface{}) {
return strings.Replace(strings.ToLower(key), "_", ".", -1), value
},
opt: defaultOpt,
},
},
{
Expand All @@ -217,6 +301,7 @@ func TestRead(t *testing.T) {
expValue: "/test/dir/file",
env: &Env{
delim: ".",
opt: defaultOpt,
},
},
{
Expand All @@ -230,6 +315,7 @@ func TestRead(t *testing.T) {
cb: func(key string, value string) (string, interface{}) {
return key, strings.Replace(strings.ToLower(value), "/", "_", -1)
},
opt: defaultOpt,
},
},
{
Expand All @@ -240,15 +326,29 @@ func TestRead(t *testing.T) {
expValue: "",
env: &Env{
delim: ".",
opt: defaultOpt,
},
},
{
name: "Environ func provided",
key: "TEST_KEY",
value: "TEST_VAL",
expKey: "TEST_OVERRIDE_KEY",
expValue: "TEST_OVERRIDE_VAL",
env: &Env{
delim: ".",
opt: &Opt{
EnvironFunc: func() []string {
return []string{"TEST_OVERRIDE_KEY=TEST_OVERRIDE_VAL"}
},
},
},
},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
err := os.Setenv(tc.key, tc.value)
assert.Nil(t, err)
defer os.Unsetenv(tc.key)
t.Setenv(tc.key, tc.value)

envs, err := tc.env.Read()
assert.Nil(t, err)
Expand Down