config/config_test.go

507 lines
12 KiB
Go
Raw Permalink Normal View History

package config
import (
"context"
"fmt"
"maps"
"os"
"testing"
coreio "forge.lthn.ai/core/go-io"
core "forge.lthn.ai/core/go/pkg/core"
"github.com/stretchr/testify/assert"
)
func TestConfig_Get_Good(t *testing.T) {
m := coreio.NewMockMedium()
cfg, err := New(WithMedium(m), WithPath("/tmp/test/config.yaml"))
assert.NoError(t, err)
err = cfg.Set("app.name", "core")
assert.NoError(t, err)
var name string
err = cfg.Get("app.name", &name)
assert.NoError(t, err)
assert.Equal(t, "core", name)
}
func TestConfig_Get_Bad(t *testing.T) {
m := coreio.NewMockMedium()
cfg, err := New(WithMedium(m), WithPath("/tmp/test/config.yaml"))
assert.NoError(t, err)
var value string
err = cfg.Get("nonexistent.key", &value)
assert.Error(t, err)
assert.Contains(t, err.Error(), "key not found")
}
func TestConfig_Set_Good(t *testing.T) {
m := coreio.NewMockMedium()
cfg, err := New(WithMedium(m), WithPath("/tmp/test/config.yaml"))
assert.NoError(t, err)
err = cfg.Set("dev.editor", "vim")
assert.NoError(t, err)
err = cfg.Commit()
assert.NoError(t, err)
// Verify the value was saved to the medium
content, readErr := m.Read("/tmp/test/config.yaml")
assert.NoError(t, readErr)
assert.Contains(t, content, "editor: vim")
// Verify we can read it back
var editor string
err = cfg.Get("dev.editor", &editor)
assert.NoError(t, err)
assert.Equal(t, "vim", editor)
}
func TestConfig_Set_Nested_Good(t *testing.T) {
m := coreio.NewMockMedium()
cfg, err := New(WithMedium(m), WithPath("/tmp/test/config.yaml"))
assert.NoError(t, err)
err = cfg.Set("a.b.c", "deep")
assert.NoError(t, err)
var val string
err = cfg.Get("a.b.c", &val)
assert.NoError(t, err)
assert.Equal(t, "deep", val)
}
func TestConfig_All_Good(t *testing.T) {
m := coreio.NewMockMedium()
cfg, err := New(WithMedium(m), WithPath("/tmp/test/config.yaml"))
assert.NoError(t, err)
_ = cfg.Set("key1", "val1")
_ = cfg.Set("key2", "val2")
all := maps.Collect(cfg.All())
assert.Equal(t, "val1", all["key1"])
assert.Equal(t, "val2", all["key2"])
}
func TestConfig_All_Order_Good(t *testing.T) {
m := coreio.NewMockMedium()
cfg, err := New(WithMedium(m), WithPath("/tmp/test/config.yaml"))
assert.NoError(t, err)
_ = cfg.Set("zulu", "last")
_ = cfg.Set("alpha", "first")
var keys []string
for key, _ := range cfg.All() {
keys = append(keys, key)
}
assert.Equal(t, []string{"alpha", "zulu"}, keys)
}
func TestConfig_Path_Good(t *testing.T) {
m := coreio.NewMockMedium()
cfg, err := New(WithMedium(m), WithPath("/custom/path/config.yaml"))
assert.NoError(t, err)
assert.Equal(t, "/custom/path/config.yaml", cfg.Path())
}
func TestConfig_Load_Existing_Good(t *testing.T) {
m := coreio.NewMockMedium()
m.Files["/tmp/test/config.yaml"] = "app:\n name: existing\n"
cfg, err := New(WithMedium(m), WithPath("/tmp/test/config.yaml"))
assert.NoError(t, err)
var name string
err = cfg.Get("app.name", &name)
assert.NoError(t, err)
assert.Equal(t, "existing", name)
}
func TestConfig_Env_Good(t *testing.T) {
// Set environment variable
t.Setenv("CORE_CONFIG_DEV_EDITOR", "nano")
m := coreio.NewMockMedium()
cfg, err := New(WithMedium(m), WithPath("/tmp/test/config.yaml"))
assert.NoError(t, err)
var editor string
err = cfg.Get("dev.editor", &editor)
assert.NoError(t, err)
assert.Equal(t, "nano", editor)
}
func TestConfig_Env_Overrides_File_Good(t *testing.T) {
// Set file config
m := coreio.NewMockMedium()
m.Files["/tmp/test/config.yaml"] = "dev:\n editor: vim\n"
// Set environment override
t.Setenv("CORE_CONFIG_DEV_EDITOR", "nano")
cfg, err := New(WithMedium(m), WithPath("/tmp/test/config.yaml"))
assert.NoError(t, err)
var editor string
err = cfg.Get("dev.editor", &editor)
assert.NoError(t, err)
assert.Equal(t, "nano", editor)
}
func TestConfig_Assign_Types_Good(t *testing.T) {
m := coreio.NewMockMedium()
m.Files["/tmp/test/config.yaml"] = "count: 42\nenabled: true\nratio: 3.14\n"
cfg, err := New(WithMedium(m), WithPath("/tmp/test/config.yaml"))
assert.NoError(t, err)
var count int
err = cfg.Get("count", &count)
assert.NoError(t, err)
assert.Equal(t, 42, count)
var enabled bool
err = cfg.Get("enabled", &enabled)
assert.NoError(t, err)
assert.True(t, enabled)
var ratio float64
err = cfg.Get("ratio", &ratio)
assert.NoError(t, err)
assert.InDelta(t, 3.14, ratio, 0.001)
}
func TestConfig_Assign_Any_Good(t *testing.T) {
m := coreio.NewMockMedium()
cfg, err := New(WithMedium(m), WithPath("/tmp/test/config.yaml"))
assert.NoError(t, err)
_ = cfg.Set("key", "value")
var val any
err = cfg.Get("key", &val)
assert.NoError(t, err)
assert.Equal(t, "value", val)
}
func TestConfig_DefaultPath_Good(t *testing.T) {
m := coreio.NewMockMedium()
cfg, err := New(WithMedium(m))
assert.NoError(t, err)
home, _ := os.UserHomeDir()
assert.Equal(t, home+"/.core/config.yaml", cfg.Path())
}
func TestLoadEnv_Good(t *testing.T) {
t.Setenv("CORE_CONFIG_FOO_BAR", "baz")
t.Setenv("CORE_CONFIG_SIMPLE", "value")
result := LoadEnv("CORE_CONFIG_")
assert.Equal(t, "baz", result["foo.bar"])
assert.Equal(t, "value", result["simple"])
}
func TestLoadEnv_PrefixNormalisation_Good(t *testing.T) {
t.Setenv("MYAPP_SETTING", "secret")
t.Setenv("MYAPP_ALPHA", "first")
keys := make([]string, 0, 2)
values := make([]string, 0, 2)
for key, value := range Env("MYAPP") {
keys = append(keys, key)
values = append(values, value.(string))
}
assert.Equal(t, []string{"alpha", "setting"}, keys)
assert.Equal(t, []string{"first", "secret"}, values)
}
func TestLoad_Bad(t *testing.T) {
m := coreio.NewMockMedium()
_, err := Load(m, "/nonexistent/file.yaml")
assert.Error(t, err)
assert.Contains(t, err.Error(), "failed to read config file")
}
func TestLoad_UnsupportedPath_Bad(t *testing.T) {
m := coreio.NewMockMedium()
m.Files["/tmp/test/config.json"] = `{"app":{"name":"core"}}`
_, err := Load(m, "/tmp/test/config.json")
assert.Error(t, err)
assert.Contains(t, err.Error(), "unsupported config file type")
}
func TestLoad_InvalidYAML_Bad(t *testing.T) {
m := coreio.NewMockMedium()
m.Files["/tmp/test/config.yaml"] = "invalid: yaml: content: [[[["
_, err := Load(m, "/tmp/test/config.yaml")
assert.Error(t, err)
assert.Contains(t, err.Error(), "failed to parse config file")
}
func TestConfig_LoadFile_JSON_Good(t *testing.T) {
m := coreio.NewMockMedium()
m.Files["/tmp/test/config.json"] = `{"app":{"name":"core"}}`
cfg, err := New(WithMedium(m), WithPath("/tmp/test/config.json"))
assert.NoError(t, err)
var name string
err = cfg.Get("app.name", &name)
assert.NoError(t, err)
assert.Equal(t, "core", name)
}
func TestConfig_LoadFile_Extensionless_Good(t *testing.T) {
m := coreio.NewMockMedium()
m.Files["/tmp/test/config"] = "app:\n name: core\n"
cfg, err := New(WithMedium(m), WithPath("/tmp/test/config"))
assert.NoError(t, err)
var name string
err = cfg.Get("app.name", &name)
assert.NoError(t, err)
assert.Equal(t, "core", name)
}
func TestConfig_LoadFile_TOML_Good(t *testing.T) {
m := coreio.NewMockMedium()
m.Files["/tmp/test/config.toml"] = "app = { name = \"core\" }\n"
cfg, err := New(WithMedium(m), WithPath("/tmp/test/config.toml"))
assert.NoError(t, err)
var name string
err = cfg.Get("app.name", &name)
assert.NoError(t, err)
assert.Equal(t, "core", name)
}
func TestConfig_LoadFile_Unsupported_Bad(t *testing.T) {
m := coreio.NewMockMedium()
cfg, err := New(WithMedium(m), WithPath("/tmp/test/config.txt"))
assert.NoError(t, err)
m.Files["/tmp/test/config.txt"] = "app.name=core"
err = cfg.LoadFile(m, "/tmp/test/config.txt")
assert.Error(t, err)
assert.Contains(t, err.Error(), "unsupported config file type")
}
func TestConfig_LoadFile_Unsupported_NoRead_Bad(t *testing.T) {
m := coreio.NewMockMedium()
cfg, err := New(WithMedium(m), WithPath("/tmp/test/config.txt"))
assert.NoError(t, err)
err = cfg.LoadFile(m, "/tmp/test/config.txt")
assert.Error(t, err)
assert.Contains(t, err.Error(), "unsupported config file type")
}
func TestSave_Good(t *testing.T) {
m := coreio.NewMockMedium()
data := map[string]any{
"key": "value",
}
err := Save(m, "/tmp/test/config.yaml", data)
assert.NoError(t, err)
content, readErr := m.Read("/tmp/test/config.yaml")
assert.NoError(t, readErr)
assert.Contains(t, content, "key: value")
}
func TestSave_Extensionless_Good(t *testing.T) {
m := coreio.NewMockMedium()
err := Save(m, "/tmp/test/config", map[string]any{"key": "value"})
assert.NoError(t, err)
content, readErr := m.Read("/tmp/test/config")
assert.NoError(t, readErr)
assert.Contains(t, content, "key: value")
}
func TestSave_UnsupportedPath_Bad(t *testing.T) {
m := coreio.NewMockMedium()
err := Save(m, "/tmp/test/config.json", map[string]any{"key": "value"})
assert.Error(t, err)
assert.Contains(t, err.Error(), "unsupported config file type")
}
func TestConfig_Commit_UnsupportedPath_Bad(t *testing.T) {
m := coreio.NewMockMedium()
cfg, err := New(WithMedium(m), WithPath("/tmp/test/config.json"))
assert.NoError(t, err)
err = cfg.Set("key", "value")
assert.NoError(t, err)
err = cfg.Commit()
assert.Error(t, err)
assert.Contains(t, err.Error(), "unsupported config file type")
}
func TestConfig_LoadFile_Env(t *testing.T) {
m := coreio.NewMockMedium()
m.Files["/.env"] = "FOO=bar\nBAZ=qux"
cfg, err := New(WithMedium(m), WithPath("/config.yaml"))
assert.NoError(t, err)
err = cfg.LoadFile(m, "/.env")
assert.NoError(t, err)
var foo string
err = cfg.Get("foo", &foo)
assert.NoError(t, err)
assert.Equal(t, "bar", foo)
}
func TestConfig_WithEnvPrefix(t *testing.T) {
t.Setenv("MYAPP_SETTING", "secret")
m := coreio.NewMockMedium()
cfg, err := New(WithMedium(m), WithEnvPrefix("MYAPP"))
assert.NoError(t, err)
var setting string
err = cfg.Get("setting", &setting)
assert.NoError(t, err)
assert.Equal(t, "secret", setting)
}
func TestConfig_WithEnvPrefix_TrailingUnderscore_Good(t *testing.T) {
t.Setenv("MYAPP_SETTING", "secret")
m := coreio.NewMockMedium()
cfg, err := New(WithMedium(m), WithEnvPrefix("MYAPP_"))
assert.NoError(t, err)
var setting string
err = cfg.Get("setting", &setting)
assert.NoError(t, err)
assert.Equal(t, "secret", setting)
}
func TestService_OnStartup_WithEnvPrefix_Good(t *testing.T) {
t.Setenv("MYAPP_SETTING", "secret")
m := coreio.NewMockMedium()
svc := &Service{
ServiceRuntime: core.NewServiceRuntime(nil, ServiceOptions{
EnvPrefix: "MYAPP",
Medium: m,
}),
}
err := svc.OnStartup(context.Background())
assert.NoError(t, err)
var setting string
err = svc.Get("setting", &setting)
assert.NoError(t, err)
assert.Equal(t, "secret", setting)
}
func TestConfig_Get_EmptyKey(t *testing.T) {
m := coreio.NewMockMedium()
m.Files["/config.yaml"] = "app:\n name: test\nversion: 1"
cfg, err := New(WithMedium(m), WithPath("/config.yaml"))
assert.NoError(t, err)
type AppConfig struct {
App struct {
Name string `mapstructure:"name"`
} `mapstructure:"app"`
Version int `mapstructure:"version"`
}
var full AppConfig
err = cfg.Get("", &full)
assert.NoError(t, err)
assert.Equal(t, "test", full.App.Name)
assert.Equal(t, 1, full.Version)
}
func ExampleConfig_Get() {
m := coreio.NewMockMedium()
cfg, _ := New(WithMedium(m), WithPath("/tmp/example/config.yaml"))
_ = cfg.Set("dev.editor", "vim")
var editor string
_ = cfg.Get("dev.editor", &editor)
fmt.Println(editor)
// Output: vim
}
func ExampleConfig_Commit() {
m := coreio.NewMockMedium()
cfg, _ := New(WithMedium(m), WithPath("/tmp/example/config.yaml"))
_ = cfg.Set("app.name", "core")
_ = cfg.Commit()
content, _ := m.Read("/tmp/example/config.yaml")
fmt.Print(content)
// Output:
// app:
// name: core
}
func ExampleEnv() {
t := "EXAMPLE_FOO_BAR"
_ = os.Setenv(t, "baz")
defer os.Unsetenv(t)
for key, value := range Env("EXAMPLE_") {
fmt.Printf("%s=%s\n", key, value)
}
// Output: foo.bar=baz
}
func ExampleConfig_LoadFile() {
m := coreio.NewMockMedium()
m.Files["/.env"] = "FOO=bar\n"
cfg, _ := New(WithMedium(m), WithPath("/config.yaml"))
_ = cfg.LoadFile(m, "/.env")
var foo string
_ = cfg.Get("foo", &foo)
fmt.Println(foo)
// Output: bar
}