From 5c471485ab6626ac84d15196e991a1bccd6fc4f0 Mon Sep 17 00:00:00 2001 From: wucm667 Date: Mon, 27 Jul 2026 06:20:11 +0800 Subject: [PATCH] fix(config): honor explicit CONFIG_FILE path Make CONFIG_FILE select an explicit config for both full loading and lightweight address lookup, with regression tests. --- backend/internal/config/config.go | 37 ++++++++-------- backend/internal/config/config_test.go | 61 ++++++++++++++++++++++++++ 2 files changed, 80 insertions(+), 18 deletions(-) diff --git a/backend/internal/config/config.go b/backend/internal/config/config.go index a62c068f21..3bd071e9a1 100644 --- a/backend/internal/config/config.go +++ b/backend/internal/config/config.go @@ -1640,20 +1640,7 @@ func LoadForBootstrap() (*Config, error) { func load(allowMissingJWTSecret bool) (*Config, error) { viper.SetConfigName("config") viper.SetConfigType("yaml") - - // Add config paths in priority order - // 1. DATA_DIR environment variable (highest priority) - if dataDir := os.Getenv("DATA_DIR"); dataDir != "" { - viper.AddConfigPath(dataDir) - } - // 2. Docker data directory - viper.AddConfigPath("/app/data") - // 3. Current directory - viper.AddConfigPath(".") - // 4. Config subdirectory - viper.AddConfigPath("./config") - // 5. System config directory - viper.AddConfigPath("/etc/sub2api") + configureConfigSource(viper.SetConfigFile, viper.AddConfigPath) // 环境变量支持 viper.AutomaticEnv() @@ -1828,6 +1815,22 @@ func load(allowMissingJWTSecret bool) (*Config, error) { return &cfg, nil } +func configureConfigSource(setConfigFile, addConfigPath func(string)) { + if configFile := strings.TrimSpace(os.Getenv("CONFIG_FILE")); configFile != "" { + setConfigFile(configFile) + return + } + + // Add config paths in priority order. + if dataDir := strings.TrimSpace(os.Getenv("DATA_DIR")); dataDir != "" { + addConfigPath(dataDir) + } + addConfigPath("/app/data") + addConfigPath(".") + addConfigPath("./config") + addConfigPath("/etc/sub2api") +} + func setDefaults() { viper.SetDefault("run_mode", RunModeStandard) @@ -3488,14 +3491,12 @@ func generateJWTSecret(byteLength int) (string, error) { // GetServerAddress returns the server address (host:port) from config file or environment variable. // This is a lightweight function that can be used before full config validation, // such as during setup wizard startup. -// Priority: config.yaml > environment variables > defaults +// Priority: environment variables > config file > defaults. func GetServerAddress() string { v := viper.New() v.SetConfigName("config") v.SetConfigType("yaml") - v.AddConfigPath(".") - v.AddConfigPath("./config") - v.AddConfigPath("/etc/sub2api") + configureConfigSource(v.SetConfigFile, v.AddConfigPath) // Support SERVER_HOST and SERVER_PORT environment variables v.AutomaticEnv() diff --git a/backend/internal/config/config_test.go b/backend/internal/config/config_test.go index e4186732a8..f67fbb4cc1 100644 --- a/backend/internal/config/config_test.go +++ b/backend/internal/config/config_test.go @@ -17,6 +17,9 @@ import ( func resetViperWithJWTSecret(t *testing.T) { t.Helper() viper.Reset() + t.Cleanup(viper.Reset) + t.Setenv("CONFIG_FILE", "") + t.Setenv("DATA_DIR", "") t.Setenv("JWT_SECRET", strings.Repeat("x", 32)) } @@ -253,8 +256,53 @@ func TestLoadTrustedProxiesPresenceFromYAML(t *testing.T) { } } +func TestLoadTrustedProxiesFromConfigFile(t *testing.T) { + resetViperWithJWTSecret(t) + configFile := filepath.Join(t.TempDir(), "reporter-config.yaml") + require.NoError(t, os.WriteFile(configFile, []byte(`server: + host: 192.0.2.10 + trusted_proxies: + - 127.0.0.1/32 + - 10.0.0.0/8 + - 172.16.0.0/12 + - 192.168.0.0/16 +`), 0o600)) + t.Setenv("CONFIG_FILE", configFile) + + cfg, err := Load() + require.NoError(t, err) + require.Equal(t, "192.0.2.10", cfg.Server.Host) + require.Equal(t, []string{"127.0.0.1/32", "10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16"}, cfg.Server.TrustedProxies) + require.True(t, cfg.Server.TrustedProxiesConfigured) +} + +func TestConfigFileTakesPrecedenceOverDataDir(t *testing.T) { + resetViperWithJWTSecret(t) + dataDir := t.TempDir() + require.NoError(t, os.WriteFile(filepath.Join(dataDir, "config.yaml"), []byte("server:\n host: 192.0.2.20\n"), 0o600)) + configFile := filepath.Join(t.TempDir(), "explicit.yaml") + require.NoError(t, os.WriteFile(configFile, []byte("server:\n host: 192.0.2.30\n"), 0o600)) + t.Setenv("DATA_DIR", dataDir) + t.Setenv("CONFIG_FILE", configFile) + + cfg, err := Load() + require.NoError(t, err) + require.Equal(t, "192.0.2.30", cfg.Server.Host) +} + +func TestLoadReturnsErrorForMissingConfigFile(t *testing.T) { + resetViperWithJWTSecret(t) + t.Setenv("CONFIG_FILE", filepath.Join(t.TempDir(), "missing.yaml")) + + _, err := Load() + require.ErrorContains(t, err, "read config error") +} + func TestLoadForBootstrapAllowsMissingJWTSecret(t *testing.T) { viper.Reset() + t.Cleanup(viper.Reset) + t.Setenv("CONFIG_FILE", "") + t.Setenv("DATA_DIR", "") t.Setenv("JWT_SECRET", "") cfg, err := LoadForBootstrap() @@ -1155,6 +1203,8 @@ func TestNormalizeStringSlice(t *testing.T) { } func TestGetServerAddressFromEnv(t *testing.T) { + t.Setenv("CONFIG_FILE", "") + t.Setenv("DATA_DIR", "") t.Setenv("SERVER_HOST", "127.0.0.1") t.Setenv("SERVER_PORT", "9090") @@ -1164,6 +1214,17 @@ func TestGetServerAddressFromEnv(t *testing.T) { } } +func TestGetServerAddressFromConfigFile(t *testing.T) { + configFile := filepath.Join(t.TempDir(), "setup.yaml") + require.NoError(t, os.WriteFile(configFile, []byte("server:\n host: 192.0.2.40\n port: 9191\n"), 0o600)) + t.Setenv("CONFIG_FILE", configFile) + t.Setenv("DATA_DIR", "") + t.Setenv("SERVER_HOST", "") + t.Setenv("SERVER_PORT", "") + + require.Equal(t, "192.0.2.40:9191", GetServerAddress()) +} + func TestValidateAbsoluteHTTPURL(t *testing.T) { if err := ValidateAbsoluteHTTPURL("https://example.com/path"); err != nil { t.Fatalf("ValidateAbsoluteHTTPURL valid url error: %v", err)