spf13--viper/viper_test.go
Gerwin van de Steeg d6e5a55f3c
feat: add errors.Is support to all errors
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.
2024-01-09 17:50:25 +13:00

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"))
}