mirror of
https://github.com/spf13/viper
synced 2024-11-16 18:07:02 +00:00
d6e5a55f3c
Add functionality to support errors.Is on all generated errors to keep in line with best practice on checking whether an error is of the specified type as per changes to error handling in go1.13.
2756 lines
63 KiB
Go
2756 lines
63 KiB
Go
// Copyright © 2014 Steve Francia <spf@spf13.com>.
|
|
//
|
|
// Use of this source code is governed by an MIT-style
|
|
// license that can be found in the LICENSE file.
|
|
|
|
package viper
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"os/exec"
|
|
"path"
|
|
"path/filepath"
|
|
"reflect"
|
|
"runtime"
|
|
"strings"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/fsnotify/fsnotify"
|
|
"github.com/mitchellh/mapstructure"
|
|
"github.com/spf13/afero"
|
|
"github.com/spf13/cast"
|
|
"github.com/spf13/pflag"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"github.com/spf13/viper/internal/features"
|
|
"github.com/spf13/viper/internal/testutil"
|
|
)
|
|
|
|
// var yamlExample = []byte(`Hacker: true
|
|
// name: steve
|
|
// hobbies:
|
|
// - skateboarding
|
|
// - snowboarding
|
|
// - go
|
|
// clothing:
|
|
// jacket: leather
|
|
// trousers: denim
|
|
// pants:
|
|
// size: large
|
|
// age: 35
|
|
// eyes : brown
|
|
// beard: true
|
|
// `)
|
|
|
|
var yamlExampleWithExtras = []byte(`Existing: true
|
|
Bogus: true
|
|
`)
|
|
|
|
type testUnmarshalExtra struct {
|
|
Existing bool
|
|
}
|
|
|
|
var tomlExample = []byte(`
|
|
title = "TOML Example"
|
|
|
|
[owner]
|
|
organization = "MongoDB"
|
|
Bio = "MongoDB Chief Developer Advocate & Hacker at Large"
|
|
dob = 1979-05-27T07:32:00Z # First class dates? Why not?`)
|
|
|
|
var dotenvExample = []byte(`
|
|
TITLE_DOTENV="DotEnv Example"
|
|
TYPE_DOTENV=donut
|
|
NAME_DOTENV=Cake`)
|
|
|
|
var jsonExample = []byte(`{
|
|
"id": "0001",
|
|
"type": "donut",
|
|
"name": "Cake",
|
|
"ppu": 0.55,
|
|
"batters": {
|
|
"batter": [
|
|
{ "type": "Regular" },
|
|
{ "type": "Chocolate" },
|
|
{ "type": "Blueberry" },
|
|
{ "type": "Devil's Food" }
|
|
]
|
|
}
|
|
}`)
|
|
|
|
var hclExample = []byte(`
|
|
id = "0001"
|
|
type = "donut"
|
|
name = "Cake"
|
|
ppu = 0.55
|
|
foos {
|
|
foo {
|
|
key = 1
|
|
}
|
|
foo {
|
|
key = 2
|
|
}
|
|
foo {
|
|
key = 3
|
|
}
|
|
foo {
|
|
key = 4
|
|
}
|
|
}`)
|
|
|
|
var propertiesExample = []byte(`
|
|
p_id: 0001
|
|
p_type: donut
|
|
p_name: Cake
|
|
p_ppu: 0.55
|
|
p_batters.batter.type: Regular
|
|
`)
|
|
|
|
var remoteExample = []byte(`{
|
|
"id":"0002",
|
|
"type":"cronut",
|
|
"newkey":"remote"
|
|
}`)
|
|
|
|
var iniExample = []byte(`; Package name
|
|
NAME = ini
|
|
; Package version
|
|
VERSION = v1
|
|
; Package import path
|
|
IMPORT_PATH = gopkg.in/%(NAME)s.%(VERSION)s
|
|
|
|
# Information about package author
|
|
# Bio can be written in multiple lines.
|
|
[author]
|
|
NAME = Unknown ; Succeeding comment
|
|
E-MAIL = fake@localhost
|
|
GITHUB = https://github.com/%(NAME)s
|
|
BIO = """Gopher.
|
|
Coding addict.
|
|
Good man.
|
|
""" # Succeeding comment`)
|
|
|
|
func initConfigs() {
|
|
Reset()
|
|
var r io.Reader
|
|
SetConfigType("yaml")
|
|
r = bytes.NewReader(yamlExample)
|
|
unmarshalReader(r, v.config)
|
|
|
|
SetConfigType("json")
|
|
r = bytes.NewReader(jsonExample)
|
|
unmarshalReader(r, v.config)
|
|
|
|
SetConfigType("hcl")
|
|
r = bytes.NewReader(hclExample)
|
|
unmarshalReader(r, v.config)
|
|
|
|
SetConfigType("properties")
|
|
r = bytes.NewReader(propertiesExample)
|
|
unmarshalReader(r, v.config)
|
|
|
|
SetConfigType("toml")
|
|
r = bytes.NewReader(tomlExample)
|
|
unmarshalReader(r, v.config)
|
|
|
|
SetConfigType("env")
|
|
r = bytes.NewReader(dotenvExample)
|
|
unmarshalReader(r, v.config)
|
|
|
|
SetConfigType("json")
|
|
remote := bytes.NewReader(remoteExample)
|
|
unmarshalReader(remote, v.kvstore)
|
|
|
|
SetConfigType("ini")
|
|
r = bytes.NewReader(iniExample)
|
|
unmarshalReader(r, v.config)
|
|
}
|
|
|
|
func initConfig(typ, config string) {
|
|
Reset()
|
|
SetConfigType(typ)
|
|
r := strings.NewReader(config)
|
|
|
|
if err := unmarshalReader(r, v.config); err != nil {
|
|
panic(err)
|
|
}
|
|
}
|
|
|
|
func initYAML() {
|
|
initConfig("yaml", string(yamlExample))
|
|
}
|
|
|
|
func initJSON() {
|
|
Reset()
|
|
SetConfigType("json")
|
|
r := bytes.NewReader(jsonExample)
|
|
|
|
unmarshalReader(r, v.config)
|
|
}
|
|
|
|
func initProperties() {
|
|
Reset()
|
|
SetConfigType("properties")
|
|
r := bytes.NewReader(propertiesExample)
|
|
|
|
unmarshalReader(r, v.config)
|
|
}
|
|
|
|
func initTOML() {
|
|
Reset()
|
|
SetConfigType("toml")
|
|
r := bytes.NewReader(tomlExample)
|
|
|
|
unmarshalReader(r, v.config)
|
|
}
|
|
|
|
func initDotEnv() {
|
|
Reset()
|
|
SetConfigType("env")
|
|
r := bytes.NewReader(dotenvExample)
|
|
|
|
unmarshalReader(r, v.config)
|
|
}
|
|
|
|
func initHcl() {
|
|
Reset()
|
|
SetConfigType("hcl")
|
|
r := bytes.NewReader(hclExample)
|
|
|
|
unmarshalReader(r, v.config)
|
|
}
|
|
|
|
func initIni() {
|
|
Reset()
|
|
SetConfigType("ini")
|
|
r := bytes.NewReader(iniExample)
|
|
|
|
unmarshalReader(r, v.config)
|
|
}
|
|
|
|
// initDirs makes directories for testing.
|
|
func initDirs(t *testing.T) (string, string) {
|
|
var (
|
|
testDirs = []string{`a a`, `b`, `C_`}
|
|
config = `improbable`
|
|
)
|
|
|
|
if runtime.GOOS != "windows" {
|
|
testDirs = append(testDirs, `d\d`)
|
|
}
|
|
|
|
root := t.TempDir()
|
|
|
|
for _, dir := range testDirs {
|
|
innerDir := filepath.Join(root, dir)
|
|
err := os.Mkdir(innerDir, 0o750)
|
|
require.NoError(t, err)
|
|
|
|
err = os.WriteFile(
|
|
filepath.Join(innerDir, config+".toml"),
|
|
[]byte(`key = "value is `+dir+`"`+"\n"),
|
|
0o640)
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
return root, config
|
|
}
|
|
|
|
// stubs for PFlag Values.
|
|
type stringValue string
|
|
|
|
func newStringValue(val string, p *string) *stringValue {
|
|
*p = val
|
|
return (*stringValue)(p)
|
|
}
|
|
|
|
func (s *stringValue) Set(val string) error {
|
|
*s = stringValue(val)
|
|
return nil
|
|
}
|
|
|
|
func (s *stringValue) Type() string {
|
|
return "string"
|
|
}
|
|
|
|
func (s *stringValue) String() string {
|
|
return string(*s)
|
|
}
|
|
|
|
func TestGetConfigFile(t *testing.T) {
|
|
t.Run("config file set", func(t *testing.T) {
|
|
fs := afero.NewMemMapFs()
|
|
|
|
err := fs.Mkdir(testutil.AbsFilePath(t, "/etc/viper"), 0o777)
|
|
require.NoError(t, err)
|
|
|
|
_, err = fs.Create(testutil.AbsFilePath(t, "/etc/viper/config.yaml"))
|
|
require.NoError(t, err)
|
|
|
|
v := New()
|
|
|
|
v.SetFs(fs)
|
|
v.AddConfigPath("/etc/viper")
|
|
v.SetConfigFile(testutil.AbsFilePath(t, "/etc/viper/config.yaml"))
|
|
|
|
filename, err := v.getConfigFile()
|
|
assert.Equal(t, testutil.AbsFilePath(t, "/etc/viper/config.yaml"), filename)
|
|
assert.NoError(t, err)
|
|
})
|
|
|
|
t.Run("find file", func(t *testing.T) {
|
|
fs := afero.NewMemMapFs()
|
|
|
|
err := fs.Mkdir(testutil.AbsFilePath(t, "/etc/viper"), 0o777)
|
|
require.NoError(t, err)
|
|
|
|
_, err = fs.Create(testutil.AbsFilePath(t, "/etc/viper/config.yaml"))
|
|
require.NoError(t, err)
|
|
|
|
v := New()
|
|
|
|
v.SetFs(fs)
|
|
v.AddConfigPath("/etc/viper")
|
|
|
|
filename, err := v.getConfigFile()
|
|
assert.Equal(t, testutil.AbsFilePath(t, "/etc/viper/config.yaml"), filename)
|
|
assert.NoError(t, err)
|
|
})
|
|
|
|
t.Run("find files only", func(t *testing.T) {
|
|
fs := afero.NewMemMapFs()
|
|
|
|
err := fs.Mkdir(testutil.AbsFilePath(t, "/etc/config"), 0o777)
|
|
require.NoError(t, err)
|
|
|
|
_, err = fs.Create(testutil.AbsFilePath(t, "/etc/config/config.yaml"))
|
|
require.NoError(t, err)
|
|
|
|
v := New()
|
|
|
|
v.SetFs(fs)
|
|
v.AddConfigPath("/etc")
|
|
v.AddConfigPath("/etc/config")
|
|
|
|
filename, err := v.getConfigFile()
|
|
assert.Equal(t, testutil.AbsFilePath(t, "/etc/config/config.yaml"), filename)
|
|
assert.NoError(t, err)
|
|
})
|
|
|
|
t.Run("precedence", func(t *testing.T) {
|
|
fs := afero.NewMemMapFs()
|
|
|
|
err := fs.Mkdir(testutil.AbsFilePath(t, "/home/viper"), 0o777)
|
|
require.NoError(t, err)
|
|
|
|
_, err = fs.Create(testutil.AbsFilePath(t, "/home/viper/config.zml"))
|
|
require.NoError(t, err)
|
|
|
|
err = fs.Mkdir(testutil.AbsFilePath(t, "/etc/viper"), 0o777)
|
|
require.NoError(t, err)
|
|
|
|
_, err = fs.Create(testutil.AbsFilePath(t, "/etc/viper/config.bml"))
|
|
require.NoError(t, err)
|
|
|
|
err = fs.Mkdir(testutil.AbsFilePath(t, "/var/viper"), 0o777)
|
|
require.NoError(t, err)
|
|
|
|
_, err = fs.Create(testutil.AbsFilePath(t, "/var/viper/config.yaml"))
|
|
require.NoError(t, err)
|
|
|
|
v := New()
|
|
|
|
v.SetFs(fs)
|
|
v.AddConfigPath("/home/viper")
|
|
v.AddConfigPath("/etc/viper")
|
|
v.AddConfigPath("/var/viper")
|
|
|
|
filename, err := v.getConfigFile()
|
|
assert.Equal(t, testutil.AbsFilePath(t, "/var/viper/config.yaml"), filename)
|
|
assert.NoError(t, err)
|
|
})
|
|
|
|
t.Run("without extension", func(t *testing.T) {
|
|
fs := afero.NewMemMapFs()
|
|
|
|
err := fs.Mkdir(testutil.AbsFilePath(t, "/etc/viper"), 0o777)
|
|
require.NoError(t, err)
|
|
|
|
_, err = fs.Create(testutil.AbsFilePath(t, "/etc/viper/.dotfilenoext"))
|
|
require.NoError(t, err)
|
|
|
|
v := New()
|
|
|
|
v.SetFs(fs)
|
|
v.AddConfigPath("/etc/viper")
|
|
v.SetConfigName(".dotfilenoext")
|
|
v.SetConfigType("yaml")
|
|
|
|
filename, err := v.getConfigFile()
|
|
assert.Equal(t, testutil.AbsFilePath(t, "/etc/viper/.dotfilenoext"), filename)
|
|
assert.NoError(t, err)
|
|
})
|
|
|
|
t.Run("without extension and config type", func(t *testing.T) {
|
|
fs := afero.NewMemMapFs()
|
|
|
|
err := fs.Mkdir(testutil.AbsFilePath(t, "/etc/viper"), 0o777)
|
|
require.NoError(t, err)
|
|
|
|
_, err = fs.Create(testutil.AbsFilePath(t, "/etc/viper/.dotfilenoext"))
|
|
require.NoError(t, err)
|
|
|
|
v := New()
|
|
|
|
v.SetFs(fs)
|
|
v.AddConfigPath("/etc/viper")
|
|
v.SetConfigName(".dotfilenoext")
|
|
|
|
_, err = v.getConfigFile()
|
|
// unless config type is set, files without extension
|
|
// are not considered
|
|
assert.Error(t, err)
|
|
})
|
|
}
|
|
|
|
func TestReadInConfig(t *testing.T) {
|
|
t.Run("config file set", func(t *testing.T) {
|
|
fs := afero.NewMemMapFs()
|
|
|
|
err := fs.Mkdir("/etc/viper", 0o777)
|
|
require.NoError(t, err)
|
|
|
|
file, err := fs.Create(testutil.AbsFilePath(t, "/etc/viper/config.yaml"))
|
|
require.NoError(t, err)
|
|
|
|
_, err = file.WriteString(`key: value`)
|
|
require.NoError(t, err)
|
|
|
|
file.Close()
|
|
|
|
v := New()
|
|
|
|
v.SetFs(fs)
|
|
v.SetConfigFile(testutil.AbsFilePath(t, "/etc/viper/config.yaml"))
|
|
|
|
err = v.ReadInConfig()
|
|
require.NoError(t, err)
|
|
|
|
assert.Equal(t, "value", v.Get("key"))
|
|
})
|
|
|
|
t.Run("find file", func(t *testing.T) {
|
|
fs := afero.NewMemMapFs()
|
|
|
|
err := fs.Mkdir(testutil.AbsFilePath(t, "/etc/viper"), 0o777)
|
|
require.NoError(t, err)
|
|
|
|
file, err := fs.Create(testutil.AbsFilePath(t, "/etc/viper/config.yaml"))
|
|
require.NoError(t, err)
|
|
|
|
_, err = file.WriteString(`key: value`)
|
|
require.NoError(t, err)
|
|
|
|
file.Close()
|
|
|
|
v := New()
|
|
|
|
v.SetFs(fs)
|
|
v.AddConfigPath("/etc/viper")
|
|
|
|
err = v.ReadInConfig()
|
|
require.NoError(t, err)
|
|
|
|
assert.Equal(t, "value", v.Get("key"))
|
|
})
|
|
}
|
|
|
|
func TestDefault(t *testing.T) {
|
|
Reset()
|
|
SetDefault("age", 45)
|
|
assert.Equal(t, 45, Get("age"))
|
|
|
|
SetDefault("clothing.jacket", "slacks")
|
|
assert.Equal(t, "slacks", Get("clothing.jacket"))
|
|
|
|
SetConfigType("yaml")
|
|
err := ReadConfig(bytes.NewBuffer(yamlExample))
|
|
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, "leather", Get("clothing.jacket"))
|
|
}
|
|
|
|
func TestUnmarshaling(t *testing.T) {
|
|
Reset()
|
|
SetConfigType("yaml")
|
|
r := bytes.NewReader(yamlExample)
|
|
|
|
unmarshalReader(r, v.config)
|
|
assert.True(t, InConfig("name"))
|
|
assert.True(t, InConfig("clothing.jacket"))
|
|
assert.False(t, InConfig("state"))
|
|
assert.False(t, InConfig("clothing.hat"))
|
|
assert.Equal(t, "steve", Get("name"))
|
|
assert.Equal(t, []any{"skateboarding", "snowboarding", "go"}, Get("hobbies"))
|
|
assert.Equal(t, map[string]any{"jacket": "leather", "trousers": "denim", "pants": map[string]any{"size": "large"}}, Get("clothing"))
|
|
assert.Equal(t, 35, Get("age"))
|
|
}
|
|
|
|
func TestUnmarshalExact(t *testing.T) {
|
|
vip := New()
|
|
target := &testUnmarshalExtra{}
|
|
vip.SetConfigType("yaml")
|
|
r := bytes.NewReader(yamlExampleWithExtras)
|
|
vip.ReadConfig(r)
|
|
err := vip.UnmarshalExact(target)
|
|
assert.Error(t, err, "UnmarshalExact should error when populating a struct from a conf that contains unused fields")
|
|
}
|
|
|
|
func TestOverrides(t *testing.T) {
|
|
Set("age", 40)
|
|
assert.Equal(t, 40, Get("age"))
|
|
}
|
|
|
|
func TestDefaultPost(t *testing.T) {
|
|
assert.NotEqual(t, "NYC", Get("state"))
|
|
SetDefault("state", "NYC")
|
|
assert.Equal(t, "NYC", Get("state"))
|
|
}
|
|
|
|
func TestAliases(t *testing.T) {
|
|
initConfigs()
|
|
Set("age", 40)
|
|
RegisterAlias("years", "age")
|
|
assert.Equal(t, 40, Get("years"))
|
|
Set("years", 45)
|
|
assert.Equal(t, 45, Get("age"))
|
|
}
|
|
|
|
func TestAliasInConfigFile(t *testing.T) {
|
|
initConfigs()
|
|
// the config file specifies "beard". If we make this an alias for
|
|
// "hasbeard", we still want the old config file to work with beard.
|
|
RegisterAlias("beard", "hasbeard")
|
|
assert.Equal(t, true, Get("hasbeard"))
|
|
Set("hasbeard", false)
|
|
assert.Equal(t, false, Get("beard"))
|
|
}
|
|
|
|
func TestYML(t *testing.T) {
|
|
initYAML()
|
|
assert.Equal(t, "steve", Get("name"))
|
|
}
|
|
|
|
func TestJSON(t *testing.T) {
|
|
initJSON()
|
|
assert.Equal(t, "0001", Get("id"))
|
|
}
|
|
|
|
func TestProperties(t *testing.T) {
|
|
initProperties()
|
|
assert.Equal(t, "0001", Get("p_id"))
|
|
}
|
|
|
|
func TestTOML(t *testing.T) {
|
|
initTOML()
|
|
assert.Equal(t, "TOML Example", Get("title"))
|
|
}
|
|
|
|
func TestDotEnv(t *testing.T) {
|
|
initDotEnv()
|
|
assert.Equal(t, "DotEnv Example", Get("title_dotenv"))
|
|
}
|
|
|
|
func TestHCL(t *testing.T) {
|
|
initHcl()
|
|
assert.Equal(t, "0001", Get("id"))
|
|
assert.Equal(t, 0.55, Get("ppu"))
|
|
assert.Equal(t, "donut", Get("type"))
|
|
assert.Equal(t, "Cake", Get("name"))
|
|
Set("id", "0002")
|
|
assert.Equal(t, "0002", Get("id"))
|
|
assert.NotEqual(t, "cronut", Get("type"))
|
|
}
|
|
|
|
func TestIni(t *testing.T) {
|
|
initIni()
|
|
assert.Equal(t, "ini", Get("default.name"))
|
|
}
|
|
|
|
func TestRemotePrecedence(t *testing.T) {
|
|
initJSON()
|
|
|
|
remote := bytes.NewReader(remoteExample)
|
|
assert.Equal(t, "0001", Get("id"))
|
|
unmarshalReader(remote, v.kvstore)
|
|
assert.Equal(t, "0001", Get("id"))
|
|
assert.NotEqual(t, "cronut", Get("type"))
|
|
assert.Equal(t, "remote", Get("newkey"))
|
|
Set("newkey", "newvalue")
|
|
assert.NotEqual(t, "remote", Get("newkey"))
|
|
assert.Equal(t, "newvalue", Get("newkey"))
|
|
Set("newkey", "remote")
|
|
}
|
|
|
|
func TestEnv(t *testing.T) {
|
|
initJSON()
|
|
|
|
BindEnv("id")
|
|
BindEnv("f", "FOOD", "OLD_FOOD")
|
|
|
|
t.Setenv("ID", "13")
|
|
t.Setenv("FOOD", "apple")
|
|
t.Setenv("OLD_FOOD", "banana")
|
|
t.Setenv("NAME", "crunk")
|
|
|
|
assert.Equal(t, "13", Get("id"))
|
|
assert.Equal(t, "apple", Get("f"))
|
|
assert.Equal(t, "Cake", Get("name"))
|
|
|
|
AutomaticEnv()
|
|
|
|
assert.Equal(t, "crunk", Get("name"))
|
|
}
|
|
|
|
func TestMultipleEnv(t *testing.T) {
|
|
initJSON()
|
|
|
|
BindEnv("f", "FOOD", "OLD_FOOD")
|
|
|
|
t.Setenv("OLD_FOOD", "banana")
|
|
|
|
assert.Equal(t, "banana", Get("f"))
|
|
}
|
|
|
|
func TestEmptyEnv(t *testing.T) {
|
|
initJSON()
|
|
|
|
BindEnv("type") // Empty environment variable
|
|
BindEnv("name") // Bound, but not set environment variable
|
|
|
|
t.Setenv("TYPE", "")
|
|
|
|
assert.Equal(t, "donut", Get("type"))
|
|
assert.Equal(t, "Cake", Get("name"))
|
|
}
|
|
|
|
func TestEmptyEnv_Allowed(t *testing.T) {
|
|
initJSON()
|
|
|
|
AllowEmptyEnv(true)
|
|
|
|
BindEnv("type") // Empty environment variable
|
|
BindEnv("name") // Bound, but not set environment variable
|
|
|
|
t.Setenv("TYPE", "")
|
|
|
|
assert.Equal(t, "", Get("type"))
|
|
assert.Equal(t, "Cake", Get("name"))
|
|
}
|
|
|
|
func TestEnvPrefix(t *testing.T) {
|
|
initJSON()
|
|
|
|
SetEnvPrefix("foo") // will be uppercased automatically
|
|
BindEnv("id")
|
|
BindEnv("f", "FOOD") // not using prefix
|
|
|
|
t.Setenv("FOO_ID", "13")
|
|
t.Setenv("FOOD", "apple")
|
|
t.Setenv("FOO_NAME", "crunk")
|
|
|
|
assert.Equal(t, "13", Get("id"))
|
|
assert.Equal(t, "apple", Get("f"))
|
|
assert.Equal(t, "Cake", Get("name"))
|
|
|
|
AutomaticEnv()
|
|
|
|
assert.Equal(t, "crunk", Get("name"))
|
|
}
|
|
|
|
func TestAutoEnv(t *testing.T) {
|
|
Reset()
|
|
|
|
AutomaticEnv()
|
|
|
|
t.Setenv("FOO_BAR", "13")
|
|
|
|
assert.Equal(t, "13", Get("foo_bar"))
|
|
}
|
|
|
|
func TestAutoEnvWithPrefix(t *testing.T) {
|
|
Reset()
|
|
|
|
AutomaticEnv()
|
|
SetEnvPrefix("Baz")
|
|
|
|
t.Setenv("BAZ_BAR", "13")
|
|
|
|
assert.Equal(t, "13", Get("bar"))
|
|
}
|
|
|
|
func TestSetEnvKeyReplacer(t *testing.T) {
|
|
Reset()
|
|
|
|
AutomaticEnv()
|
|
|
|
t.Setenv("REFRESH_INTERVAL", "30s")
|
|
|
|
replacer := strings.NewReplacer("-", "_")
|
|
SetEnvKeyReplacer(replacer)
|
|
|
|
assert.Equal(t, "30s", Get("refresh-interval"))
|
|
}
|
|
|
|
func TestEnvKeyReplacer(t *testing.T) {
|
|
v := NewWithOptions(EnvKeyReplacer(strings.NewReplacer("-", "_")))
|
|
|
|
v.AutomaticEnv()
|
|
|
|
t.Setenv("REFRESH_INTERVAL", "30s")
|
|
|
|
assert.Equal(t, "30s", v.Get("refresh-interval"))
|
|
}
|
|
|
|
func TestEnvSubConfig(t *testing.T) {
|
|
initYAML()
|
|
|
|
v.AutomaticEnv()
|
|
|
|
v.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
|
|
|
|
t.Setenv("CLOTHING_PANTS_SIZE", "small")
|
|
subv := v.Sub("clothing").Sub("pants")
|
|
assert.Equal(t, "small", subv.Get("size"))
|
|
|
|
// again with EnvPrefix
|
|
v.SetEnvPrefix("foo") // will be uppercased automatically
|
|
subWithPrefix := v.Sub("clothing").Sub("pants")
|
|
t.Setenv("FOO_CLOTHING_PANTS_SIZE", "large")
|
|
assert.Equal(t, "large", subWithPrefix.Get("size"))
|
|
}
|
|
|
|
func TestAllKeys(t *testing.T) {
|
|
initConfigs()
|
|
|
|
ks := []string{
|
|
"title",
|
|
"author.bio",
|
|
"author.e-mail",
|
|
"author.github",
|
|
"author.name",
|
|
"newkey",
|
|
"owner.organization",
|
|
"owner.dob",
|
|
"owner.bio",
|
|
"name",
|
|
"beard",
|
|
"ppu",
|
|
"batters.batter",
|
|
"hobbies",
|
|
"clothing.jacket",
|
|
"clothing.trousers",
|
|
"default.import_path",
|
|
"default.name",
|
|
"default.version",
|
|
"clothing.pants.size",
|
|
"age",
|
|
"hacker",
|
|
"id",
|
|
"type",
|
|
"eyes",
|
|
"p_id",
|
|
"p_ppu",
|
|
"p_batters.batter.type",
|
|
"p_type",
|
|
"p_name",
|
|
"foos",
|
|
"title_dotenv",
|
|
"type_dotenv",
|
|
"name_dotenv",
|
|
}
|
|
dob, _ := time.Parse(time.RFC3339, "1979-05-27T07:32:00Z")
|
|
all := map[string]any{
|
|
"owner": map[string]any{
|
|
"organization": "MongoDB",
|
|
"bio": "MongoDB Chief Developer Advocate & Hacker at Large",
|
|
"dob": dob,
|
|
},
|
|
"title": "TOML Example",
|
|
"author": map[string]any{
|
|
"e-mail": "fake@localhost",
|
|
"github": "https://github.com/Unknown",
|
|
"name": "Unknown",
|
|
"bio": "Gopher.\nCoding addict.\nGood man.\n",
|
|
},
|
|
"ppu": 0.55,
|
|
"eyes": "brown",
|
|
"clothing": map[string]any{
|
|
"trousers": "denim",
|
|
"jacket": "leather",
|
|
"pants": map[string]any{"size": "large"},
|
|
},
|
|
"default": map[string]any{
|
|
"import_path": "gopkg.in/ini.v1",
|
|
"name": "ini",
|
|
"version": "v1",
|
|
},
|
|
"id": "0001",
|
|
"batters": map[string]any{
|
|
"batter": []any{
|
|
map[string]any{"type": "Regular"},
|
|
map[string]any{"type": "Chocolate"},
|
|
map[string]any{"type": "Blueberry"},
|
|
map[string]any{"type": "Devil's Food"},
|
|
},
|
|
},
|
|
"hacker": true,
|
|
"beard": true,
|
|
"hobbies": []any{
|
|
"skateboarding",
|
|
"snowboarding",
|
|
"go",
|
|
},
|
|
"age": 35,
|
|
"type": "donut",
|
|
"newkey": "remote",
|
|
"name": "Cake",
|
|
"p_id": "0001",
|
|
"p_ppu": "0.55",
|
|
"p_name": "Cake",
|
|
"p_batters": map[string]any{
|
|
"batter": map[string]any{"type": "Regular"},
|
|
},
|
|
"p_type": "donut",
|
|
"foos": []map[string]any{
|
|
{
|
|
"foo": []map[string]any{
|
|
{"key": 1},
|
|
{"key": 2},
|
|
{"key": 3},
|
|
{"key": 4},
|
|
},
|
|
},
|
|
},
|
|
"title_dotenv": "DotEnv Example",
|
|
"type_dotenv": "donut",
|
|
"name_dotenv": "Cake",
|
|
}
|
|
|
|
assert.ElementsMatch(t, ks, AllKeys())
|
|
assert.Equal(t, all, AllSettings())
|
|
}
|
|
|
|
func TestAllKeysWithEnv(t *testing.T) {
|
|
v := New()
|
|
|
|
// bind and define environment variables (including a nested one)
|
|
v.BindEnv("id")
|
|
v.BindEnv("foo.bar")
|
|
v.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
|
|
|
|
t.Setenv("ID", "13")
|
|
t.Setenv("FOO_BAR", "baz")
|
|
|
|
assert.ElementsMatch(t, []string{"id", "foo.bar"}, v.AllKeys())
|
|
}
|
|
|
|
func TestAliasesOfAliases(t *testing.T) {
|
|
Set("Title", "Checking Case")
|
|
RegisterAlias("Foo", "Bar")
|
|
RegisterAlias("Bar", "Title")
|
|
assert.Equal(t, "Checking Case", Get("FOO"))
|
|
}
|
|
|
|
func TestRecursiveAliases(t *testing.T) {
|
|
Set("baz", "bat")
|
|
RegisterAlias("Baz", "Roo")
|
|
RegisterAlias("Roo", "baz")
|
|
assert.Equal(t, "bat", Get("Baz"))
|
|
}
|
|
|
|
func TestUnmarshal(t *testing.T) {
|
|
Reset()
|
|
SetDefault("port", 1313)
|
|
Set("name", "Steve")
|
|
Set("duration", "1s1ms")
|
|
Set("modes", []int{1, 2, 3})
|
|
|
|
type config struct {
|
|
Port int
|
|
Name string
|
|
Duration time.Duration
|
|
Modes []int
|
|
}
|
|
|
|
var C config
|
|
|
|
err := Unmarshal(&C)
|
|
require.NoError(t, err, "unable to decode into struct")
|
|
|
|
assert.Equal(
|
|
t,
|
|
&config{
|
|
Name: "Steve",
|
|
Port: 1313,
|
|
Duration: time.Second + time.Millisecond,
|
|
Modes: []int{1, 2, 3},
|
|
},
|
|
&C,
|
|
)
|
|
|
|
Set("port", 1234)
|
|
err = Unmarshal(&C)
|
|
require.NoError(t, err, "unable to decode into struct")
|
|
|
|
assert.Equal(
|
|
t,
|
|
&config{
|
|
Name: "Steve",
|
|
Port: 1234,
|
|
Duration: time.Second + time.Millisecond,
|
|
Modes: []int{1, 2, 3},
|
|
},
|
|
&C,
|
|
)
|
|
}
|
|
|
|
func TestUnmarshalWithDecoderOptions(t *testing.T) {
|
|
Set("credentials", "{\"foo\":\"bar\"}")
|
|
|
|
opt := DecodeHook(mapstructure.ComposeDecodeHookFunc(
|
|
mapstructure.StringToTimeDurationHookFunc(),
|
|
mapstructure.StringToSliceHookFunc(","),
|
|
// Custom Decode Hook Function
|
|
func(rf reflect.Kind, rt reflect.Kind, data any) (any, error) {
|
|
if rf != reflect.String || rt != reflect.Map {
|
|
return data, nil
|
|
}
|
|
m := map[string]string{}
|
|
raw := data.(string)
|
|
if raw == "" {
|
|
return m, nil
|
|
}
|
|
err := json.Unmarshal([]byte(raw), &m)
|
|
return m, err
|
|
},
|
|
))
|
|
|
|
type config struct {
|
|
Credentials map[string]string
|
|
}
|
|
|
|
var C config
|
|
|
|
err := Unmarshal(&C, opt)
|
|
require.NoError(t, err, "unable to decode into struct")
|
|
|
|
assert.Equal(t, &config{
|
|
Credentials: map[string]string{"foo": "bar"},
|
|
}, &C)
|
|
}
|
|
|
|
func TestUnmarshalWithAutomaticEnv(t *testing.T) {
|
|
if !features.BindStruct {
|
|
t.Skip("binding struct is not enabled")
|
|
}
|
|
|
|
t.Setenv("PORT", "1313")
|
|
t.Setenv("NAME", "Steve")
|
|
t.Setenv("DURATION", "1s1ms")
|
|
t.Setenv("MODES", "1,2,3")
|
|
t.Setenv("SECRET", "42")
|
|
t.Setenv("FILESYSTEM_SIZE", "4096")
|
|
|
|
type AuthConfig struct {
|
|
Secret string `mapstructure:"secret"`
|
|
}
|
|
|
|
type StorageConfig struct {
|
|
Size int `mapstructure:"size"`
|
|
}
|
|
|
|
type Configuration struct {
|
|
Port int `mapstructure:"port"`
|
|
Name string `mapstructure:"name"`
|
|
Duration time.Duration `mapstructure:"duration"`
|
|
|
|
// Infer name from struct
|
|
Modes []int
|
|
|
|
// Squash nested struct (omit prefix)
|
|
Authentication AuthConfig `mapstructure:",squash"`
|
|
|
|
// Different key
|
|
Storage StorageConfig `mapstructure:"filesystem"`
|
|
|
|
// Omitted field
|
|
Flag bool `mapstructure:"flag"`
|
|
}
|
|
|
|
v := New()
|
|
v.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
|
|
v.AutomaticEnv()
|
|
|
|
t.Run("OK", func(t *testing.T) {
|
|
var config Configuration
|
|
if err := v.Unmarshal(&config); err != nil {
|
|
t.Fatalf("unable to decode into struct, %v", err)
|
|
}
|
|
|
|
assert.Equal(
|
|
t,
|
|
Configuration{
|
|
Name: "Steve",
|
|
Port: 1313,
|
|
Duration: time.Second + time.Millisecond,
|
|
Modes: []int{1, 2, 3},
|
|
Authentication: AuthConfig{
|
|
Secret: "42",
|
|
},
|
|
Storage: StorageConfig{
|
|
Size: 4096,
|
|
},
|
|
},
|
|
config,
|
|
)
|
|
})
|
|
|
|
t.Run("Precedence", func(t *testing.T) {
|
|
var config Configuration
|
|
|
|
v.Set("port", 1234)
|
|
if err := v.Unmarshal(&config); err != nil {
|
|
t.Fatalf("unable to decode into struct, %v", err)
|
|
}
|
|
|
|
assert.Equal(
|
|
t,
|
|
Configuration{
|
|
Name: "Steve",
|
|
Port: 1234,
|
|
Duration: time.Second + time.Millisecond,
|
|
Modes: []int{1, 2, 3},
|
|
Authentication: AuthConfig{
|
|
Secret: "42",
|
|
},
|
|
Storage: StorageConfig{
|
|
Size: 4096,
|
|
},
|
|
},
|
|
config,
|
|
)
|
|
})
|
|
|
|
t.Run("Unset", func(t *testing.T) {
|
|
var config Configuration
|
|
|
|
err := v.Unmarshal(&config, func(config *mapstructure.DecoderConfig) {
|
|
config.ErrorUnset = true
|
|
})
|
|
|
|
assert.Error(t, err, "expected viper.Unmarshal to return error due to unset field 'FLAG'")
|
|
})
|
|
|
|
t.Run("Exact", func(t *testing.T) {
|
|
var config Configuration
|
|
|
|
v.Set("port", 1234)
|
|
if err := v.UnmarshalExact(&config); err != nil {
|
|
t.Fatalf("unable to decode into struct, %v", err)
|
|
}
|
|
|
|
assert.Equal(
|
|
t,
|
|
Configuration{
|
|
Name: "Steve",
|
|
Port: 1234,
|
|
Duration: time.Second + time.Millisecond,
|
|
Modes: []int{1, 2, 3},
|
|
Authentication: AuthConfig{
|
|
Secret: "42",
|
|
},
|
|
Storage: StorageConfig{
|
|
Size: 4096,
|
|
},
|
|
},
|
|
config,
|
|
)
|
|
})
|
|
}
|
|
|
|
func TestBindPFlags(t *testing.T) {
|
|
v := New() // create independent Viper object
|
|
flagSet := pflag.NewFlagSet("test", pflag.ContinueOnError)
|
|
|
|
testValues := map[string]*string{
|
|
"host": nil,
|
|
"port": nil,
|
|
"endpoint": nil,
|
|
}
|
|
|
|
mutatedTestValues := map[string]string{
|
|
"host": "localhost",
|
|
"port": "6060",
|
|
"endpoint": "/public",
|
|
}
|
|
|
|
for name := range testValues {
|
|
testValues[name] = flagSet.String(name, "", "test")
|
|
}
|
|
|
|
err := v.BindPFlags(flagSet)
|
|
require.NoError(t, err, "error binding flag set")
|
|
|
|
flagSet.VisitAll(func(flag *pflag.Flag) {
|
|
flag.Value.Set(mutatedTestValues[flag.Name])
|
|
flag.Changed = true
|
|
})
|
|
|
|
for name, expected := range mutatedTestValues {
|
|
assert.Equal(t, expected, v.Get(name))
|
|
}
|
|
}
|
|
|
|
func TestBindPFlagsStringSlice(t *testing.T) {
|
|
tests := []struct {
|
|
Expected []string
|
|
Value string
|
|
}{
|
|
{[]string{}, ""},
|
|
{[]string{"jeden"}, "jeden"},
|
|
{[]string{"dwa", "trzy"}, "dwa,trzy"},
|
|
{[]string{"cztery", "piec , szesc"}, "cztery,\"piec , szesc\""},
|
|
}
|
|
|
|
v := New() // create independent Viper object
|
|
defaultVal := []string{"default"}
|
|
v.SetDefault("stringslice", defaultVal)
|
|
|
|
for _, testValue := range tests {
|
|
flagSet := pflag.NewFlagSet("test", pflag.ContinueOnError)
|
|
flagSet.StringSlice("stringslice", testValue.Expected, "test")
|
|
|
|
for _, changed := range []bool{true, false} {
|
|
flagSet.VisitAll(func(f *pflag.Flag) {
|
|
f.Value.Set(testValue.Value)
|
|
f.Changed = changed
|
|
})
|
|
|
|
err := v.BindPFlags(flagSet)
|
|
require.NoError(t, err, "error binding flag set")
|
|
|
|
type TestStr struct {
|
|
StringSlice []string
|
|
}
|
|
val := &TestStr{}
|
|
err = v.Unmarshal(val)
|
|
require.NoError(t, err, "cannot unmarshal")
|
|
if changed {
|
|
assert.Equal(t, testValue.Expected, val.StringSlice)
|
|
assert.Equal(t, testValue.Expected, v.Get("stringslice"))
|
|
} else {
|
|
assert.Equal(t, defaultVal, val.StringSlice)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestBindPFlagsStringArray(t *testing.T) {
|
|
tests := []struct {
|
|
Expected []string
|
|
Value string
|
|
}{
|
|
{[]string{}, ""},
|
|
{[]string{"jeden"}, "jeden"},
|
|
{[]string{"dwa,trzy"}, "dwa,trzy"},
|
|
{[]string{"cztery,\"piec , szesc\""}, "cztery,\"piec , szesc\""},
|
|
}
|
|
|
|
v := New() // create independent Viper object
|
|
defaultVal := []string{"default"}
|
|
v.SetDefault("stringarray", defaultVal)
|
|
|
|
for _, testValue := range tests {
|
|
flagSet := pflag.NewFlagSet("test", pflag.ContinueOnError)
|
|
flagSet.StringArray("stringarray", testValue.Expected, "test")
|
|
|
|
for _, changed := range []bool{true, false} {
|
|
flagSet.VisitAll(func(f *pflag.Flag) {
|
|
f.Value.Set(testValue.Value)
|
|
f.Changed = changed
|
|
})
|
|
|
|
err := v.BindPFlags(flagSet)
|
|
require.NoError(t, err, "error binding flag set")
|
|
|
|
type TestStr struct {
|
|
StringArray []string
|
|
}
|
|
val := &TestStr{}
|
|
err = v.Unmarshal(val)
|
|
require.NoError(t, err, "cannot unmarshal")
|
|
if changed {
|
|
assert.Equal(t, testValue.Expected, val.StringArray)
|
|
assert.Equal(t, testValue.Expected, v.Get("stringarray"))
|
|
} else {
|
|
assert.Equal(t, defaultVal, val.StringArray)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestSliceFlagsReturnCorrectType(t *testing.T) {
|
|
flagSet := pflag.NewFlagSet("test", pflag.ContinueOnError)
|
|
flagSet.IntSlice("int", []int{1, 2}, "")
|
|
flagSet.StringSlice("str", []string{"3", "4"}, "")
|
|
flagSet.DurationSlice("duration", []time.Duration{5 * time.Second}, "")
|
|
|
|
v := New()
|
|
v.BindPFlags(flagSet)
|
|
|
|
all := v.AllSettings()
|
|
|
|
assert.IsType(t, []int{}, all["int"])
|
|
assert.IsType(t, []string{}, all["str"])
|
|
assert.IsType(t, []time.Duration{}, all["duration"])
|
|
}
|
|
|
|
func TestBindPFlagsIntSlice(t *testing.T) {
|
|
tests := []struct {
|
|
Expected []int
|
|
Value string
|
|
}{
|
|
{[]int{}, ""},
|
|
{[]int{1}, "1"},
|
|
{[]int{2, 3}, "2,3"},
|
|
}
|
|
|
|
v := New() // create independent Viper object
|
|
defaultVal := []int{0}
|
|
v.SetDefault("intslice", defaultVal)
|
|
|
|
for _, testValue := range tests {
|
|
flagSet := pflag.NewFlagSet("test", pflag.ContinueOnError)
|
|
flagSet.IntSlice("intslice", testValue.Expected, "test")
|
|
|
|
for _, changed := range []bool{true, false} {
|
|
flagSet.VisitAll(func(f *pflag.Flag) {
|
|
f.Value.Set(testValue.Value)
|
|
f.Changed = changed
|
|
})
|
|
|
|
err := v.BindPFlags(flagSet)
|
|
require.NoError(t, err, "error binding flag set")
|
|
|
|
type TestInt struct {
|
|
IntSlice []int
|
|
}
|
|
val := &TestInt{}
|
|
err = v.Unmarshal(val)
|
|
require.NoError(t, err, "cannot unmarshal")
|
|
if changed {
|
|
assert.Equal(t, testValue.Expected, val.IntSlice)
|
|
assert.Equal(t, testValue.Expected, v.Get("intslice"))
|
|
} else {
|
|
assert.Equal(t, defaultVal, val.IntSlice)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestBindPFlag(t *testing.T) {
|
|
testString := "testing"
|
|
testValue := newStringValue(testString, &testString)
|
|
|
|
flag := &pflag.Flag{
|
|
Name: "testflag",
|
|
Value: testValue,
|
|
Changed: false,
|
|
}
|
|
|
|
BindPFlag("testvalue", flag)
|
|
|
|
assert.Equal(t, testString, Get("testvalue"))
|
|
|
|
flag.Value.Set("testing_mutate")
|
|
flag.Changed = true // hack for pflag usage
|
|
|
|
assert.Equal(t, "testing_mutate", Get("testvalue"))
|
|
}
|
|
|
|
func TestBindPFlagDetectNilFlag(t *testing.T) {
|
|
result := BindPFlag("testvalue", nil)
|
|
assert.Error(t, result)
|
|
}
|
|
|
|
func TestBindPFlagStringToString(t *testing.T) {
|
|
tests := []struct {
|
|
Expected map[string]string
|
|
Value string
|
|
}{
|
|
{map[string]string{}, ""},
|
|
{map[string]string{"yo": "hi"}, "yo=hi"},
|
|
{map[string]string{"yo": "hi", "oh": "hi=there"}, "yo=hi,oh=hi=there"},
|
|
{map[string]string{"yo": ""}, "yo="},
|
|
{map[string]string{"yo": "", "oh": "hi=there"}, "yo=,oh=hi=there"},
|
|
}
|
|
|
|
v := New() // create independent Viper object
|
|
defaultVal := map[string]string{}
|
|
v.SetDefault("stringtostring", defaultVal)
|
|
|
|
for _, testValue := range tests {
|
|
flagSet := pflag.NewFlagSet("test", pflag.ContinueOnError)
|
|
flagSet.StringToString("stringtostring", testValue.Expected, "test")
|
|
|
|
for _, changed := range []bool{true, false} {
|
|
flagSet.VisitAll(func(f *pflag.Flag) {
|
|
f.Value.Set(testValue.Value)
|
|
f.Changed = changed
|
|
})
|
|
|
|
err := v.BindPFlags(flagSet)
|
|
require.NoError(t, err, "error binding flag set")
|
|
|
|
type TestMap struct {
|
|
StringToString map[string]string
|
|
}
|
|
val := &TestMap{}
|
|
err = v.Unmarshal(val)
|
|
require.NoError(t, err, "cannot unmarshal")
|
|
if changed {
|
|
assert.Equal(t, testValue.Expected, val.StringToString)
|
|
} else {
|
|
assert.Equal(t, defaultVal, val.StringToString)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestBindPFlagStringToInt(t *testing.T) {
|
|
tests := []struct {
|
|
Expected map[string]int
|
|
Value string
|
|
}{
|
|
{map[string]int{"yo": 1, "oh": 21}, "yo=1,oh=21"},
|
|
{map[string]int{"yo": 100000000, "oh": 0}, "yo=100000000,oh=0"},
|
|
{map[string]int{}, "yo=2,oh=21.0"},
|
|
{map[string]int{}, "yo=,oh=20.99"},
|
|
{map[string]int{}, "yo=,oh="},
|
|
}
|
|
|
|
v := New() // create independent Viper object
|
|
defaultVal := map[string]int{}
|
|
v.SetDefault("stringtoint", defaultVal)
|
|
|
|
for _, testValue := range tests {
|
|
flagSet := pflag.NewFlagSet("test", pflag.ContinueOnError)
|
|
flagSet.StringToInt("stringtoint", testValue.Expected, "test")
|
|
|
|
for _, changed := range []bool{true, false} {
|
|
flagSet.VisitAll(func(f *pflag.Flag) {
|
|
f.Value.Set(testValue.Value)
|
|
f.Changed = changed
|
|
})
|
|
|
|
err := v.BindPFlags(flagSet)
|
|
require.NoError(t, err, "error binding flag set")
|
|
|
|
type TestMap struct {
|
|
StringToInt map[string]int
|
|
}
|
|
val := &TestMap{}
|
|
err = v.Unmarshal(val)
|
|
require.NoError(t, err, "cannot unmarshal")
|
|
if changed {
|
|
assert.Equal(t, testValue.Expected, val.StringToInt)
|
|
} else {
|
|
assert.Equal(t, defaultVal, val.StringToInt)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestBoundCaseSensitivity(t *testing.T) {
|
|
initConfigs()
|
|
assert.Equal(t, "brown", Get("eyes"))
|
|
|
|
BindEnv("eYEs", "TURTLE_EYES")
|
|
|
|
t.Setenv("TURTLE_EYES", "blue")
|
|
|
|
assert.Equal(t, "blue", Get("eyes"))
|
|
|
|
testString := "green"
|
|
testValue := newStringValue(testString, &testString)
|
|
|
|
flag := &pflag.Flag{
|
|
Name: "eyeballs",
|
|
Value: testValue,
|
|
Changed: true,
|
|
}
|
|
|
|
BindPFlag("eYEs", flag)
|
|
assert.Equal(t, "green", Get("eyes"))
|
|
}
|
|
|
|
func TestSizeInBytes(t *testing.T) {
|
|
input := map[string]uint{
|
|
"": 0,
|
|
"b": 0,
|
|
"12 bytes": 0,
|
|
"200000000000gb": 0,
|
|
"12 b": 12,
|
|
"43 MB": 43 * (1 << 20),
|
|
"10mb": 10 * (1 << 20),
|
|
"1gb": 1 << 30,
|
|
}
|
|
|
|
for str, expected := range input {
|
|
assert.Equal(t, expected, parseSizeInBytes(str), str)
|
|
}
|
|
}
|
|
|
|
func TestFindsNestedKeys(t *testing.T) {
|
|
initConfigs()
|
|
dob, _ := time.Parse(time.RFC3339, "1979-05-27T07:32:00Z")
|
|
|
|
Set("super", map[string]any{
|
|
"deep": map[string]any{
|
|
"nested": "value",
|
|
},
|
|
})
|
|
|
|
expected := map[string]any{
|
|
"super": map[string]any{
|
|
"deep": map[string]any{
|
|
"nested": "value",
|
|
},
|
|
},
|
|
"super.deep": map[string]any{
|
|
"nested": "value",
|
|
},
|
|
"super.deep.nested": "value",
|
|
"owner.organization": "MongoDB",
|
|
"batters.batter": []any{
|
|
map[string]any{
|
|
"type": "Regular",
|
|
},
|
|
map[string]any{
|
|
"type": "Chocolate",
|
|
},
|
|
map[string]any{
|
|
"type": "Blueberry",
|
|
},
|
|
map[string]any{
|
|
"type": "Devil's Food",
|
|
},
|
|
},
|
|
"hobbies": []any{
|
|
"skateboarding", "snowboarding", "go",
|
|
},
|
|
"TITLE_DOTENV": "DotEnv Example",
|
|
"TYPE_DOTENV": "donut",
|
|
"NAME_DOTENV": "Cake",
|
|
"title": "TOML Example",
|
|
"newkey": "remote",
|
|
"batters": map[string]any{
|
|
"batter": []any{
|
|
map[string]any{
|
|
"type": "Regular",
|
|
},
|
|
map[string]any{
|
|
"type": "Chocolate",
|
|
},
|
|
map[string]any{
|
|
"type": "Blueberry",
|
|
},
|
|
map[string]any{
|
|
"type": "Devil's Food",
|
|
},
|
|
},
|
|
},
|
|
"eyes": "brown",
|
|
"age": 35,
|
|
"owner": map[string]any{
|
|
"organization": "MongoDB",
|
|
"bio": "MongoDB Chief Developer Advocate & Hacker at Large",
|
|
"dob": dob,
|
|
},
|
|
"owner.bio": "MongoDB Chief Developer Advocate & Hacker at Large",
|
|
"type": "donut",
|
|
"id": "0001",
|
|
"name": "Cake",
|
|
"hacker": true,
|
|
"ppu": 0.55,
|
|
"clothing": map[string]any{
|
|
"jacket": "leather",
|
|
"trousers": "denim",
|
|
"pants": map[string]any{
|
|
"size": "large",
|
|
},
|
|
},
|
|
"clothing.jacket": "leather",
|
|
"clothing.pants.size": "large",
|
|
"clothing.trousers": "denim",
|
|
"owner.dob": dob,
|
|
"beard": true,
|
|
"foos": []map[string]any{
|
|
{
|
|
"foo": []map[string]any{
|
|
{
|
|
"key": 1,
|
|
},
|
|
{
|
|
"key": 2,
|
|
},
|
|
{
|
|
"key": 3,
|
|
},
|
|
{
|
|
"key": 4,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
for key, expectedValue := range expected {
|
|
assert.Equal(t, expectedValue, v.Get(key))
|
|
}
|
|
}
|
|
|
|
func TestReadBufConfig(t *testing.T) {
|
|
v := New()
|
|
v.SetConfigType("yaml")
|
|
v.ReadConfig(bytes.NewBuffer(yamlExample))
|
|
t.Log(v.AllKeys())
|
|
|
|
assert.True(t, v.InConfig("name"))
|
|
assert.True(t, v.InConfig("clothing.jacket"))
|
|
assert.False(t, v.InConfig("state"))
|
|
assert.False(t, v.InConfig("clothing.hat"))
|
|
assert.Equal(t, "steve", v.Get("name"))
|
|
assert.Equal(t, []any{"skateboarding", "snowboarding", "go"}, v.Get("hobbies"))
|
|
assert.Equal(t, map[string]any{"jacket": "leather", "trousers": "denim", "pants": map[string]any{"size": "large"}}, v.Get("clothing"))
|
|
assert.Equal(t, 35, v.Get("age"))
|
|
}
|
|
|
|
func TestIsSet(t *testing.T) {
|
|
v := New()
|
|
v.SetConfigType("yaml")
|
|
|
|
/* config and defaults */
|
|
v.ReadConfig(bytes.NewBuffer(yamlExample))
|
|
v.SetDefault("clothing.shoes", "sneakers")
|
|
|
|
assert.True(t, v.IsSet("clothing"))
|
|
assert.True(t, v.IsSet("clothing.jacket"))
|
|
assert.False(t, v.IsSet("clothing.jackets"))
|
|
assert.True(t, v.IsSet("clothing.shoes"))
|
|
|
|
/* state change */
|
|
assert.False(t, v.IsSet("helloworld"))
|
|
v.Set("helloworld", "fubar")
|
|
assert.True(t, v.IsSet("helloworld"))
|
|
|
|
/* env */
|
|
v.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
|
|
v.BindEnv("eyes")
|
|
v.BindEnv("foo")
|
|
v.BindEnv("clothing.hat")
|
|
v.BindEnv("clothing.hats")
|
|
|
|
t.Setenv("FOO", "bar")
|
|
t.Setenv("CLOTHING_HAT", "bowler")
|
|
|
|
assert.True(t, v.IsSet("eyes")) // in the config file
|
|
assert.True(t, v.IsSet("foo")) // in the environment
|
|
assert.True(t, v.IsSet("clothing.hat")) // in the environment
|
|
assert.False(t, v.IsSet("clothing.hats")) // not defined
|
|
|
|
/* flags */
|
|
flagset := pflag.NewFlagSet("testisset", pflag.ContinueOnError)
|
|
flagset.Bool("foobaz", false, "foobaz")
|
|
flagset.Bool("barbaz", false, "barbaz")
|
|
foobaz, barbaz := flagset.Lookup("foobaz"), flagset.Lookup("barbaz")
|
|
v.BindPFlag("foobaz", foobaz)
|
|
v.BindPFlag("barbaz", barbaz)
|
|
barbaz.Value.Set("true")
|
|
barbaz.Changed = true // hack for pflag usage
|
|
|
|
assert.False(t, v.IsSet("foobaz"))
|
|
assert.True(t, v.IsSet("barbaz"))
|
|
}
|
|
|
|
func TestDirsSearch(t *testing.T) {
|
|
root, config := initDirs(t)
|
|
|
|
v := New()
|
|
v.SetConfigName(config)
|
|
v.SetDefault(`key`, `default`)
|
|
|
|
entries, err := os.ReadDir(root)
|
|
require.NoError(t, err)
|
|
for _, e := range entries {
|
|
if e.IsDir() {
|
|
v.AddConfigPath(filepath.Join(root, e.Name()))
|
|
}
|
|
}
|
|
|
|
err = v.ReadInConfig()
|
|
require.NoError(t, err)
|
|
|
|
assert.Equal(t, `value is `+filepath.Base(v.configPaths[0]), v.GetString(`key`))
|
|
}
|
|
|
|
func TestWrongDirsSearchNotFound(t *testing.T) {
|
|
_, config := initDirs(t)
|
|
|
|
v := New()
|
|
v.SetConfigName(config)
|
|
v.SetDefault(`key`, `default`)
|
|
|
|
v.AddConfigPath(`whattayoutalkingbout`)
|
|
v.AddConfigPath(`thispathaintthere`)
|
|
|
|
err := v.ReadInConfig()
|
|
assert.IsType(t, err, ConfigFileNotFoundError{"", ""})
|
|
|
|
// Even though config did not load and the error might have
|
|
// been ignored by the client, the default still loads
|
|
assert.Equal(t, `default`, v.GetString(`key`))
|
|
}
|
|
|
|
func TestWrongDirsSearchNotFoundForMerge(t *testing.T) {
|
|
_, config := initDirs(t)
|
|
|
|
v := New()
|
|
v.SetConfigName(config)
|
|
v.SetDefault(`key`, `default`)
|
|
|
|
v.AddConfigPath(`whattayoutalkingbout`)
|
|
v.AddConfigPath(`thispathaintthere`)
|
|
|
|
err := v.MergeInConfig()
|
|
assert.Equal(t, reflect.TypeOf(ConfigFileNotFoundError{"", ""}), reflect.TypeOf(err))
|
|
|
|
// Even though config did not load and the error might have
|
|
// been ignored by the client, the default still loads
|
|
assert.Equal(t, `default`, v.GetString(`key`))
|
|
}
|
|
|
|
var yamlInvalid = []byte(`hash: map
|
|
- foo
|
|
- bar
|
|
`)
|
|
|
|
func TestUnwrapParseErrors(t *testing.T) {
|
|
SetConfigType("yaml")
|
|
assert.ErrorAs(t, ReadConfig(bytes.NewBuffer(yamlInvalid)), &ConfigParseError{})
|
|
}
|
|
|
|
func TestSub(t *testing.T) {
|
|
v := New()
|
|
v.SetConfigType("yaml")
|
|
v.ReadConfig(bytes.NewBuffer(yamlExample))
|
|
|
|
subv := v.Sub("clothing")
|
|
assert.Equal(t, v.Get("clothing.pants.size"), subv.Get("pants.size"))
|
|
|
|
subv = v.Sub("clothing.pants")
|
|
assert.Equal(t, v.Get("clothing.pants.size"), subv.Get("size"))
|
|
|
|
subv = v.Sub("clothing.pants.size")
|
|
assert.Equal(t, (*Viper)(nil), subv)
|
|
|
|
subv = v.Sub("missing.key")
|
|
assert.Equal(t, (*Viper)(nil), subv)
|
|
|
|
subv = v.Sub("clothing")
|
|
assert.Equal(t, []string{"clothing"}, subv.parents)
|
|
|
|
subv = v.Sub("clothing").Sub("pants")
|
|
assert.Equal(t, []string{"clothing", "pants"}, subv.parents)
|
|
}
|
|
|
|
var hclWriteExpected = []byte(`"foos" = {
|
|
"foo" = {
|
|
"key" = 1
|
|
}
|
|
|
|
"foo" = {
|
|
"key" = 2
|
|
}
|
|
|
|
"foo" = {
|
|
"key" = 3
|
|
}
|
|
|
|
"foo" = {
|
|
"key" = 4
|
|
}
|
|
}
|
|
|
|
"id" = "0001"
|
|
|
|
"name" = "Cake"
|
|
|
|
"ppu" = 0.55
|
|
|
|
"type" = "donut"`)
|
|
|
|
var jsonWriteExpected = []byte(`{
|
|
"batters": {
|
|
"batter": [
|
|
{
|
|
"type": "Regular"
|
|
},
|
|
{
|
|
"type": "Chocolate"
|
|
},
|
|
{
|
|
"type": "Blueberry"
|
|
},
|
|
{
|
|
"type": "Devil's Food"
|
|
}
|
|
]
|
|
},
|
|
"id": "0001",
|
|
"name": "Cake",
|
|
"ppu": 0.55,
|
|
"type": "donut"
|
|
}`)
|
|
|
|
var propertiesWriteExpected = []byte(`p_id = 0001
|
|
p_type = donut
|
|
p_name = Cake
|
|
p_ppu = 0.55
|
|
p_batters.batter.type = Regular
|
|
`)
|
|
|
|
// var yamlWriteExpected = []byte(`age: 35
|
|
// beard: true
|
|
// clothing:
|
|
// jacket: leather
|
|
// pants:
|
|
// size: large
|
|
// trousers: denim
|
|
// eyes: brown
|
|
// hacker: true
|
|
// hobbies:
|
|
// - skateboarding
|
|
// - snowboarding
|
|
// - go
|
|
// name: steve
|
|
// `)
|
|
|
|
func TestWriteConfig(t *testing.T) {
|
|
fs := afero.NewMemMapFs()
|
|
testCases := map[string]struct {
|
|
configName string
|
|
inConfigType string
|
|
outConfigType string
|
|
fileName string
|
|
input []byte
|
|
expectedContent []byte
|
|
}{
|
|
"hcl with file extension": {
|
|
configName: "c",
|
|
inConfigType: "hcl",
|
|
outConfigType: "hcl",
|
|
fileName: "c.hcl",
|
|
input: hclExample,
|
|
expectedContent: hclWriteExpected,
|
|
},
|
|
"hcl without file extension": {
|
|
configName: "c",
|
|
inConfigType: "hcl",
|
|
outConfigType: "hcl",
|
|
fileName: "c",
|
|
input: hclExample,
|
|
expectedContent: hclWriteExpected,
|
|
},
|
|
"hcl with file extension and mismatch type": {
|
|
configName: "c",
|
|
inConfigType: "hcl",
|
|
outConfigType: "json",
|
|
fileName: "c.hcl",
|
|
input: hclExample,
|
|
expectedContent: hclWriteExpected,
|
|
},
|
|
"json with file extension": {
|
|
configName: "c",
|
|
inConfigType: "json",
|
|
outConfigType: "json",
|
|
fileName: "c.json",
|
|
input: jsonExample,
|
|
expectedContent: jsonWriteExpected,
|
|
},
|
|
"json without file extension": {
|
|
configName: "c",
|
|
inConfigType: "json",
|
|
outConfigType: "json",
|
|
fileName: "c",
|
|
input: jsonExample,
|
|
expectedContent: jsonWriteExpected,
|
|
},
|
|
"json with file extension and mismatch type": {
|
|
configName: "c",
|
|
inConfigType: "json",
|
|
outConfigType: "hcl",
|
|
fileName: "c.json",
|
|
input: jsonExample,
|
|
expectedContent: jsonWriteExpected,
|
|
},
|
|
"properties with file extension": {
|
|
configName: "c",
|
|
inConfigType: "properties",
|
|
outConfigType: "properties",
|
|
fileName: "c.properties",
|
|
input: propertiesExample,
|
|
expectedContent: propertiesWriteExpected,
|
|
},
|
|
"properties without file extension": {
|
|
configName: "c",
|
|
inConfigType: "properties",
|
|
outConfigType: "properties",
|
|
fileName: "c",
|
|
input: propertiesExample,
|
|
expectedContent: propertiesWriteExpected,
|
|
},
|
|
"yaml with file extension": {
|
|
configName: "c",
|
|
inConfigType: "yaml",
|
|
outConfigType: "yaml",
|
|
fileName: "c.yaml",
|
|
input: yamlExample,
|
|
expectedContent: yamlWriteExpected,
|
|
},
|
|
"yaml without file extension": {
|
|
configName: "c",
|
|
inConfigType: "yaml",
|
|
outConfigType: "yaml",
|
|
fileName: "c",
|
|
input: yamlExample,
|
|
expectedContent: yamlWriteExpected,
|
|
},
|
|
"yaml with file extension and mismatch type": {
|
|
configName: "c",
|
|
inConfigType: "yaml",
|
|
outConfigType: "json",
|
|
fileName: "c.yaml",
|
|
input: yamlExample,
|
|
expectedContent: yamlWriteExpected,
|
|
},
|
|
}
|
|
for name, tc := range testCases {
|
|
t.Run(name, func(t *testing.T) {
|
|
v := New()
|
|
v.SetFs(fs)
|
|
v.SetConfigName(tc.fileName)
|
|
v.SetConfigType(tc.inConfigType)
|
|
|
|
err := v.ReadConfig(bytes.NewBuffer(tc.input))
|
|
require.NoError(t, err)
|
|
v.SetConfigType(tc.outConfigType)
|
|
err = v.WriteConfigAs(tc.fileName)
|
|
require.NoError(t, err)
|
|
read, err := afero.ReadFile(fs, tc.fileName)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, tc.expectedContent, read)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestWriteConfigTOML(t *testing.T) {
|
|
fs := afero.NewMemMapFs()
|
|
|
|
testCases := map[string]struct {
|
|
configName string
|
|
configType string
|
|
fileName string
|
|
input []byte
|
|
}{
|
|
"with file extension": {
|
|
configName: "c",
|
|
configType: "toml",
|
|
fileName: "c.toml",
|
|
input: tomlExample,
|
|
},
|
|
"without file extension": {
|
|
configName: "c",
|
|
configType: "toml",
|
|
fileName: "c",
|
|
input: tomlExample,
|
|
},
|
|
}
|
|
for name, tc := range testCases {
|
|
t.Run(name, func(t *testing.T) {
|
|
v := New()
|
|
v.SetFs(fs)
|
|
v.SetConfigName(tc.configName)
|
|
v.SetConfigType(tc.configType)
|
|
err := v.ReadConfig(bytes.NewBuffer(tc.input))
|
|
require.NoError(t, err)
|
|
err = v.WriteConfigAs(tc.fileName)
|
|
require.NoError(t, err)
|
|
|
|
// The TOML String method does not order the contents.
|
|
// Therefore, we must read the generated file and compare the data.
|
|
v2 := New()
|
|
v2.SetFs(fs)
|
|
v2.SetConfigName(tc.configName)
|
|
v2.SetConfigType(tc.configType)
|
|
v2.SetConfigFile(tc.fileName)
|
|
err = v2.ReadInConfig()
|
|
require.NoError(t, err)
|
|
|
|
assert.Equal(t, v.GetString("title"), v2.GetString("title"))
|
|
assert.Equal(t, v.GetString("owner.bio"), v2.GetString("owner.bio"))
|
|
assert.Equal(t, v.GetString("owner.dob"), v2.GetString("owner.dob"))
|
|
assert.Equal(t, v.GetString("owner.organization"), v2.GetString("owner.organization"))
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestWriteConfigDotEnv(t *testing.T) {
|
|
fs := afero.NewMemMapFs()
|
|
testCases := map[string]struct {
|
|
configName string
|
|
configType string
|
|
fileName string
|
|
input []byte
|
|
}{
|
|
"with file extension": {
|
|
configName: "c",
|
|
configType: "env",
|
|
fileName: "c.env",
|
|
input: dotenvExample,
|
|
},
|
|
"without file extension": {
|
|
configName: "c",
|
|
configType: "env",
|
|
fileName: "c",
|
|
input: dotenvExample,
|
|
},
|
|
}
|
|
for name, tc := range testCases {
|
|
t.Run(name, func(t *testing.T) {
|
|
v := New()
|
|
v.SetFs(fs)
|
|
v.SetConfigName(tc.configName)
|
|
v.SetConfigType(tc.configType)
|
|
err := v.ReadConfig(bytes.NewBuffer(tc.input))
|
|
require.NoError(t, err)
|
|
err = v.WriteConfigAs(tc.fileName)
|
|
require.NoError(t, err)
|
|
|
|
// The TOML String method does not order the contents.
|
|
// Therefore, we must read the generated file and compare the data.
|
|
v2 := New()
|
|
v2.SetFs(fs)
|
|
v2.SetConfigName(tc.configName)
|
|
v2.SetConfigType(tc.configType)
|
|
v2.SetConfigFile(tc.fileName)
|
|
err = v2.ReadInConfig()
|
|
require.NoError(t, err)
|
|
|
|
assert.Equal(t, v.GetString("title_dotenv"), v2.GetString("title_dotenv"))
|
|
assert.Equal(t, v.GetString("type_dotenv"), v2.GetString("type_dotenv"))
|
|
assert.Equal(t, v.GetString("kind_dotenv"), v2.GetString("kind_dotenv"))
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestSafeWriteConfig(t *testing.T) {
|
|
v := New()
|
|
fs := afero.NewMemMapFs()
|
|
v.SetFs(fs)
|
|
v.AddConfigPath("/test")
|
|
v.SetConfigName("c")
|
|
v.SetConfigType("yaml")
|
|
require.NoError(t, v.ReadConfig(bytes.NewBuffer(yamlExample)))
|
|
require.NoError(t, v.SafeWriteConfig())
|
|
read, err := afero.ReadFile(fs, testutil.AbsFilePath(t, "/test/c.yaml"))
|
|
require.NoError(t, err)
|
|
assert.Equal(t, yamlWriteExpected, read)
|
|
}
|
|
|
|
func TestSafeWriteConfigWithMissingConfigPath(t *testing.T) {
|
|
v := New()
|
|
fs := afero.NewMemMapFs()
|
|
v.SetFs(fs)
|
|
v.SetConfigName("c")
|
|
v.SetConfigType("yaml")
|
|
require.EqualError(t, v.SafeWriteConfig(), "missing configuration for 'configPath'")
|
|
}
|
|
|
|
func TestSafeWriteConfigWithExistingFile(t *testing.T) {
|
|
v := New()
|
|
fs := afero.NewMemMapFs()
|
|
fs.Create(testutil.AbsFilePath(t, "/test/c.yaml"))
|
|
v.SetFs(fs)
|
|
v.AddConfigPath("/test")
|
|
v.SetConfigName("c")
|
|
v.SetConfigType("yaml")
|
|
err := v.SafeWriteConfig()
|
|
require.Error(t, err)
|
|
_, ok := err.(ConfigFileAlreadyExistsError)
|
|
assert.True(t, ok, "Expected ConfigFileAlreadyExistsError")
|
|
}
|
|
|
|
func TestSafeWriteAsConfig(t *testing.T) {
|
|
v := New()
|
|
fs := afero.NewMemMapFs()
|
|
v.SetFs(fs)
|
|
err := v.ReadConfig(bytes.NewBuffer(yamlExample))
|
|
require.NoError(t, err)
|
|
require.NoError(t, v.SafeWriteConfigAs("/test/c.yaml"))
|
|
_, err = afero.ReadFile(fs, "/test/c.yaml")
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
func TestSafeWriteConfigAsWithExistingFile(t *testing.T) {
|
|
v := New()
|
|
fs := afero.NewMemMapFs()
|
|
fs.Create("/test/c.yaml")
|
|
v.SetFs(fs)
|
|
err := v.SafeWriteConfigAs("/test/c.yaml")
|
|
require.Error(t, err)
|
|
_, ok := err.(ConfigFileAlreadyExistsError)
|
|
assert.True(t, ok, "Expected ConfigFileAlreadyExistsError")
|
|
}
|
|
|
|
func TestWriteHiddenFile(t *testing.T) {
|
|
v := New()
|
|
fs := afero.NewMemMapFs()
|
|
fs.Create(testutil.AbsFilePath(t, "/test/.config"))
|
|
v.SetFs(fs)
|
|
|
|
v.SetConfigName(".config")
|
|
v.SetConfigType("yaml")
|
|
v.AddConfigPath("/test")
|
|
|
|
err := v.ReadInConfig()
|
|
require.NoError(t, err)
|
|
|
|
err = v.WriteConfig()
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
var yamlMergeExampleTgt = []byte(`
|
|
hello:
|
|
pop: 37890
|
|
largenum: 765432101234567
|
|
num2pow63: 9223372036854775808
|
|
universe: null
|
|
world:
|
|
- us
|
|
- uk
|
|
- fr
|
|
- de
|
|
`)
|
|
|
|
var yamlMergeExampleSrc = []byte(`
|
|
hello:
|
|
pop: 45000
|
|
largenum: 7654321001234567
|
|
universe:
|
|
- mw
|
|
- ad
|
|
ints:
|
|
- 1
|
|
- 2
|
|
fu: bar
|
|
`)
|
|
|
|
var jsonMergeExampleTgt = []byte(`
|
|
{
|
|
"hello": {
|
|
"foo": null,
|
|
"pop": 123456
|
|
}
|
|
}
|
|
`)
|
|
|
|
var jsonMergeExampleSrc = []byte(`
|
|
{
|
|
"hello": {
|
|
"foo": "foo str",
|
|
"pop": "pop str"
|
|
}
|
|
}
|
|
`)
|
|
|
|
func TestMergeConfig(t *testing.T) {
|
|
v := New()
|
|
v.SetConfigType("yml")
|
|
err := v.ReadConfig(bytes.NewBuffer(yamlMergeExampleTgt))
|
|
require.NoError(t, err)
|
|
|
|
assert.Equal(t, 37890, v.GetInt("hello.pop"))
|
|
assert.Equal(t, int32(37890), v.GetInt32("hello.pop"))
|
|
assert.Equal(t, int64(765432101234567), v.GetInt64("hello.largenum"))
|
|
assert.Equal(t, uint(37890), v.GetUint("hello.pop"))
|
|
assert.Equal(t, uint16(37890), v.GetUint16("hello.pop"))
|
|
assert.Equal(t, uint32(37890), v.GetUint32("hello.pop"))
|
|
assert.Equal(t, uint64(9223372036854775808), v.GetUint64("hello.num2pow63"))
|
|
assert.Len(t, v.GetStringSlice("hello.world"), 4)
|
|
assert.Empty(t, v.GetString("fu"))
|
|
|
|
err = v.MergeConfig(bytes.NewBuffer(yamlMergeExampleSrc))
|
|
require.NoError(t, err)
|
|
|
|
assert.Equal(t, 45000, v.GetInt("hello.pop"))
|
|
assert.Equal(t, int32(45000), v.GetInt32("hello.pop"))
|
|
assert.Equal(t, int64(7654321001234567), v.GetInt64("hello.largenum"))
|
|
assert.Len(t, v.GetStringSlice("hello.world"), 4)
|
|
assert.Len(t, v.GetStringSlice("hello.universe"), 2)
|
|
assert.Len(t, v.GetIntSlice("hello.ints"), 2)
|
|
assert.Equal(t, "bar", v.GetString("fu"))
|
|
}
|
|
|
|
func TestMergeConfigOverrideType(t *testing.T) {
|
|
v := New()
|
|
v.SetConfigType("json")
|
|
err := v.ReadConfig(bytes.NewBuffer(jsonMergeExampleTgt))
|
|
require.NoError(t, err)
|
|
|
|
err = v.MergeConfig(bytes.NewBuffer(jsonMergeExampleSrc))
|
|
require.NoError(t, err)
|
|
|
|
assert.Equal(t, "pop str", v.GetString("hello.pop"))
|
|
assert.Equal(t, "foo str", v.GetString("hello.foo"))
|
|
}
|
|
|
|
func TestMergeConfigNoMerge(t *testing.T) {
|
|
v := New()
|
|
v.SetConfigType("yml")
|
|
err := v.ReadConfig(bytes.NewBuffer(yamlMergeExampleTgt))
|
|
require.NoError(t, err)
|
|
|
|
assert.Equal(t, 37890, v.GetInt("hello.pop"))
|
|
assert.Len(t, v.GetStringSlice("hello.world"), 4)
|
|
assert.Empty(t, v.GetString("fu"))
|
|
|
|
err = v.ReadConfig(bytes.NewBuffer(yamlMergeExampleSrc))
|
|
require.NoError(t, err)
|
|
|
|
assert.Equal(t, 45000, v.GetInt("hello.pop"))
|
|
assert.Empty(t, v.GetStringSlice("hello.world"))
|
|
assert.Len(t, v.GetStringSlice("hello.universe"), 2)
|
|
assert.Len(t, v.GetIntSlice("hello.ints"), 2)
|
|
assert.Equal(t, "bar", v.GetString("fu"))
|
|
}
|
|
|
|
func TestMergeConfigMap(t *testing.T) {
|
|
v := New()
|
|
v.SetConfigType("yml")
|
|
err := v.ReadConfig(bytes.NewBuffer(yamlMergeExampleTgt))
|
|
require.NoError(t, err)
|
|
|
|
assertFn := func(i int) {
|
|
large := v.GetInt64("hello.largenum")
|
|
pop := v.GetInt("hello.pop")
|
|
assert.Equal(t, int64(765432101234567), large)
|
|
assert.Equal(t, i, pop)
|
|
}
|
|
|
|
assertFn(37890)
|
|
|
|
update := map[string]any{
|
|
"Hello": map[string]any{
|
|
"Pop": 1234,
|
|
},
|
|
"World": map[any]any{
|
|
"Rock": 345,
|
|
},
|
|
}
|
|
|
|
err = v.MergeConfigMap(update)
|
|
require.NoError(t, err)
|
|
|
|
assert.Equal(t, 345, v.GetInt("world.rock"))
|
|
|
|
assertFn(1234)
|
|
}
|
|
|
|
func TestUnmarshalingWithAliases(t *testing.T) {
|
|
v := New()
|
|
v.SetDefault("ID", 1)
|
|
v.Set("name", "Steve")
|
|
v.Set("lastname", "Owen")
|
|
|
|
v.RegisterAlias("UserID", "ID")
|
|
v.RegisterAlias("Firstname", "name")
|
|
v.RegisterAlias("Surname", "lastname")
|
|
|
|
type config struct {
|
|
ID int
|
|
FirstName string
|
|
Surname string
|
|
}
|
|
|
|
var C config
|
|
err := v.Unmarshal(&C)
|
|
require.NoError(t, err, "unable to decode into struct")
|
|
|
|
assert.Equal(t, &config{ID: 1, FirstName: "Steve", Surname: "Owen"}, &C)
|
|
}
|
|
|
|
func TestSetConfigNameClearsFileCache(t *testing.T) {
|
|
SetConfigFile("/tmp/config.yaml")
|
|
SetConfigName("default")
|
|
f, err := v.getConfigFile()
|
|
require.Error(t, err, "config file cache should have been cleared")
|
|
assert.Empty(t, f)
|
|
}
|
|
|
|
func TestShadowedNestedValue(t *testing.T) {
|
|
config := `name: steve
|
|
clothing:
|
|
jacket: leather
|
|
trousers: denim
|
|
pants:
|
|
size: large
|
|
`
|
|
initConfig("yaml", config)
|
|
|
|
assert.Equal(t, "steve", GetString("name"))
|
|
|
|
polyester := "polyester"
|
|
SetDefault("clothing.shirt", polyester)
|
|
SetDefault("clothing.jacket.price", 100)
|
|
|
|
assert.Equal(t, "leather", GetString("clothing.jacket"))
|
|
assert.Nil(t, Get("clothing.jacket.price"))
|
|
assert.Equal(t, polyester, GetString("clothing.shirt"))
|
|
|
|
clothingSettings := AllSettings()["clothing"].(map[string]any)
|
|
assert.Equal(t, "leather", clothingSettings["jacket"])
|
|
assert.Equal(t, polyester, clothingSettings["shirt"])
|
|
}
|
|
|
|
func TestDotParameter(t *testing.T) {
|
|
initJSON()
|
|
// should take precedence over batters defined in jsonExample
|
|
r := bytes.NewReader([]byte(`{ "batters.batter": [ { "type": "Small" } ] }`))
|
|
unmarshalReader(r, v.config)
|
|
|
|
actual := Get("batters.batter")
|
|
expected := []any{map[string]any{"type": "Small"}}
|
|
assert.Equal(t, expected, actual)
|
|
}
|
|
|
|
func TestCaseInsensitive(t *testing.T) {
|
|
for _, config := range []struct {
|
|
typ string
|
|
content string
|
|
}{
|
|
{"yaml", `
|
|
aBcD: 1
|
|
eF:
|
|
gH: 2
|
|
iJk: 3
|
|
Lm:
|
|
nO: 4
|
|
P:
|
|
Q: 5
|
|
R: 6
|
|
`},
|
|
{"json", `{
|
|
"aBcD": 1,
|
|
"eF": {
|
|
"iJk": 3,
|
|
"Lm": {
|
|
"P": {
|
|
"Q": 5,
|
|
"R": 6
|
|
},
|
|
"nO": 4
|
|
},
|
|
"gH": 2
|
|
}
|
|
}`},
|
|
{"toml", `aBcD = 1
|
|
[eF]
|
|
gH = 2
|
|
iJk = 3
|
|
[eF.Lm]
|
|
nO = 4
|
|
[eF.Lm.P]
|
|
Q = 5
|
|
R = 6
|
|
`},
|
|
} {
|
|
doTestCaseInsensitive(t, config.typ, config.content)
|
|
}
|
|
}
|
|
|
|
func TestCaseInsensitiveSet(t *testing.T) {
|
|
Reset()
|
|
m1 := map[string]any{
|
|
"Foo": 32,
|
|
"Bar": map[any]any{
|
|
"ABc": "A",
|
|
"cDE": "B",
|
|
},
|
|
}
|
|
|
|
m2 := map[string]any{
|
|
"Foo": 52,
|
|
"Bar": map[any]any{
|
|
"bCd": "A",
|
|
"eFG": "B",
|
|
},
|
|
}
|
|
|
|
Set("Given1", m1)
|
|
Set("Number1", 42)
|
|
|
|
SetDefault("Given2", m2)
|
|
SetDefault("Number2", 52)
|
|
|
|
// Verify SetDefault
|
|
assert.Equal(t, 52, Get("number2"))
|
|
assert.Equal(t, 52, Get("given2.foo"))
|
|
assert.Equal(t, "A", Get("given2.bar.bcd"))
|
|
_, ok := m2["Foo"]
|
|
assert.True(t, ok)
|
|
|
|
// Verify Set
|
|
assert.Equal(t, 42, Get("number1"))
|
|
assert.Equal(t, 32, Get("given1.foo"))
|
|
assert.Equal(t, "A", Get("given1.bar.abc"))
|
|
_, ok = m1["Foo"]
|
|
assert.True(t, ok)
|
|
}
|
|
|
|
func TestParseNested(t *testing.T) {
|
|
type duration struct {
|
|
Delay time.Duration
|
|
}
|
|
|
|
type item struct {
|
|
Name string
|
|
Delay time.Duration
|
|
Nested duration
|
|
}
|
|
|
|
config := `[[parent]]
|
|
delay="100ms"
|
|
[parent.nested]
|
|
delay="200ms"
|
|
`
|
|
initConfig("toml", config)
|
|
|
|
var items []item
|
|
err := v.UnmarshalKey("parent", &items)
|
|
require.NoError(t, err, "unable to decode into struct")
|
|
|
|
assert.Len(t, items, 1)
|
|
assert.Equal(t, 100*time.Millisecond, items[0].Delay)
|
|
assert.Equal(t, 200*time.Millisecond, items[0].Nested.Delay)
|
|
}
|
|
|
|
func doTestCaseInsensitive(t *testing.T, typ, config string) {
|
|
initConfig(typ, config)
|
|
Set("RfD", true)
|
|
assert.Equal(t, true, Get("rfd"))
|
|
assert.Equal(t, true, Get("rFD"))
|
|
assert.Equal(t, 1, cast.ToInt(Get("abcd")))
|
|
assert.Equal(t, 1, cast.ToInt(Get("Abcd")))
|
|
assert.Equal(t, 2, cast.ToInt(Get("ef.gh")))
|
|
assert.Equal(t, 3, cast.ToInt(Get("ef.ijk")))
|
|
assert.Equal(t, 4, cast.ToInt(Get("ef.lm.no")))
|
|
assert.Equal(t, 5, cast.ToInt(Get("ef.lm.p.q")))
|
|
}
|
|
|
|
func newViperWithConfigFile(t *testing.T) (*Viper, string) {
|
|
watchDir := t.TempDir()
|
|
configFile := path.Join(watchDir, "config.yaml")
|
|
err := os.WriteFile(configFile, []byte("foo: bar\n"), 0o640)
|
|
require.NoError(t, err)
|
|
v := New()
|
|
v.SetConfigFile(configFile)
|
|
err = v.ReadInConfig()
|
|
require.NoError(t, err)
|
|
require.Equal(t, "bar", v.Get("foo"))
|
|
return v, configFile
|
|
}
|
|
|
|
func newViperWithSymlinkedConfigFile(t *testing.T) (*Viper, string, string) {
|
|
watchDir := t.TempDir()
|
|
dataDir1 := path.Join(watchDir, "data1")
|
|
err := os.Mkdir(dataDir1, 0o777)
|
|
require.NoError(t, err)
|
|
realConfigFile := path.Join(dataDir1, "config.yaml")
|
|
t.Logf("Real config file location: %s\n", realConfigFile)
|
|
err = os.WriteFile(realConfigFile, []byte("foo: bar\n"), 0o640)
|
|
require.NoError(t, err)
|
|
// now, symlink the tm `data1` dir to `data` in the baseDir
|
|
os.Symlink(dataDir1, path.Join(watchDir, "data"))
|
|
// and link the `<watchdir>/datadir1/config.yaml` to `<watchdir>/config.yaml`
|
|
configFile := path.Join(watchDir, "config.yaml")
|
|
os.Symlink(path.Join(watchDir, "data", "config.yaml"), configFile)
|
|
t.Logf("Config file location: %s\n", path.Join(watchDir, "config.yaml"))
|
|
// init Viper
|
|
v := New()
|
|
v.SetConfigFile(configFile)
|
|
err = v.ReadInConfig()
|
|
require.NoError(t, err)
|
|
require.Equal(t, "bar", v.Get("foo"))
|
|
return v, watchDir, configFile
|
|
}
|
|
|
|
func TestWatchFile(t *testing.T) {
|
|
if runtime.GOOS == "linux" {
|
|
// TODO(bep) FIX ME
|
|
t.Skip("Skip test on Linux ...")
|
|
}
|
|
|
|
t.Run("file content changed", func(t *testing.T) {
|
|
// given a `config.yaml` file being watched
|
|
v, configFile := newViperWithConfigFile(t)
|
|
_, err := os.Stat(configFile)
|
|
require.NoError(t, err)
|
|
t.Logf("test config file: %s\n", configFile)
|
|
wg := sync.WaitGroup{}
|
|
wg.Add(1)
|
|
var wgDoneOnce sync.Once // OnConfigChange is called twice on Windows
|
|
v.OnConfigChange(func(in fsnotify.Event) {
|
|
t.Logf("config file changed")
|
|
wgDoneOnce.Do(func() {
|
|
wg.Done()
|
|
})
|
|
})
|
|
v.WatchConfig()
|
|
// when overwriting the file and waiting for the custom change notification handler to be triggered
|
|
err = os.WriteFile(configFile, []byte("foo: baz\n"), 0o640)
|
|
wg.Wait()
|
|
// then the config value should have changed
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "baz", v.Get("foo"))
|
|
})
|
|
|
|
t.Run("link to real file changed (à la Kubernetes)", func(t *testing.T) {
|
|
// skip if not executed on Linux
|
|
if runtime.GOOS != "linux" {
|
|
t.Skipf("Skipping test as symlink replacements don't work on non-linux environment...")
|
|
}
|
|
v, watchDir, _ := newViperWithSymlinkedConfigFile(t)
|
|
wg := sync.WaitGroup{}
|
|
v.WatchConfig()
|
|
v.OnConfigChange(func(in fsnotify.Event) {
|
|
t.Logf("config file changed")
|
|
wg.Done()
|
|
})
|
|
wg.Add(1)
|
|
// when link to another `config.yaml` file
|
|
dataDir2 := path.Join(watchDir, "data2")
|
|
err := os.Mkdir(dataDir2, 0o777)
|
|
require.NoError(t, err)
|
|
configFile2 := path.Join(dataDir2, "config.yaml")
|
|
err = os.WriteFile(configFile2, []byte("foo: baz\n"), 0o640)
|
|
require.NoError(t, err)
|
|
// change the symlink using the `ln -sfn` command
|
|
err = exec.Command("ln", "-sfn", dataDir2, path.Join(watchDir, "data")).Run()
|
|
require.NoError(t, err)
|
|
wg.Wait()
|
|
// then
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "baz", v.Get("foo"))
|
|
})
|
|
}
|
|
|
|
func TestUnmarshal_DotSeparatorBackwardCompatibility(t *testing.T) {
|
|
flags := pflag.NewFlagSet("test", pflag.ContinueOnError)
|
|
flags.String("foo.bar", "cobra_flag", "")
|
|
|
|
v := New()
|
|
assert.NoError(t, v.BindPFlags(flags))
|
|
|
|
config := &struct {
|
|
Foo struct {
|
|
Bar string
|
|
}
|
|
}{}
|
|
|
|
assert.NoError(t, v.Unmarshal(config))
|
|
assert.Equal(t, "cobra_flag", config.Foo.Bar)
|
|
}
|
|
|
|
// var yamlExampleWithDot = []byte(`Hacker: true
|
|
// name: steve
|
|
// hobbies:
|
|
// - skateboarding
|
|
// - snowboarding
|
|
// - go
|
|
// clothing:
|
|
// jacket: leather
|
|
// trousers: denim
|
|
// pants:
|
|
// size: large
|
|
// age: 35
|
|
// eyes : brown
|
|
// beard: true
|
|
// emails:
|
|
// steve@hacker.com:
|
|
// created: 01/02/03
|
|
// active: true
|
|
// `)
|
|
|
|
func TestKeyDelimiter(t *testing.T) {
|
|
v := NewWithOptions(KeyDelimiter("::"))
|
|
v.SetConfigType("yaml")
|
|
r := strings.NewReader(string(yamlExampleWithDot))
|
|
|
|
err := v.unmarshalReader(r, v.config)
|
|
require.NoError(t, err)
|
|
|
|
values := map[string]any{
|
|
"image": map[string]any{
|
|
"repository": "someImage",
|
|
"tag": "1.0.0",
|
|
},
|
|
"ingress": map[string]any{
|
|
"annotations": map[string]any{
|
|
"traefik.frontend.rule.type": "PathPrefix",
|
|
"traefik.ingress.kubernetes.io/ssl-redirect": "true",
|
|
},
|
|
},
|
|
}
|
|
|
|
v.SetDefault("charts::values", values)
|
|
|
|
assert.Equal(t, "leather", v.GetString("clothing::jacket"))
|
|
assert.Equal(t, "01/02/03", v.GetString("emails::steve@hacker.com::created"))
|
|
|
|
type config struct {
|
|
Charts struct {
|
|
Values map[string]any
|
|
}
|
|
}
|
|
|
|
expected := config{
|
|
Charts: struct {
|
|
Values map[string]any
|
|
}{
|
|
Values: values,
|
|
},
|
|
}
|
|
|
|
var actual config
|
|
|
|
assert.NoError(t, v.Unmarshal(&actual))
|
|
|
|
assert.Equal(t, expected, actual)
|
|
}
|
|
|
|
var yamlDeepNestedSlices = []byte(`TV:
|
|
- title: "The Expanse"
|
|
title_i18n:
|
|
USA: "The Expanse"
|
|
Japan: "エクスパンス -巨獣めざめる-"
|
|
seasons:
|
|
- first_released: "December 14, 2015"
|
|
episodes:
|
|
- title: "Dulcinea"
|
|
air_date: "December 14, 2015"
|
|
- title: "The Big Empty"
|
|
air_date: "December 15, 2015"
|
|
- title: "Remember the Cant"
|
|
air_date: "December 22, 2015"
|
|
- first_released: "February 1, 2017"
|
|
episodes:
|
|
- title: "Safe"
|
|
air_date: "February 1, 2017"
|
|
- title: "Doors & Corners"
|
|
air_date: "February 1, 2017"
|
|
- title: "Static"
|
|
air_date: "February 8, 2017"
|
|
episodes:
|
|
- ["Dulcinea", "The Big Empty", "Remember the Cant"]
|
|
- ["Safe", "Doors & Corners", "Static"]
|
|
`)
|
|
|
|
func TestSliceIndexAccess(t *testing.T) {
|
|
v.SetConfigType("yaml")
|
|
r := strings.NewReader(string(yamlDeepNestedSlices))
|
|
|
|
err := v.unmarshalReader(r, v.config)
|
|
require.NoError(t, err)
|
|
|
|
assert.Equal(t, "The Expanse", v.GetString("tv.0.title"))
|
|
assert.Equal(t, "February 1, 2017", v.GetString("tv.0.seasons.1.first_released"))
|
|
assert.Equal(t, "Static", v.GetString("tv.0.seasons.1.episodes.2.title"))
|
|
assert.Equal(t, "December 15, 2015", v.GetString("tv.0.seasons.0.episodes.1.air_date"))
|
|
|
|
// Test nested keys with capital letters
|
|
assert.Equal(t, "The Expanse", v.GetString("tv.0.title_i18n.USA"))
|
|
assert.Equal(t, "エクスパンス -巨獣めざめる-", v.GetString("tv.0.title_i18n.Japan"))
|
|
|
|
// Test for index out of bounds
|
|
assert.Equal(t, "", v.GetString("tv.0.seasons.2.first_released"))
|
|
|
|
// Accessing multidimensional arrays
|
|
assert.Equal(t, "Static", v.GetString("tv.0.episodes.1.2"))
|
|
}
|
|
|
|
func TestIsPathShadowedInFlatMap(t *testing.T) {
|
|
v := New()
|
|
|
|
stringMap := map[string]string{
|
|
"foo": "value",
|
|
}
|
|
|
|
flagMap := map[string]FlagValue{
|
|
"foo": pflagValue{},
|
|
}
|
|
|
|
path1 := []string{"foo", "bar"}
|
|
expected1 := "foo"
|
|
|
|
// "foo.bar" should shadowed by "foo"
|
|
assert.Equal(t, expected1, v.isPathShadowedInFlatMap(path1, stringMap))
|
|
assert.Equal(t, expected1, v.isPathShadowedInFlatMap(path1, flagMap))
|
|
|
|
path2 := []string{"bar", "foo"}
|
|
expected2 := ""
|
|
|
|
// "bar.foo" should not shadowed by "foo"
|
|
assert.Equal(t, expected2, v.isPathShadowedInFlatMap(path2, stringMap))
|
|
assert.Equal(t, expected2, v.isPathShadowedInFlatMap(path2, flagMap))
|
|
}
|
|
|
|
func TestFlagShadow(t *testing.T) {
|
|
v := New()
|
|
|
|
v.SetDefault("foo.bar1.bar2", "default")
|
|
|
|
flags := pflag.NewFlagSet("test", pflag.ContinueOnError)
|
|
flags.String("foo.bar1", "shadowed", "")
|
|
flags.VisitAll(func(flag *pflag.Flag) {
|
|
flag.Changed = true
|
|
})
|
|
|
|
v.BindPFlags(flags)
|
|
|
|
assert.Equal(t, "shadowed", v.GetString("foo.bar1"))
|
|
// the default "foo.bar1.bar2" value should shadowed by flag "foo.bar1" value
|
|
// and should return an empty string
|
|
assert.Equal(t, "", v.GetString("foo.bar1.bar2"))
|
|
}
|
|
|
|
func BenchmarkGetBool(b *testing.B) {
|
|
key := "BenchmarkGetBool"
|
|
v = New()
|
|
v.Set(key, true)
|
|
|
|
for i := 0; i < b.N; i++ {
|
|
if !v.GetBool(key) {
|
|
b.Fatal("GetBool returned false")
|
|
}
|
|
}
|
|
}
|
|
|
|
func BenchmarkGet(b *testing.B) {
|
|
key := "BenchmarkGet"
|
|
v = New()
|
|
v.Set(key, true)
|
|
|
|
for i := 0; i < b.N; i++ {
|
|
if !v.Get(key).(bool) {
|
|
b.Fatal("Get returned false")
|
|
}
|
|
}
|
|
}
|
|
|
|
// BenchmarkGetBoolFromMap is the "perfect result" for the above.
|
|
func BenchmarkGetBoolFromMap(b *testing.B) {
|
|
m := make(map[string]bool)
|
|
key := "BenchmarkGetBool"
|
|
m[key] = true
|
|
|
|
for i := 0; i < b.N; i++ {
|
|
if !m[key] {
|
|
b.Fatal("Map value was false")
|
|
}
|
|
}
|
|
}
|
|
|
|
// Skip some tests on Windows that kept failing when Windows was added to the CI as a target.
|
|
//
|
|
//nolint:gocritic // sloppyTestFuncName
|
|
func skipWindows(t *testing.T) {
|
|
if runtime.GOOS == "windows" {
|
|
t.Skip("Skip test on Windows")
|
|
}
|
|
}
|
|
|
|
// Test the ConfigMarshalError
|
|
func TestConfigMarshalError(t *testing.T) {
|
|
// test a generic error
|
|
err1 := fmt.Errorf("test error")
|
|
assert.NotErrorIs(t, err1, &ConfigMarshalError{})
|
|
// test the wrapped generic error
|
|
err2 := ConfigMarshalError{err: err1}
|
|
assert.ErrorIs(t, err2, &ConfigMarshalError{})
|
|
assert.ErrorIs(t, err2.Unwrap(), err1)
|
|
}
|
|
|
|
func TestUnsupportedConfigError(t *testing.T) {
|
|
err1 := fmt.Errorf("test error")
|
|
err2 := UnsupportedConfigError("some string")
|
|
assert.NotErrorIs(t, err2, err1)
|
|
assert.NotErrorIs(t, err1, err2)
|
|
assert.ErrorIs(t, err2, UnsupportedConfigError("some string"))
|
|
assert.NotErrorIs(t, err2, UnsupportedConfigError("other string"))
|
|
}
|
|
|
|
func TestUnsupportedRemoteProviderError(t *testing.T) {
|
|
err1 := fmt.Errorf("test error")
|
|
err2 := UnsupportedRemoteProviderError("some string")
|
|
assert.NotErrorIs(t, err1, err2)
|
|
assert.NotErrorIs(t, err2, err1)
|
|
assert.ErrorIs(t, err2, UnsupportedRemoteProviderError("some string"))
|
|
assert.NotErrorIs(t, err2, UnsupportedRemoteProviderError("other string"))
|
|
}
|
|
|
|
func TestRemoteConfigError(t *testing.T) {
|
|
err1 := fmt.Errorf("test error")
|
|
err2 := RemoteConfigError("some string")
|
|
assert.NotErrorIs(t, err1, err2)
|
|
assert.NotErrorIs(t, err2, err1)
|
|
assert.ErrorIs(t, err2, RemoteConfigError("some string"))
|
|
assert.NotErrorIs(t, err2, RemoteConfigError("other string"))
|
|
}
|
|
|
|
func TestConfigFileNotFoundError(t *testing.T) {
|
|
err1 := fmt.Errorf("test error")
|
|
err2 := ConfigFileNotFoundError{name: "name", locations: "locations"}
|
|
assert.NotErrorIs(t, err1, err2)
|
|
assert.NotErrorIs(t, err2, err1)
|
|
assert.ErrorIs(t, err2, &ConfigFileNotFoundError{})
|
|
assert.ErrorIs(t, err2, &ConfigFileNotFoundError{name: "name", locations: "locations"})
|
|
assert.ErrorIs(t, err2, &ConfigFileNotFoundError{name: "other name", locations: "other locations"})
|
|
}
|
|
|
|
func TestConfigFileAlreadyExistsError(t *testing.T) {
|
|
err1 := fmt.Errorf("test error")
|
|
err2 := ConfigFileAlreadyExistsError("some string")
|
|
assert.NotErrorIs(t, err1, err2)
|
|
assert.NotErrorIs(t, err2, err1)
|
|
assert.ErrorIs(t, err2, ConfigFileAlreadyExistsError("some string"))
|
|
assert.NotErrorIs(t, err2, ConfigFileAlreadyExistsError("other string"))
|
|
}
|