package config import ( "os" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.uber.org/zap" ) func TestLoad(t *testing.T) { // Sauvegarder les valeurs originales originalDBPassword := os.Getenv("DB_PASSWORD") originalJWTSecret := os.Getenv("JWT_SECRET") originalAppPort := os.Getenv("APP_PORT") // Nettoyer après le test defer func() { if originalDBPassword != "" { os.Setenv("DB_PASSWORD", originalDBPassword) } else { os.Unsetenv("DB_PASSWORD") } if originalJWTSecret != "" { os.Setenv("JWT_SECRET", originalJWTSecret) } else { os.Unsetenv("JWT_SECRET") } if originalAppPort != "" { os.Setenv("APP_PORT", originalAppPort) } else { os.Unsetenv("APP_PORT") } }() // Définir les variables requises os.Setenv("DB_PASSWORD", "test_password") os.Setenv("JWT_SECRET", "test_secret") config, err := Load() require.NoError(t, err) require.NotNil(t, config) // Vérifier les valeurs par défaut assert.Equal(t, 8080, config.AppPort) assert.Equal(t, "development", config.AppEnv) assert.Equal(t, "localhost", config.DBHost) assert.Equal(t, 5432, config.DBPort) assert.Equal(t, "veza", config.DBUser) assert.Equal(t, "veza_db", config.DBName) assert.Equal(t, "redis://localhost:6379", config.RedisURL) // Vérifier les valeurs requises assert.Equal(t, "test_password", config.DBPassword) assert.Equal(t, "test_secret", config.JWTSecret) } func TestLoad_WithCustomValues(t *testing.T) { // Sauvegarder les valeurs originales originalDBPassword := os.Getenv("DB_PASSWORD") originalJWTSecret := os.Getenv("JWT_SECRET") originalAppPort := os.Getenv("APP_PORT") originalDBHost := os.Getenv("DB_HOST") originalDBPort := os.Getenv("DB_PORT") // Nettoyer après le test defer func() { if originalDBPassword != "" { os.Setenv("DB_PASSWORD", originalDBPassword) } else { os.Unsetenv("DB_PASSWORD") } if originalJWTSecret != "" { os.Setenv("JWT_SECRET", originalJWTSecret) } else { os.Unsetenv("JWT_SECRET") } if originalAppPort != "" { os.Setenv("APP_PORT", originalAppPort) } else { os.Unsetenv("APP_PORT") } if originalDBHost != "" { os.Setenv("DB_HOST", originalDBHost) } else { os.Unsetenv("DB_HOST") } if originalDBPort != "" { os.Setenv("DB_PORT", originalDBPort) } else { os.Unsetenv("DB_PORT") } }() // Définir des valeurs personnalisées os.Setenv("DB_PASSWORD", "custom_password") os.Setenv("JWT_SECRET", "custom_secret") os.Setenv("APP_PORT", "9090") os.Setenv("DB_HOST", "custom_host") os.Setenv("DB_PORT", "3306") config, err := Load() require.NoError(t, err) assert.Equal(t, 9090, config.AppPort) assert.Equal(t, "custom_host", config.DBHost) assert.Equal(t, 3306, config.DBPort) assert.Equal(t, "custom_password", config.DBPassword) assert.Equal(t, "custom_secret", config.JWTSecret) } func TestLoad_MissingRequiredVariable_DBPassword(t *testing.T) { // Sauvegarder les valeurs originales originalDBPassword := os.Getenv("DB_PASSWORD") originalJWTSecret := os.Getenv("JWT_SECRET") // Nettoyer après le test defer func() { if originalDBPassword != "" { os.Setenv("DB_PASSWORD", originalDBPassword) } else { os.Unsetenv("DB_PASSWORD") } if originalJWTSecret != "" { os.Setenv("JWT_SECRET", originalJWTSecret) } else { os.Unsetenv("JWT_SECRET") } }() // Supprimer les variables requises os.Unsetenv("DB_PASSWORD") os.Setenv("JWT_SECRET", "test_secret") // Devrait paniquer assert.Panics(t, func() { _, _ = Load() }, "Should panic when DB_PASSWORD is missing") } func TestLoad_MissingRequiredVariable_JWTSecret(t *testing.T) { // Sauvegarder les valeurs originales originalDBPassword := os.Getenv("DB_PASSWORD") originalJWTSecret := os.Getenv("JWT_SECRET") // Nettoyer après le test defer func() { if originalDBPassword != "" { os.Setenv("DB_PASSWORD", originalDBPassword) } else { os.Unsetenv("DB_PASSWORD") } if originalJWTSecret != "" { os.Setenv("JWT_SECRET", originalJWTSecret) } else { os.Unsetenv("JWT_SECRET") } }() // Supprimer les variables requises os.Setenv("DB_PASSWORD", "test_password") os.Unsetenv("JWT_SECRET") // Devrait paniquer assert.Panics(t, func() { _, _ = Load() }, "Should panic when JWT_SECRET is missing") } func TestGetEnv(t *testing.T) { // Sauvegarder la valeur originale originalValue := os.Getenv("TEST_VAR") defer func() { if originalValue != "" { os.Setenv("TEST_VAR", originalValue) } else { os.Unsetenv("TEST_VAR") } }() // Test avec valeur définie os.Setenv("TEST_VAR", "test_value") assert.Equal(t, "test_value", getEnv("TEST_VAR", "default")) // Test sans valeur (devrait retourner défaut) os.Unsetenv("TEST_VAR") assert.Equal(t, "default", getEnv("TEST_VAR", "default")) } func TestGetEnvInt(t *testing.T) { // Sauvegarder la valeur originale originalValue := os.Getenv("TEST_INT") defer func() { if originalValue != "" { os.Setenv("TEST_INT", originalValue) } else { os.Unsetenv("TEST_INT") } }() // Test avec valeur entière valide os.Setenv("TEST_INT", "42") assert.Equal(t, 42, getEnvInt("TEST_INT", 10)) // Test sans valeur (devrait retourner défaut) os.Unsetenv("TEST_INT") assert.Equal(t, 10, getEnvInt("TEST_INT", 10)) // Test avec valeur invalide (devrait retourner défaut) os.Setenv("TEST_INT", "not_a_number") assert.Equal(t, 10, getEnvInt("TEST_INT", 10)) } func TestGetEnvRequired(t *testing.T) { // Sauvegarder la valeur originale originalValue := os.Getenv("TEST_REQUIRED") defer func() { if originalValue != "" { os.Setenv("TEST_REQUIRED", originalValue) } else { os.Unsetenv("TEST_REQUIRED") } }() // Test avec valeur définie os.Setenv("TEST_REQUIRED", "required_value") assert.Equal(t, "required_value", getEnvRequired("TEST_REQUIRED")) // Test sans valeur (devrait paniquer) os.Unsetenv("TEST_REQUIRED") assert.Panics(t, func() { _ = getEnvRequired("TEST_REQUIRED") }, "Should panic when required variable is missing") } func TestLoad_DefaultValues(t *testing.T) { // Sauvegarder les valeurs originales originalDBPassword := os.Getenv("DB_PASSWORD") originalJWTSecret := os.Getenv("JWT_SECRET") originalAppEnv := os.Getenv("APP_ENV") originalRedisURL := os.Getenv("REDIS_URL") // Nettoyer après le test defer func() { if originalDBPassword != "" { os.Setenv("DB_PASSWORD", originalDBPassword) } else { os.Unsetenv("DB_PASSWORD") } if originalJWTSecret != "" { os.Setenv("JWT_SECRET", originalJWTSecret) } else { os.Unsetenv("JWT_SECRET") } if originalAppEnv != "" { os.Setenv("APP_ENV", originalAppEnv) } else { os.Unsetenv("APP_ENV") } if originalRedisURL != "" { os.Setenv("REDIS_URL", originalRedisURL) } else { os.Unsetenv("REDIS_URL") } }() // Définir seulement les variables requises os.Setenv("DB_PASSWORD", "test") os.Setenv("JWT_SECRET", "secret") // Supprimer les variables optionnelles pour tester les valeurs par défaut os.Unsetenv("APP_ENV") os.Unsetenv("REDIS_URL") config, err := Load() require.NoError(t, err) // Vérifier que les valeurs par défaut sont utilisées assert.Equal(t, "development", config.AppEnv) assert.Equal(t, "redis://localhost:6379", config.RedisURL) } // TestNewConfig_RequiresJWTSecret vérifie que NewConfig() refuse de démarrer sans JWT_SECRET // Ce test valide la correction de sécurité qui empêche l'utilisation d'une valeur par défaut hardcodée func TestNewConfig_RequiresJWTSecret(t *testing.T) { // Sauvegarder les valeurs originales originalJWTSecret := os.Getenv("JWT_SECRET") originalDatabaseURL := os.Getenv("DATABASE_URL") // Nettoyer après le test defer func() { if originalJWTSecret != "" { os.Setenv("JWT_SECRET", originalJWTSecret) } else { os.Unsetenv("JWT_SECRET") } if originalDatabaseURL != "" { os.Setenv("DATABASE_URL", originalDatabaseURL) } else { os.Unsetenv("DATABASE_URL") } }() // Supprimer JWT_SECRET - devrait causer un panic os.Unsetenv("JWT_SECRET") // Définir DATABASE_URL pour éviter un panic sur cette variable (on teste seulement JWT_SECRET) os.Setenv("DATABASE_URL", "postgresql://test:test@localhost:5432/test_db") // Devrait paniquer car JWT_SECRET est requis assert.Panics(t, func() { _, _ = NewConfig() }, "NewConfig should panic when JWT_SECRET is missing") } // TestNewConfig_RequiresDatabaseURL vérifie que NewConfig() refuse de démarrer sans DATABASE_URL // Ce test valide la correction de sécurité qui empêche l'utilisation d'une valeur par défaut avec credentials func TestNewConfig_RequiresDatabaseURL(t *testing.T) { // Sauvegarder les valeurs originales originalJWTSecret := os.Getenv("JWT_SECRET") originalDatabaseURL := os.Getenv("DATABASE_URL") // Nettoyer après le test defer func() { if originalJWTSecret != "" { os.Setenv("JWT_SECRET", originalJWTSecret) } else { os.Unsetenv("JWT_SECRET") } if originalDatabaseURL != "" { os.Setenv("DATABASE_URL", originalDatabaseURL) } else { os.Unsetenv("DATABASE_URL") } }() // Définir JWT_SECRET (minimum 32 caractères pour passer la validation) os.Setenv("JWT_SECRET", "test-jwt-secret-key-minimum-32-characters-long") // Supprimer DATABASE_URL - devrait causer un panic os.Unsetenv("DATABASE_URL") // Devrait paniquer car DATABASE_URL est requis assert.Panics(t, func() { _, _ = NewConfig() }, "NewConfig should panic when DATABASE_URL is missing") } // ============================================================================ // P0-SECURITY: Tests pour la sécurisation de la configuration CORS // ============================================================================ // TestLoadConfig_DevDefaults vérifie que les defaults dev sont corrects (P0-SECURITY) func TestLoadConfig_DevDefaults(t *testing.T) { // Sauvegarder les valeurs originales originalEnv := os.Getenv("APP_ENV") originalJWTSecret := os.Getenv("JWT_SECRET") originalDatabaseURL := os.Getenv("DATABASE_URL") originalCORSOrigins := os.Getenv("CORS_ALLOWED_ORIGINS") // Nettoyer après le test defer func() { if originalEnv != "" { os.Setenv("APP_ENV", originalEnv) } else { os.Unsetenv("APP_ENV") } if originalJWTSecret != "" { os.Setenv("JWT_SECRET", originalJWTSecret) } else { os.Unsetenv("JWT_SECRET") } if originalDatabaseURL != "" { os.Setenv("DATABASE_URL", originalDatabaseURL) } else { os.Unsetenv("DATABASE_URL") } if originalCORSOrigins != "" { os.Setenv("CORS_ALLOWED_ORIGINS", originalCORSOrigins) } else { os.Unsetenv("CORS_ALLOWED_ORIGINS") } }() // Configuration pour développement os.Setenv("APP_ENV", "development") os.Setenv("JWT_SECRET", "test-jwt-secret-key-minimum-32-characters-long") os.Setenv("DATABASE_URL", "postgresql://test:test@localhost:5432/test_db") os.Unsetenv("CORS_ALLOWED_ORIGINS") // Pas défini pour tester les defaults // Note: NewConfig() nécessite Redis et DB, donc on teste seulement getCORSOrigins origins := getCORSOrigins("development") require.NotEmpty(t, origins, "Development should have default CORS origins") assert.Contains(t, origins, "http://localhost:3000", "Should include localhost:3000") assert.Contains(t, origins, "http://127.0.0.1:3000", "Should include 127.0.0.1:3000") assert.NotContains(t, origins, "*", "Should not contain wildcard") } // TestLoadConfig_ProdMissingCritical vérifie que prod refuse si CORS manquant (P0-SECURITY) func TestLoadConfig_ProdMissingCritical(t *testing.T) { // Sauvegarder les valeurs originales originalEnv := os.Getenv("APP_ENV") originalJWTSecret := os.Getenv("JWT_SECRET") originalDatabaseURL := os.Getenv("DATABASE_URL") originalCORSOrigins := os.Getenv("CORS_ALLOWED_ORIGINS") // Nettoyer après le test defer func() { if originalEnv != "" { os.Setenv("APP_ENV", originalEnv) } else { os.Unsetenv("APP_ENV") } if originalJWTSecret != "" { os.Setenv("JWT_SECRET", originalJWTSecret) } else { os.Unsetenv("JWT_SECRET") } if originalDatabaseURL != "" { os.Setenv("DATABASE_URL", originalDatabaseURL) } else { os.Unsetenv("DATABASE_URL") } if originalCORSOrigins != "" { os.Setenv("CORS_ALLOWED_ORIGINS", originalCORSOrigins) } else { os.Unsetenv("CORS_ALLOWED_ORIGINS") } }() // Configuration pour production sans CORS os.Setenv("APP_ENV", "production") os.Setenv("JWT_SECRET", "test-jwt-secret-key-minimum-32-characters-long") os.Setenv("DATABASE_URL", "postgresql://test:test@localhost:5432/test_db") os.Unsetenv("CORS_ALLOWED_ORIGINS") // Manquant intentionnellement // Créer une config minimale pour tester la validation cfg := &Config{ Env: "production", JWTSecret: "test-jwt-secret-key-minimum-32-characters-long", DatabaseURL: "postgresql://test:test@localhost:5432/test_db", RedisURL: "redis://localhost:6379", AppPort: 8080, LogLevel: "INFO", RateLimitLimit: 100, // Valeur valide pour passer Validate() RateLimitWindow: 60, // Valeur valide pour passer Validate() CORSOrigins: []string{}, // Vide - devrait échouer en prod } // Créer un logger minimal pour la config logger, _ := zap.NewDevelopment() cfg.Logger = logger // La validation devrait échouer err := cfg.ValidateForEnvironment() require.Error(t, err, "Production config should fail validation when CORS_ALLOWED_ORIGINS is empty") assert.Contains(t, err.Error(), "CORS_ALLOWED_ORIGINS is required", "Error should mention CORS requirement") } // TestLoadConfig_ProdWildcard vérifie que prod refuse le wildcard (P0-SECURITY) func TestLoadConfig_ProdWildcard(t *testing.T) { // Sauvegarder les valeurs originales originalEnv := os.Getenv("APP_ENV") originalCORSOrigins := os.Getenv("CORS_ALLOWED_ORIGINS") // Nettoyer après le test defer func() { if originalEnv != "" { os.Setenv("APP_ENV", originalEnv) } else { os.Unsetenv("APP_ENV") } if originalCORSOrigins != "" { os.Setenv("CORS_ALLOWED_ORIGINS", originalCORSOrigins) } else { os.Unsetenv("CORS_ALLOWED_ORIGINS") } }() // Configuration pour production avec wildcard os.Setenv("APP_ENV", "production") // Créer une config minimale avec wildcard cfg := &Config{ Env: "production", JWTSecret: "test-jwt-secret-key-minimum-32-characters-long", DatabaseURL: "postgresql://test:test@localhost:5432/test_db", RedisURL: "redis://localhost:6379", AppPort: 8080, LogLevel: "INFO", RateLimitLimit: 100, // Valeur valide pour passer Validate() RateLimitWindow: 60, // Valeur valide pour passer Validate() CORSOrigins: []string{"*"}, // Wildcard - devrait échouer en prod } // Créer un logger minimal pour la config logger, _ := zap.NewDevelopment() cfg.Logger = logger // La validation devrait échouer err := cfg.ValidateForEnvironment() require.Error(t, err, "Production config should fail validation when CORS contains wildcard") assert.Contains(t, err.Error(), "wildcard", "Error should mention wildcard prohibition") } // TestLoadConfig_ProdValid vérifie qu'une config prod valide passe (P0-SECURITY) func TestLoadConfig_ProdValid(t *testing.T) { // Sauvegarder les valeurs originales originalEnv := os.Getenv("APP_ENV") originalCORSOrigins := os.Getenv("CORS_ALLOWED_ORIGINS") // Nettoyer après le test defer func() { if originalEnv != "" { os.Setenv("APP_ENV", originalEnv) } else { os.Unsetenv("APP_ENV") } if originalCORSOrigins != "" { os.Setenv("CORS_ALLOWED_ORIGINS", originalCORSOrigins) } else { os.Unsetenv("CORS_ALLOWED_ORIGINS") } }() // Configuration pour production valide os.Setenv("APP_ENV", "production") // Créer une config minimale valide cfg := &Config{ Env: "production", JWTSecret: "test-jwt-secret-key-minimum-32-characters-long", DatabaseURL: "postgresql://test:test@localhost:5432/test_db", RedisURL: "redis://localhost:6379", AppPort: 8080, LogLevel: "INFO", RateLimitLimit: 100, // Valeur valide pour passer Validate() RateLimitWindow: 60, // Valeur valide pour passer Validate() CORSOrigins: []string{"https://app.veza.com", "https://www.veza.com"}, // Valide - pas de wildcard } // Créer un logger minimal pour la config logger, _ := zap.NewDevelopment() cfg.Logger = logger // La validation devrait passer err := cfg.ValidateForEnvironment() assert.NoError(t, err, "Valid production config should pass validation") } // TestGetCORSOrigins_EnvironmentDefaults teste les defaults selon l'environnement (P0-SECURITY) func TestGetCORSOrigins_EnvironmentDefaults(t *testing.T) { tests := []struct { name string env string expected []string }{ { name: "development defaults", env: "development", expected: []string{"http://localhost:3000", "http://127.0.0.1:3000", "http://localhost:5173", "http://127.0.0.1:5173"}, }, { name: "staging defaults", env: "staging", expected: []string{"http://localhost:3000", "http://127.0.0.1:3000", "http://localhost:5173", "http://127.0.0.1:5173"}, }, { name: "production no defaults", env: "production", expected: []string{}, }, { name: "test no defaults", env: "test", expected: []string{}, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { // Sauvegarder CORS_ALLOWED_ORIGINS originalCORSOrigins := os.Getenv("CORS_ALLOWED_ORIGINS") defer func() { if originalCORSOrigins != "" { os.Setenv("CORS_ALLOWED_ORIGINS", originalCORSOrigins) } else { os.Unsetenv("CORS_ALLOWED_ORIGINS") } }() // S'assurer que CORS_ALLOWED_ORIGINS n'est pas défini os.Unsetenv("CORS_ALLOWED_ORIGINS") origins := getCORSOrigins(tt.env) assert.Equal(t, tt.expected, origins, "CORS origins should match expected defaults for %s", tt.env) }) } } // TestGetCORSOrigins_ExplicitValue teste que les valeurs explicites sont utilisées (P0-SECURITY) func TestGetCORSOrigins_ExplicitValue(t *testing.T) { // Sauvegarder CORS_ALLOWED_ORIGINS originalCORSOrigins := os.Getenv("CORS_ALLOWED_ORIGINS") defer func() { if originalCORSOrigins != "" { os.Setenv("CORS_ALLOWED_ORIGINS", originalCORSOrigins) } else { os.Unsetenv("CORS_ALLOWED_ORIGINS") } }() // Définir explicitement CORS_ALLOWED_ORIGINS os.Setenv("CORS_ALLOWED_ORIGINS", "https://example.com,https://app.example.com") origins := getCORSOrigins("production") assert.Equal(t, []string{"https://example.com", "https://app.example.com"}, origins, "Should use explicit CORS_ALLOWED_ORIGINS value") }