422 lines
9.6 KiB
Go
422 lines
9.6 KiB
Go
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=disable 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")
|
|
}
|
|
}
|
|
|
|
// 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")
|
|
}
|
|
}
|