v0.2.0 toml-only, harden concurrency and env export, dep updates
Test / test (push) Canceled after 0s

This commit is contained in:
lixen committed 2026-09-28 16:55:58 -04:00
1 parent 3c35601179
commit e08467bf38
42 files changed
+2533 -4274

No files matched your search

+27 -327
View File
@@ -1,234 +1,43 @@
// FILE: lixenwraith/config/dynamic_test.go
package config
import (
"encoding/json"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"os"
"path/filepath"
"runtime"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// TestMultiFormatLoading tests loading different config formats
func TestMultiFormatLoading(t *testing.T) {
tmpDir := t.TempDir()
// Create test config in different formats
tomlConfig := `
[server]
host = "toml-host"
port = 8080
[database]
url = "postgres://localhost/toml"
`
jsonConfig := `{
"server": {
"host": "json-host",
"port": 9090
},
"database": {
"url": "postgres://localhost/json"
}
}`
yamlConfig := `
server:
host: yaml-host
port: 7070
database:
url: postgres://localhost/yaml
`
// Write config files
tomlPath := filepath.Join(tmpDir, "config.toml")
jsonPath := filepath.Join(tmpDir, "config.json")
yamlPath := filepath.Join(tmpDir, "config.yaml")
require.NoError(t, os.WriteFile(tomlPath, []byte(tomlConfig), 0644))
require.NoError(t, os.WriteFile(jsonPath, []byte(jsonConfig), 0644))
require.NoError(t, os.WriteFile(yamlPath, []byte(yamlConfig), 0644))
t.Run("AutoDetectFormats", func(t *testing.T) {
cfg := New()
cfg.Register("server.host", "")
cfg.Register("server.port", 0)
cfg.Register("database.url", "")
// Test TOML
cfg.SetFileFormat("auto")
require.NoError(t, cfg.LoadFile(tomlPath))
host, _ := cfg.Get("server.host")
assert.Equal(t, "toml-host", host)
// Test JSON
require.NoError(t, cfg.LoadFile(jsonPath))
host, _ = cfg.Get("server.host")
assert.Equal(t, "json-host", host)
port, _ := cfg.Get("server.port")
// JSON number should be preserved as json.Number but convertible
switch v := port.(type) {
case json.Number:
// Expected for raw value
assert.Equal(t, json.Number("9090"), v)
case int64:
// Expected after decode hook conversion
assert.Equal(t, int64(9090), v)
case float64:
// Alternative conversion
assert.Equal(t, float64(9090), v)
default:
t.Errorf("Unexpected type for port: %T", port)
}
// Test YAML
require.NoError(t, cfg.LoadFile(yamlPath))
host, _ = cfg.Get("server.host")
assert.Equal(t, "yaml-host", host)
})
t.Run("ExplicitFormat", func(t *testing.T) {
cfg := New()
cfg.Register("server.host", "")
// Force JSON parsing on .conf file
confPath := filepath.Join(tmpDir, "config.conf")
require.NoError(t, os.WriteFile(confPath, []byte(jsonConfig), 0644))
cfg.SetFileFormat("json")
require.NoError(t, cfg.LoadFile(confPath))
host, _ := cfg.Get("server.host")
assert.Equal(t, "json-host", host)
})
t.Run("ContentDetection", func(t *testing.T) {
cfg := New()
cfg.Register("server.host", "")
// Ambiguous extension
ambigPath := filepath.Join(tmpDir, "config.conf")
require.NoError(t, os.WriteFile(ambigPath, []byte(yamlConfig), 0644))
cfg.SetFileFormat("auto")
require.NoError(t, cfg.LoadFile(ambigPath))
host, _ := cfg.Get("server.host")
assert.Equal(t, "yaml-host", host)
})
}
// TestDynamicFormatSwitching tests runtime format changes
func TestDynamicFormatSwitching(t *testing.T) {
tmpDir := t.TempDir()
// Create configs in different formats with same structure
configs := map[string]string{
"toml": `value = "from-toml"`,
"json": `{"value": "from-json"}`,
"yaml": `value: from-yaml`,
func TestTOMLOnly(t *testing.T) {
for _, format := range []string{"json", "yaml", "other"} {
c := New()
require.Error(t, c.SetFileFormat(format))
_, err := NewBuilder().WithTagName(format).Build()
require.Error(t, err)
var dst struct{ Value string }
require.Error(t, ScanMap(nil, &dst, format))
}
cfg := New()
cfg.Register("value", "default")
for format, content := range configs {
t.Run(format, func(t *testing.T) {
filePath := filepath.Join(tmpDir, "config."+format)
require.NoError(t, os.WriteFile(filePath, []byte(content), 0644))
// Set format and load
require.NoError(t, cfg.SetFileFormat(format))
require.NoError(t, cfg.LoadFile(filePath))
val, _ := cfg.Get("value")
assert.Equal(t, "from-"+format, val)
})
for _, tc := range []struct{ ext, content string }{
{".toml", `value = "toml"`}, {".conf", `value = "toml"`}, {".config", `value = "toml"`},
} {
c := New()
require.NoError(t, c.Register("value", ""))
path := filepath.Join(t.TempDir(), "config"+tc.ext)
require.NoError(t, os.WriteFile(path, []byte(tc.content), 0600))
require.NoError(t, c.LoadFile(path))
}
for _, tc := range []struct{ ext, content string }{
{".json", `{"value":"json"}`}, {".yaml", "value: yaml"}, {".conf", `{"value":"json"}`},
} {
c := New()
require.NoError(t, c.Register("value", ""))
path := filepath.Join(t.TempDir(), "config"+tc.ext)
require.NoError(t, os.WriteFile(path, []byte(tc.content), 0600))
require.Error(t, c.LoadFile(path))
}
}
// TestWatchFileFormatSwitch tests watching different file formats
func TestWatchFileFormatSwitch(t *testing.T) {
tmpDir := t.TempDir()
tomlPath := filepath.Join(tmpDir, "config.toml")
jsonPath := filepath.Join(tmpDir, "config.json")
require.NoError(t, os.WriteFile(tomlPath, []byte(`value = "toml-1"`), 0644))
require.NoError(t, os.WriteFile(jsonPath, []byte(`{"value": "json-1"}`), 0644))
cfg := New()
cfg.Register("value", "default")
// Configure fast polling for test
opts := WatchOptions{
PollInterval: testPollInterval, // Fast polling for tests
Debounce: testDebounce, // Short debounce
MaxWatchers: 10,
}
// Start watching TOML
cfg.SetFileFormat("auto")
require.NoError(t, cfg.LoadFile(tomlPath))
cfg.AutoUpdateWithOptions(opts)
defer cfg.StopAutoUpdate()
// Wait for watcher to start
require.Eventually(t, func() bool {
return cfg.IsWatching()
}, 4*testDebounce, 2*SpinWaitInterval)
val, _ := cfg.Get("value")
assert.Equal(t, "toml-1", val)
// Switch to JSON with format hint
require.NoError(t, cfg.WatchFile(jsonPath, "json"))
// Wait for new watcher to start
require.Eventually(t, func() bool {
return cfg.IsWatching()
}, 4*testDebounce, 2*SpinWaitInterval)
// Get watch channel AFTER switching files
changes := cfg.Watch()
val, _ = cfg.Get("value")
assert.Equal(t, "json-1", val)
// Update JSON file
require.NoError(t, os.WriteFile(jsonPath, []byte(`{"value": "json-2"}`), 0644))
// Wait for change notification
select {
case path := <-changes:
assert.Equal(t, "value", path)
// Wait a bit for value to be updated
require.Eventually(t, func() bool {
val, _ := cfg.Get("value")
return val == "json-2"
}, testEventuallyTimeout, 2*SpinWaitInterval)
case <-time.After(testWatchTimeout):
t.Error("Timeout waiting for JSON file change")
}
// Update old TOML file - should NOT trigger notification
require.NoError(t, os.WriteFile(tomlPath, []byte(`value = "toml-2"`), 0644))
// Should not receive notification from old file
select {
case <-changes:
t.Error("Should not receive changes from old TOML file")
case <-time.After(testPollWindow):
// Expected - no change notification
}
}
// TestSecurityOptions tests security features
func TestSecurityOptions(t *testing.T) {
tmpDir := t.TempDir()
@@ -307,112 +116,3 @@ func waitForWatchingState(t *testing.T, cfg *Config, expected bool, msgAndArgs .
return cfg.IsWatching() == expected
}, testEventuallyTimeout, 2*SpinWaitInterval, msgAndArgs...)
}
// TestBuilderWithFormat tests Builder integration
func TestBuilderWithFormat(t *testing.T) {
tmpDir := t.TempDir()
jsonPath := filepath.Join(tmpDir, "config.json")
jsonConfig := `{
"server": {
"host": "builder-host",
"port": 8080
}
}`
require.NoError(t, os.WriteFile(jsonPath, []byte(jsonConfig), 0644))
type Config struct {
Server struct {
Host string `json:"host" toml:"host"`
Port int `json:"port" toml:"port"`
} `json:"server" toml:"server"`
}
defaults := &Config{}
defaults.Server.Host = "default-host"
defaults.Server.Port = 3000
cfg, err := NewBuilder().
WithDefaults(defaults).
WithFile(jsonPath).
WithFileFormat("json").
WithTagName("toml"). // Use toml tags for registration
WithSecurityOptions(SecurityOptions{
PreventPathTraversal: true,
MaxFileSize: 1024 * 1024, // 1MB
}).
Build()
require.NoError(t, err)
// Check the value was loaded
host, exists := cfg.Get("server.host")
assert.True(t, exists, "server.host should exist")
assert.Equal(t, "builder-host", host)
port, exists := cfg.Get("server.port")
assert.True(t, exists, "server.port should exist")
// Handle json.Number or converted int
switch v := port.(type) {
case json.Number:
p, _ := v.Int64()
assert.Equal(t, int64(8080), p)
case int64:
assert.Equal(t, int64(8080), v)
case float64:
assert.Equal(t, float64(8080), v)
default:
t.Errorf("Unexpected type for port: %T", port)
}
}
// BenchmarkFormatParsing benchmarks different format parsing speeds
func BenchmarkFormatParsing(b *testing.B) {
tmpDir := b.TempDir()
// Create test data
configs := map[string]string{
"toml": `
[server]
host = "localhost"
port = 8080
[database]
url = "postgres://localhost/db"
[cache]
ttl = 300
`,
"json": `{
"server": {"host": "localhost", "port": 8080},
"database": {"url": "postgres://localhost/db"},
"cache": {"ttl": 300}
}`,
"yaml": `
server:
host: localhost
port: 8080
database:
url: postgres://localhost/db
cache:
ttl: 300
`,
}
for format, content := range configs {
b.Run(format, func(b *testing.B) {
path := filepath.Join(tmpDir, "bench."+format)
os.WriteFile(path, []byte(content), 0644)
cfg := New()
cfg.Register("server.host", "")
cfg.Register("server.port", 0)
cfg.Register("database.url", "")
cfg.Register("cache.ttl", 0)
cfg.SetFileFormat(format)
b.ResetTimer()
for i := 0; i < b.N; i++ {
cfg.LoadFile(path)
}
})
}
}