- config.go 重构:抽取 loadConfig 共享逻辑,新增 LoadServerConfig/LoadDesktopConfig/LoadDesktopConfigAtPath,LoadConfig 保持向后兼容 - setupConfigFile 移除 SafeWriteConfigAs 自动创建逻辑,文件不存在时仅使用默认值 - cmd/desktop 切换为 LoadDesktopConfig,端口/HTTP/浏览器/托盘统一使用 cfg.Server.Port - cmd/server 显式使用 LoadServerConfig 明确入口语义 - 提取 desktop 可测 helper:desktopListenAddr/desktopURL/desktopPortMenuTitle/desktopConfigErrorMessage - 新增测试:desktop 忽略 CLI/env/未知参数、配置快照不变、无效配置文件不静默回退、端口 helper 一致性 - README 区分 server/desktop 配置源,移除首次启动自动创建配置文件描述 - 同步 delta specs 到 openspec/specs/ 主规范
311 lines
8.7 KiB
Go
311 lines
8.7 KiB
Go
package config
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
"time"
|
|
|
|
"nex/backend/internal/config"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
"gopkg.in/yaml.v3"
|
|
)
|
|
|
|
func TestLoadConfig_DefaultValues(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
configPath := filepath.Join(tmpDir, "config.yaml")
|
|
|
|
cfg, err := config.LoadConfigFromPath(configPath)
|
|
require.NoError(t, err)
|
|
|
|
assert.Equal(t, 9826, cfg.Server.Port)
|
|
assert.Equal(t, 30*time.Second, cfg.Server.ReadTimeout)
|
|
assert.Equal(t, 30*time.Second, cfg.Server.WriteTimeout)
|
|
|
|
assert.Equal(t, 10, cfg.Database.MaxIdleConns)
|
|
assert.Equal(t, 100, cfg.Database.MaxOpenConns)
|
|
assert.Equal(t, 1*time.Hour, cfg.Database.ConnMaxLifetime)
|
|
|
|
assert.Equal(t, "info", cfg.Log.Level)
|
|
assert.Equal(t, 100, cfg.Log.MaxSize)
|
|
assert.Equal(t, 10, cfg.Log.MaxBackups)
|
|
assert.Equal(t, 30, cfg.Log.MaxAge)
|
|
assert.True(t, cfg.Log.Compress)
|
|
}
|
|
|
|
func TestLoadConfig_EnvOverride(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
configPath := filepath.Join(tmpDir, "config.yaml")
|
|
|
|
t.Setenv("NEX_SERVER_PORT", "9000")
|
|
t.Setenv("NEX_LOG_LEVEL", "debug")
|
|
t.Setenv("NEX_DATABASE_MAX_IDLE_CONNS", "20")
|
|
|
|
cfg, err := config.LoadConfigFromPath(configPath)
|
|
require.NoError(t, err)
|
|
|
|
assert.Equal(t, 9000, cfg.Server.Port)
|
|
assert.Equal(t, "debug", cfg.Log.Level)
|
|
assert.Equal(t, 20, cfg.Database.MaxIdleConns)
|
|
}
|
|
|
|
func TestLoadConfig_YAMLFile(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
configPath := filepath.Join(tmpDir, "config.yaml")
|
|
|
|
yamlContent := `
|
|
server:
|
|
port: 8080
|
|
read_timeout: 60s
|
|
write_timeout: 60s
|
|
database:
|
|
path: /custom/path.db
|
|
max_idle_conns: 5
|
|
max_open_conns: 50
|
|
conn_max_lifetime: 2h
|
|
log:
|
|
level: warn
|
|
path: /custom/log
|
|
max_size: 200
|
|
max_backups: 5
|
|
max_age: 7
|
|
compress: false
|
|
`
|
|
err := os.WriteFile(configPath, []byte(yamlContent), 0o600)
|
|
require.NoError(t, err)
|
|
|
|
cfg, err := config.LoadConfigFromPath(configPath)
|
|
require.NoError(t, err)
|
|
|
|
assert.Equal(t, 8080, cfg.Server.Port)
|
|
assert.Equal(t, 60*time.Second, cfg.Server.ReadTimeout)
|
|
assert.Equal(t, 60*time.Second, cfg.Server.WriteTimeout)
|
|
assert.Equal(t, "/custom/path.db", cfg.Database.Path)
|
|
assert.Equal(t, 5, cfg.Database.MaxIdleConns)
|
|
assert.Equal(t, 50, cfg.Database.MaxOpenConns)
|
|
assert.Equal(t, 2*time.Hour, cfg.Database.ConnMaxLifetime)
|
|
assert.Equal(t, "warn", cfg.Log.Level)
|
|
assert.Equal(t, "/custom/log", cfg.Log.Path)
|
|
assert.Equal(t, 200, cfg.Log.MaxSize)
|
|
assert.Equal(t, 5, cfg.Log.MaxBackups)
|
|
assert.Equal(t, 7, cfg.Log.MaxAge)
|
|
assert.False(t, cfg.Log.Compress)
|
|
}
|
|
|
|
func TestLoadConfig_PriorityChain(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
configPath := filepath.Join(tmpDir, "config.yaml")
|
|
|
|
yamlContent := `
|
|
server:
|
|
port: 8080
|
|
log:
|
|
level: warn
|
|
`
|
|
err := os.WriteFile(configPath, []byte(yamlContent), 0o600)
|
|
require.NoError(t, err)
|
|
|
|
t.Setenv("NEX_SERVER_PORT", "9000")
|
|
|
|
originalArgs := os.Args
|
|
defer func() { os.Args = originalArgs }()
|
|
os.Args = []string{"test", "--server-port", "9999"}
|
|
|
|
cfg, err := config.LoadConfigFromPath(configPath)
|
|
require.NoError(t, err)
|
|
|
|
assert.Equal(t, 9999, cfg.Server.Port, "CLI should override ENV and YAML")
|
|
assert.Equal(t, "warn", cfg.Log.Level, "YAML value should be used when no CLI/ENV override")
|
|
}
|
|
|
|
func TestLoadConfig_NoAutoCreate(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
configPath := filepath.Join(tmpDir, "config.yaml")
|
|
|
|
_, err := os.Stat(configPath)
|
|
assert.True(t, os.IsNotExist(err), "config file should not exist before load")
|
|
|
|
cfg, err := config.LoadConfigFromPath(configPath)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, cfg)
|
|
|
|
assert.Equal(t, 9826, cfg.Server.Port, "should load with default values")
|
|
|
|
_, err = os.Stat(configPath)
|
|
assert.True(t, os.IsNotExist(err), "config file should not be auto-created")
|
|
}
|
|
|
|
func TestSaveAndLoadConfig(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
configPath := filepath.Join(tmpDir, "config.yaml")
|
|
|
|
cfg := &config.Config{
|
|
Server: config.ServerConfig{
|
|
Port: 7777,
|
|
ReadTimeout: 45 * time.Second,
|
|
WriteTimeout: 45 * time.Second,
|
|
},
|
|
Database: config.DatabaseConfig{
|
|
Driver: "sqlite",
|
|
Path: filepath.Join(tmpDir, "test.db"),
|
|
Port: 3306,
|
|
DBName: "nex",
|
|
MaxIdleConns: 15,
|
|
MaxOpenConns: 150,
|
|
ConnMaxLifetime: 2 * time.Hour,
|
|
},
|
|
Log: config.LogConfig{
|
|
Level: "debug",
|
|
Path: filepath.Join(tmpDir, "log"),
|
|
MaxSize: 50,
|
|
MaxBackups: 3,
|
|
MaxAge: 14,
|
|
Compress: false,
|
|
},
|
|
}
|
|
|
|
data, err := yaml.Marshal(cfg)
|
|
require.NoError(t, err)
|
|
|
|
err = os.WriteFile(configPath, data, 0o600)
|
|
require.NoError(t, err)
|
|
|
|
loaded, err := config.LoadConfigFromPath(configPath)
|
|
require.NoError(t, err)
|
|
|
|
assert.Equal(t, cfg.Server.Port, loaded.Server.Port)
|
|
assert.Equal(t, cfg.Server.ReadTimeout, loaded.Server.ReadTimeout)
|
|
assert.Equal(t, cfg.Server.WriteTimeout, loaded.Server.WriteTimeout)
|
|
assert.Equal(t, cfg.Database.MaxIdleConns, loaded.Database.MaxIdleConns)
|
|
assert.Equal(t, cfg.Database.MaxOpenConns, loaded.Database.MaxOpenConns)
|
|
assert.Equal(t, cfg.Database.ConnMaxLifetime, loaded.Database.ConnMaxLifetime)
|
|
assert.Equal(t, cfg.Log.Level, loaded.Log.Level)
|
|
assert.Equal(t, cfg.Log.MaxSize, loaded.Log.MaxSize)
|
|
assert.Equal(t, cfg.Log.MaxBackups, loaded.Log.MaxBackups)
|
|
assert.Equal(t, cfg.Log.MaxAge, loaded.Log.MaxAge)
|
|
assert.Equal(t, cfg.Log.Compress, loaded.Log.Compress)
|
|
}
|
|
|
|
func TestLoadDesktopConfig_FileOnly(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
configPath := filepath.Join(tmpDir, "config.yaml")
|
|
|
|
yamlContent := `
|
|
server:
|
|
port: 8080
|
|
log:
|
|
level: debug
|
|
`
|
|
err := os.WriteFile(configPath, []byte(yamlContent), 0o600)
|
|
require.NoError(t, err)
|
|
|
|
cfg, err := config.LoadDesktopConfigAtPath(configPath)
|
|
require.NoError(t, err)
|
|
|
|
assert.Equal(t, 8080, cfg.Server.Port)
|
|
assert.Equal(t, "debug", cfg.Log.Level)
|
|
}
|
|
|
|
func TestLoadDesktopConfig_IgnoresCLI(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
configPath := filepath.Join(tmpDir, "config.yaml")
|
|
|
|
err := os.WriteFile(configPath, []byte("server:\n port: 8080\n"), 0o600)
|
|
require.NoError(t, err)
|
|
|
|
originalArgs := os.Args
|
|
defer func() { os.Args = originalArgs }()
|
|
os.Args = []string{"nex", "--server-port", "9999"}
|
|
|
|
cfg, err := config.LoadDesktopConfigAtPath(configPath)
|
|
require.NoError(t, err)
|
|
|
|
assert.Equal(t, 8080, cfg.Server.Port, "desktop should ignore CLI args and use config file")
|
|
}
|
|
|
|
func TestLoadDesktopConfig_IgnoresEnv(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
configPath := filepath.Join(tmpDir, "config.yaml")
|
|
|
|
err := os.WriteFile(configPath, []byte("server:\n port: 8080\n"), 0o600)
|
|
require.NoError(t, err)
|
|
|
|
t.Setenv("NEX_SERVER_PORT", "9000")
|
|
t.Setenv("NEX_LOG_LEVEL", "debug")
|
|
|
|
cfg, err := config.LoadDesktopConfigAtPath(configPath)
|
|
require.NoError(t, err)
|
|
|
|
assert.Equal(t, 8080, cfg.Server.Port, "desktop should ignore env vars and use config file")
|
|
assert.Equal(t, "info", cfg.Log.Level, "desktop should ignore env vars and use default")
|
|
}
|
|
|
|
func TestLoadDesktopConfig_IgnoresUnknownArgs(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
configPath := filepath.Join(tmpDir, "config.yaml")
|
|
|
|
err := os.WriteFile(configPath, []byte("server:\n port: 8080\n"), 0o600)
|
|
require.NoError(t, err)
|
|
|
|
originalArgs := os.Args
|
|
defer func() { os.Args = originalArgs }()
|
|
os.Args = []string{"nex", "--unknown-flag", "value", "--another-unknown"}
|
|
|
|
cfg, err := config.LoadDesktopConfigAtPath(configPath)
|
|
require.NoError(t, err, "desktop should not fail on unknown CLI args")
|
|
|
|
assert.Equal(t, 8080, cfg.Server.Port)
|
|
}
|
|
|
|
func TestLoadDesktopConfig_Snapshot(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
configPath := filepath.Join(tmpDir, "config.yaml")
|
|
|
|
err := os.WriteFile(configPath, []byte("server:\n port: 8080\n"), 0o600)
|
|
require.NoError(t, err)
|
|
|
|
cfg, err := config.LoadDesktopConfigAtPath(configPath)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, 8080, cfg.Server.Port)
|
|
|
|
err = os.WriteFile(configPath, []byte("server:\n port: 9999\n"), 0o600)
|
|
require.NoError(t, err)
|
|
|
|
assert.Equal(t, 8080, cfg.Server.Port, "loaded config snapshot should not change when file changes")
|
|
|
|
cfg2, err := config.LoadDesktopConfigAtPath(configPath)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, 9999, cfg2.Server.Port, "reload should pick up new config values")
|
|
}
|
|
|
|
func TestLoadDesktopConfig_InvalidFileFails(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
content string
|
|
}{
|
|
{
|
|
name: "invalid yaml",
|
|
content: "server:\n port: [\n",
|
|
},
|
|
{
|
|
name: "validation failure",
|
|
content: "server:\n port: 70000\n",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
configPath := filepath.Join(tmpDir, "config.yaml")
|
|
|
|
err := os.WriteFile(configPath, []byte(tt.content), 0o600)
|
|
require.NoError(t, err)
|
|
|
|
_, err = config.LoadDesktopConfigAtPath(configPath)
|
|
require.Error(t, err, "desktop should not silently fall back to defaults for invalid config files")
|
|
})
|
|
}
|
|
}
|