140 lines
5.5 KiB
Go
140 lines
5.5 KiB
Go
package config
|
|
|
|
import (
|
|
"os"
|
|
"testing"
|
|
)
|
|
|
|
const historicKey = "hfb-sys-2024-secret-key-32bytes!"
|
|
|
|
func TestIsProductionEnv(t *testing.T) {
|
|
cases := []struct {
|
|
in string
|
|
want bool
|
|
}{
|
|
{"production", true},
|
|
{"PRODUCTION", true},
|
|
{" production ", true},
|
|
{"development", false},
|
|
{"", false},
|
|
{"staging", false},
|
|
}
|
|
for _, c := range cases {
|
|
if got := IsProductionEnv(c.in); got != c.want {
|
|
t.Fatalf("IsProductionEnv(%q) = %v, want %v", c.in, got, c.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestLoadDatabaseConfigFromEnvironment(t *testing.T) {
|
|
t.Setenv("DATABASE_MAX_OPEN_CONNS", "80")
|
|
t.Setenv("DATABASE_MAX_IDLE_CONNS", "16")
|
|
t.Setenv("DATABASE_CONN_MAX_LIFETIME_MINUTES", "45")
|
|
t.Setenv("DATABASE_CONN_MAX_IDLE_TIME_MINUTES", "7")
|
|
t.Setenv("DATABASE_SLOW_QUERY_THRESHOLD_MS", "750")
|
|
|
|
cfg := Load()
|
|
if cfg.Database.MaxOpenConns != 80 || cfg.Database.MaxIdleConns != 16 {
|
|
t.Fatalf("database pool = %+v, want open=80 idle=16", cfg.Database)
|
|
}
|
|
if cfg.Database.ConnMaxLifetime.Minutes() != 45 || cfg.Database.ConnMaxIdleTime.Minutes() != 7 {
|
|
t.Fatalf("database lifetimes = %s/%s, want 45m/7m", cfg.Database.ConnMaxLifetime, cfg.Database.ConnMaxIdleTime)
|
|
}
|
|
if cfg.Database.SlowQueryThreshold.Milliseconds() != 750 {
|
|
t.Fatalf("slow query threshold = %s, want 750ms", cfg.Database.SlowQueryThreshold)
|
|
}
|
|
}
|
|
|
|
func TestLoadDatabaseConfigInvalidValuesUseDefaults(t *testing.T) {
|
|
t.Setenv("DATABASE_MAX_OPEN_CONNS", "0")
|
|
t.Setenv("DATABASE_MAX_IDLE_CONNS", "-1")
|
|
t.Setenv("DATABASE_CONN_MAX_LIFETIME_MINUTES", "0")
|
|
t.Setenv("DATABASE_CONN_MAX_IDLE_TIME_MINUTES", "-1")
|
|
t.Setenv("DATABASE_SLOW_QUERY_THRESHOLD_MS", "0")
|
|
|
|
cfg := Load()
|
|
if cfg.Database.MaxOpenConns != 50 || cfg.Database.MaxIdleConns != 10 {
|
|
t.Fatalf("invalid pool values = %+v, want defaults open=50 idle=10", cfg.Database)
|
|
}
|
|
if cfg.Database.ConnMaxLifetime.Minutes() != 30 || cfg.Database.ConnMaxIdleTime.Minutes() != 5 || cfg.Database.SlowQueryThreshold.Milliseconds() != 500 {
|
|
t.Fatalf("invalid database durations = %+v, want defaults", cfg.Database)
|
|
}
|
|
}
|
|
|
|
// TestFieldEncryptionLegacyKeyNonProductionDefaultsToHistoric 验证非生产环境未设置 legacy 时
|
|
// 回退到历史硬编码密钥(开发态零配置兼容旧密文)。
|
|
func TestFieldEncryptionLegacyKeyNonProductionDefaultsToHistoric(t *testing.T) {
|
|
unsetEnv(t, "FIELD_ENCRYPTION_LEGACY_KEY")
|
|
if got := fieldEncryptionLegacyKey("development"); got != historicKey {
|
|
t.Fatalf("non-production default legacy = %q, want %q", got, historicKey)
|
|
}
|
|
}
|
|
|
|
// TestFieldEncryptionLegacyKeyNonProductionExplicitOverride 验证非生产环境显式设置 legacy 时用该值。
|
|
func TestFieldEncryptionLegacyKeyNonProductionExplicitOverride(t *testing.T) {
|
|
t.Setenv("FIELD_ENCRYPTION_LEGACY_KEY", "explicit-legacy-16bytes!")
|
|
if got := fieldEncryptionLegacyKey("development"); got != "explicit-legacy-16bytes!" {
|
|
t.Fatalf("non-production explicit legacy = %q, want explicit-legacy-16bytes!", got)
|
|
}
|
|
}
|
|
|
|
// TestFieldEncryptionLegacyKeyProductionEmptyDisablesFallback 验证生产环境未设置/留空 legacy
|
|
// 返回空(禁用回退),而不是注入历史硬编码密钥。
|
|
// 这是密钥轮换闭环的关键:删除 env 必须真正关闭旧密钥。
|
|
func TestFieldEncryptionLegacyKeyProductionEmptyDisablesFallback(t *testing.T) {
|
|
t.Setenv("FIELD_ENCRYPTION_LEGACY_KEY", "")
|
|
if got := fieldEncryptionLegacyKey("production"); got != "" {
|
|
t.Fatalf("production empty legacy = %q, want empty (fallback disabled); injecting historic key breaks rotation closure", got)
|
|
}
|
|
}
|
|
|
|
// TestFieldEncryptionLegacyKeyProductionExplicitValue 验证生产环境显式设置 legacy 时用该值
|
|
// (轮换期间保留旧密钥解密存量)。
|
|
func TestFieldEncryptionLegacyKeyProductionExplicitValue(t *testing.T) {
|
|
t.Setenv("FIELD_ENCRYPTION_LEGACY_KEY", "prod-old-key-32bytes-0123456789")
|
|
if got := fieldEncryptionLegacyKey("production"); got != "prod-old-key-32bytes-0123456789" {
|
|
t.Fatalf("production explicit legacy = %q, want prod-old-key-32bytes-0123456789", got)
|
|
}
|
|
}
|
|
|
|
// TestValidateProductionSecurityRejectsPrimaryEqualsLegacy 验证生产环境 primary 等于 legacy 时报错。
|
|
func TestValidateProductionSecurityRejectsPrimaryEqualsLegacy(t *testing.T) {
|
|
t.Setenv("APP_ENV", "production")
|
|
t.Setenv("JWT_SECRET", "a-very-long-random-jwt-secret-at-least-32-bytes!!")
|
|
t.Setenv("PAYMENT_CONFIG_ENCRYPTION_KEY", "0123456789abcdef0123456789abcdef")
|
|
t.Setenv("FIELD_ENCRYPTION_KEY", "0123456789abcdef0123456789abcdef")
|
|
t.Setenv("FIELD_ENCRYPTION_LEGACY_KEY", "0123456789abcdef0123456789abcdef")
|
|
cfg := Load()
|
|
if err := cfg.ValidateProductionSecurity(); err == nil {
|
|
t.Fatal("ValidateProductionSecurity should reject FIELD_ENCRYPTION_KEY == LEGACY_KEY")
|
|
}
|
|
}
|
|
|
|
// TestValidateProductionSecurityRejectsInvalidLegacyKey 验证生产环境显式设置 legacy 时也校验长度。
|
|
func TestValidateProductionSecurityRejectsInvalidLegacyKey(t *testing.T) {
|
|
t.Setenv("APP_ENV", "PRODUCTION")
|
|
t.Setenv("JWT_SECRET", "a-very-long-random-jwt-secret-at-least-32-bytes!!")
|
|
t.Setenv("PAYMENT_CONFIG_ENCRYPTION_KEY", "0123456789abcdef0123456789abcdef")
|
|
t.Setenv("FIELD_ENCRYPTION_KEY", "abcdef0123456789abcdef0123456789")
|
|
t.Setenv("FIELD_ENCRYPTION_LEGACY_KEY", "too-short")
|
|
cfg := Load()
|
|
if err := cfg.ValidateProductionSecurity(); err == nil {
|
|
t.Fatal("ValidateProductionSecurity should reject invalid FIELD_ENCRYPTION_LEGACY_KEY")
|
|
}
|
|
}
|
|
|
|
func unsetEnv(t *testing.T, key string) {
|
|
t.Helper()
|
|
old, ok := os.LookupEnv(key)
|
|
if err := os.Unsetenv(key); err != nil {
|
|
t.Fatalf("unset %s: %v", key, err)
|
|
}
|
|
t.Cleanup(func() {
|
|
if ok {
|
|
_ = os.Setenv(key, old)
|
|
return
|
|
}
|
|
_ = os.Unsetenv(key)
|
|
})
|
|
}
|