package db import ( "strings" "testing" "time" "github.com/stretchr/testify/assert" "visiona-backend/internal/config" ) // DSN / SafeTarget 為純函式,不需 DB,故不帶 dbtest tag,預設 go test 即執行。 func TestBuildDSN_Basic(t *testing.T) { cfg := config.DatabaseConfig{ Host: "db.example.com", Port: 5432, User: "appuser", Password: "s3cret", DBName: "visiona", SSLMode: "require", MaxConns: 10, MinConns: 2, } dsn := BuildDSN(cfg) assert.True(t, strings.HasPrefix(dsn, "postgres://"), "scheme 應為 postgres://,got %s", dsn) assert.Contains(t, dsn, "appuser:s3cret@db.example.com:5432/visiona") assert.Contains(t, dsn, "sslmode=require") assert.Contains(t, dsn, "pool_max_conns=10") assert.Contains(t, dsn, "pool_min_conns=2") } func TestBuildDSN_PasswordWithSpecialChars(t *testing.T) { cfg := config.DatabaseConfig{ Host: "localhost", Port: 5432, User: "user@org", Password: "p@ss:w/rd?x", DBName: "db", SSLMode: "disable", } dsn := BuildDSN(cfg) // 特殊字元應被 percent-encode,不破壞 DSN 結構(仍能被解析)。 assert.NotContains(t, dsn, "p@ss:w/rd?x", "密碼特殊字元應被 encode") assert.Contains(t, dsn, "sslmode=disable") } func TestBuildDSN_DefaultSSLMode(t *testing.T) { cfg := config.DatabaseConfig{Host: "h", Port: 5432, User: "u", DBName: "d"} dsn := BuildDSN(cfg) assert.Contains(t, dsn, "sslmode=require", "SSLMode 留空應預設 require") } func TestSafeTarget_NoCredentials(t *testing.T) { cfg := config.DatabaseConfig{ Host: "db.example.com", Port: 5432, User: "appuser", Password: "topsecret", DBName: "visiona", } target := SafeTarget(cfg) assert.Equal(t, "db.example.com:5432/visiona", target) assert.NotContains(t, target, "appuser", "SafeTarget 不應含 user") assert.NotContains(t, target, "topsecret", "SafeTarget 不應含密碼") } func TestSafeTarget_DefaultPort(t *testing.T) { cfg := config.DatabaseConfig{Host: "h", DBName: "d"} assert.Equal(t, "h:5432/d", SafeTarget(cfg)) } func TestDatabaseConfig_Enabled(t *testing.T) { cases := []struct { name string cfg config.DatabaseConfig want bool }{ {"all set", config.DatabaseConfig{Host: "h", User: "u", DBName: "d"}, true}, {"missing host", config.DatabaseConfig{User: "u", DBName: "d"}, false}, {"missing user", config.DatabaseConfig{Host: "h", DBName: "d"}, false}, {"missing dbname", config.DatabaseConfig{Host: "h", User: "u"}, false}, {"empty", config.DatabaseConfig{}, false}, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { assert.Equal(t, tc.want, tc.cfg.Enabled()) }) } } func TestRedisConfig_Enabled(t *testing.T) { assert.True(t, config.RedisConfig{Host: "h"}.Enabled()) assert.False(t, config.RedisConfig{}.Enabled()) } // migrateDSN 應把 postgres:// scheme 去掉(golang-migrate pgx5 driver 期望的形式)。 func TestMigrateDSN_StripsScheme(t *testing.T) { cfg := config.DatabaseConfig{ Host: "h", Port: 5432, User: "u", Password: "pw", DBName: "d", SSLMode: "disable", ConnTimeout: 5 * time.Second, } got := migrateDSN(cfg) assert.False(t, strings.HasPrefix(got, "postgres://"), "migrateDSN 不應保留 postgres:// prefix,got %s", got) assert.Contains(t, got, "@h:5432/d") } // Regression(塊 0 修正):migrateDSN 絕不可帶 pgxpool 專屬參數。 // golang-migrate 的 pgx5 driver 走標準 database/sql 單連線,PG server 端不認得 // pool_max_conns / pool_min_conns,會直接 FATAL(SQLSTATE 42704)導致所有 migration 失敗。 // 這個無 docker 也能跑的純函式測試,確保不會再 regression。 func TestMigrateDSN_NoPoolParams(t *testing.T) { cfg := config.DatabaseConfig{ Host: "h", Port: 5432, User: "u", Password: "pw", DBName: "d", SSLMode: "disable", MaxConns: 10, MinConns: 2, } got := migrateDSN(cfg) assert.NotContains(t, got, "pool_max_conns", "migrateDSN 不可帶 pgxpool 專屬參數 pool_max_conns,got %s", got) assert.NotContains(t, got, "pool_min_conns", "migrateDSN 不可帶 pgxpool 專屬參數 pool_min_conns,got %s", got) // 標準 libpq 參數仍須正確帶上。 assert.Contains(t, got, "sslmode=disable", "migrateDSN 應保留 sslmode,got %s", got) // 對照組:BuildDSN(pgxpool 路徑)仍應帶 pool 參數。 pool := BuildDSN(cfg) assert.Contains(t, pool, "pool_max_conns=10", "BuildDSN 應保留 pool_max_conns") assert.Contains(t, pool, "pool_min_conns=2", "BuildDSN 應保留 pool_min_conns") }