package config_test import ( "os" "path/filepath" "strings" "testing" "github.com/EthanCodeCraft/xlgo-core/config" ) func getTempDir() string { dir := os.TempDir() return filepath.Join(dir, "xlgo_test") } func setupTempFile(name, content string) (string, error) { dir := getTempDir() os.MkdirAll(dir, 0755) path := filepath.Join(dir, name) err := os.WriteFile(path, []byte(content), 0644) return path, err } func TestAppConfig(t *testing.T) { // 测试 AppConfig 方法 app := config.AppConfig{ Name: "TestApp", SiteName: "test_site", Version: "1.0.0", Env: "dev", Debug: true, BaseURL: "https://test.example.com", } // GetSiteName if app.GetSiteName() != "test_site" { t.Error("GetSiteName failed") } // IsDebug if !app.IsDebug() { t.Error("IsDebug failed") } // IsDev if !app.IsDev() { t.Error("IsDev failed") } // IsProd if app.IsProd() { t.Error("IsProd should be false for dev") } // 测试 nil 安全性 var nilApp *config.AppConfig if nilApp.GetSiteName() != "" { t.Error("nil GetSiteName should return empty") } if nilApp.IsDebug() { t.Error("nil IsDebug should return false") } } func TestAppConfigIsProd(t *testing.T) { app := config.AppConfig{Env: "prod"} if !app.IsProd() { t.Error("IsProd failed for prod") } app2 := config.AppConfig{Env: "production"} if !app2.IsProd() { t.Error("IsProd failed for production") } } func TestDatabaseConfigDSN(t *testing.T) { db := config.DatabaseConfig{ Host: "localhost", Port: 3306, User: "root", Password: "password", Name: "testdb", } dsn := db.DSN() expected := "root:password@tcp(localhost:3306)/testdb?charset=utf8mb4&parseTime=True&loc=Local" if dsn != expected { t.Errorf("DSN = %s, want %s", dsn, expected) } } func TestDatabaseConfigPostgresDSN(t *testing.T) { db := config.DatabaseConfig{ Driver: config.DriverPostgres, Host: "localhost", Port: 5432, User: "postgres", Password: "password", Name: "testdb", } dsn := db.DSN() expected := "host='localhost' port=5432 user='postgres' password='password' dbname='testdb' sslmode=prefer TimeZone='Asia/Shanghai'" if dsn != expected { t.Errorf("Postgres DSN = %s, want %s", dsn, expected) } // 显式 MySQL DSN 不受 Driver 影响 if db.MySQLDSN() == "" { t.Error("MySQLDSN should not be empty") } } func TestDatabaseConfigUnknownDriverDoesNotFallback(t *testing.T) { db := config.DatabaseConfig{ Driver: "no-such-driver", Host: "localhost", Port: 3306, User: "root", Password: "password", Name: "testdb", } if dsn := db.DSN(); dsn != "" { t.Fatalf("未知 driver 不应静默回退 MySQL,实际 DSN=%q", dsn) } cfg := &config.Config{Database: db} if err := cfg.Validate(); err == nil || !strings.Contains(err.Error(), "database.driver") { t.Fatalf("未知 driver 应在 Validate 阶段报错,实际: %v", err) } } // TestDatabaseConfigDSNPasswordEscape_M9:含特殊字符的密码须被转义,不破坏 DSN。 func TestDatabaseConfigDSNPasswordEscape_M9(t *testing.T) { // MySQL:密码含 @/:/空格 → url.QueryEscape mysql := config.DatabaseConfig{ Driver: config.DriverMySQL, Host: "localhost", Port: 3306, User: "root", Password: "p@ss w:ord", Name: "testdb", } mdsn := mysql.MySQLDSN() // 密码段应被转义,@ 不应与 DSN 的 @ 分隔符混淆 if !strings.Contains(mdsn, "root:p%40ss+w%3Aord@tcp") { t.Errorf("MySQL DSN password not escaped: %s", mdsn) } // Postgres:密码含单引号 → 翻倍转义 pg := config.DatabaseConfig{ Driver: config.DriverPostgres, Host: "localhost", Port: 5432, User: "postgres", Password: "p'ord", Name: "testdb", } pdsn := pg.PostgresDSN() if !strings.Contains(pdsn, `password='p\'ord'`) { t.Errorf("Postgres DSN password not escaped: %s", pdsn) } // Timezone 可配置 pg2 := config.DatabaseConfig{Driver: config.DriverPostgres, Host: "h", Port: 5432, User: "u", Password: "p", Name: "n", Timezone: "UTC"} if !strings.Contains(pg2.PostgresDSN(), "TimeZone='UTC'") { t.Errorf("Postgres DSN should honor Timezone=UTC: %s", pg2.PostgresDSN()) } mysql2 := config.DatabaseConfig{Driver: config.DriverMySQL, Host: "h", Port: 3306, User: "u", Password: "p", Name: "n", Timezone: "UTC"} if !strings.Contains(mysql2.MySQLDSN(), "loc=UTC") { t.Errorf("MySQL DSN should honor Timezone=UTC: %s", mysql2.MySQLDSN()) } } func TestDatabaseConfigCustomDSN(t *testing.T) { db := config.DatabaseConfig{ Driver: config.DriverPostgres, CustomDSN: "custom-connection-string", } if db.DSN() != "custom-connection-string" { t.Errorf("CustomDSN should take precedence, got %s", db.DSN()) } } func TestRedisConfigAddr(t *testing.T) { redis := config.RedisConfig{ Host: "localhost", Port: 6379, } addr := redis.Addr() if addr != "localhost:6379" { t.Errorf("Addr = %s, want localhost:6379", addr) } } func TestConfigLoad(t *testing.T) { // 创建临时配置文件 content := ` app: name: "测试应用" site_name: "test_api" version: "1.0.0" env: "dev" debug: true server: port: 8080 mode: "development" database: host: "localhost" port: 3306 user: "root" password: "test" name: "testdb" redis: host: "localhost" port: 6379 password: "" db: 0 jwt: secret: "test-secret-12345678901234567890123456789012" expire: "1h" ` tmpFile, err := setupTempFile("test_config.yaml", content) if err != nil { t.Fatalf("WriteFile error: %v", err) } defer os.Remove(tmpFile) defer os.Remove(getTempDir()) // 重置全局状态 config.Set(nil) // 加载配置 cfg, err := config.Load(tmpFile) if err != nil { t.Fatalf("Load error: %v", err) } // 验证配置 if cfg.App.Name != "测试应用" { t.Errorf("App.Name = %s", cfg.App.Name) } if cfg.App.SiteName != "test_api" { t.Errorf("App.SiteName = %s", cfg.App.SiteName) } if cfg.Server.Port != 8080 { t.Errorf("Server.Port = %d", cfg.Server.Port) } if cfg.Database.Host != "localhost" { t.Errorf("Database.Host = %s", cfg.Database.Host) } // 测试 Get cfg2 := config.Get() if cfg2 == nil { t.Error("Get returned nil") } // 测试 GetSiteName if cfg.GetSiteName() != "test_api" { t.Errorf("GetSiteName = %s", cfg.GetSiteName()) } // 测试 GetAppName if cfg.GetAppName() != "测试应用" { t.Errorf("GetAppName = %s", cfg.GetAppName()) } // 测试 IsDevelopment if !cfg.IsDevelopment() { t.Error("IsDevelopment should be true") } // 测试 IsProduction if cfg.IsProduction() { t.Error("IsProduction should be false") } // 测试 GetString/GetInt (子测试) t.Run("GetString", func(t *testing.T) { val := config.GetString("app.site_name") if val != "test_api" { t.Errorf("GetString = %s, want test_api", val) } }) t.Run("GetInt", func(t *testing.T) { port := config.GetInt("server.port") if port != 8080 { t.Errorf("GetInt = %d, want 8080", port) } }) t.Run("GetBool", func(t *testing.T) { if config.GetBool("nonexistent") != false { t.Error("GetBool should return false for nonexistent") } }) } func TestConfigGetString(_ *testing.T) { // 已在 TestConfigLoad 中作为子测试完成 // 此函数保留为占位符,避免删除后影响其他测试引用 } func TestConfigLoadReloadsDifferentFiles(t *testing.T) { first, err := setupTempFile("first_config.yaml", "app:\n name: first\nserver:\n port: 1001\n") if err != nil { t.Fatalf("WriteFile first error: %v", err) } second, err := setupTempFile("second_config.yaml", "app:\n name: second\nserver:\n port: 1002\n") if err != nil { t.Fatalf("WriteFile second error: %v", err) } defer os.Remove(first) defer os.Remove(second) cfg, err := config.Load(first) if err != nil { t.Fatalf("Load first error: %v", err) } if cfg.App.Name != "first" || cfg.Server.Port != 1001 { t.Fatalf("unexpected first config: %+v", cfg) } cfg, err = config.Load(second) if err != nil { t.Fatalf("Load second error: %v", err) } if cfg.App.Name != "second" || cfg.Server.Port != 1002 { t.Fatalf("unexpected second config: %+v", cfg) } } func TestConfigManagerIsolation(t *testing.T) { first, err := setupTempFile("manager_first.yaml", "app:\n name: manager_first\n") if err != nil { t.Fatalf("WriteFile first error: %v", err) } second, err := setupTempFile("manager_second.yaml", "app:\n name: manager_second\n") if err != nil { t.Fatalf("WriteFile second error: %v", err) } defer os.Remove(first) defer os.Remove(second) m1 := config.NewManager(first) m2 := config.NewManager(second) cfg1, err := m1.Load() if err != nil { t.Fatalf("m1 Load error: %v", err) } cfg2, err := m2.Load() if err != nil { t.Fatalf("m2 Load error: %v", err) } if cfg1.App.Name != "manager_first" || cfg2.App.Name != "manager_second" { t.Fatalf("managers should be isolated: %+v %+v", cfg1, cfg2) } } func TestConfigSet(t *testing.T) { cfg := &config.Config{ App: config.AppConfig{ Name: "Manual", SiteName: "manual_site", }, } config.Set(cfg) if config.Get().App.Name != "Manual" { t.Error("Set failed") } } func TestConfigMethodsOnNil(t *testing.T) { // 测试 nil Config 的方法安全性 var nilCfg *config.Config if nilCfg.IsDevelopment() { t.Error("nil IsDevelopment should be false") } if nilCfg.IsProduction() { t.Error("nil IsProduction should be false") } if nilCfg.GetAppName() != "" { t.Error("nil GetAppName should return empty") } if nilCfg.GetSiteName() != "" { t.Error("nil GetSiteName should return empty") } } func TestStorageConfig(t *testing.T) { cfg := config.StorageConfig{ Driver: "local", Local: config.LocalStorageConfig{ Path: "/uploads", BaseURL: "http://localhost/uploads", }, } if cfg.Driver != "local" { t.Error("StorageConfig Driver failed") } } func TestLogConfig(t *testing.T) { log := config.LogConfig{ Dir: "/logs", MaxSize: 100, MaxBackups: 5, MaxAge: 30, Compress: true, } if log.Dir != "/logs" { t.Error("LogConfig Dir failed") } } func TestUploadConfig(t *testing.T) { upload := config.UploadConfig{ MaxFileSize: 10, MaxVideoSize: 100, AllowedImageTypes: []string{"image/jpeg", "image/png"}, } if upload.MaxFileSize != 10 { t.Error("UploadConfig failed") } }