veza/veza-backend-api/internal/config/config_test.go
okinrev a3f2f2c59b P0: stabilisation backend/chat/stream + nouvelle base migrations v1
Backend Go:
- Remplacement complet des anciennes migrations par la base V1 alignée sur ORIGIN.
- Durcissement global du parsing JSON (BindAndValidateJSON + RespondWithAppError).
- Sécurisation de config.go, CORS, statuts de santé et monitoring.
- Implémentation des transactions P0 (RBAC, duplication de playlists, social toggles).
- Ajout d’un job worker structuré (emails, analytics, thumbnails) + tests associés.
- Nouvelle doc backend : AUDIT_CONFIG, BACKEND_CONFIG, AUTH_PASSWORD_RESET, JOB_WORKER_*.

Chat server (Rust):
- Refonte du pipeline JWT + sécurité, audit et rate limiting avancé.
- Implémentation complète du cycle de message (read receipts, delivered, edit/delete, typing).
- Nettoyage des panics, gestion d’erreurs robuste, logs structurés.
- Migrations chat alignées sur le schéma UUID et nouvelles features.

Stream server (Rust):
- Refonte du moteur de streaming (encoding pipeline + HLS) et des modules core.
- Transactions P0 pour les jobs et segments, garanties d’atomicité.
- Documentation détaillée de la pipeline (AUDIT_STREAM_*, DESIGN_STREAM_PIPELINE, TRANSACTIONS_P0_IMPLEMENTATION).

Documentation & audits:
- TRIAGE.md et AUDIT_STABILITY.md à jour avec l’état réel des 3 services.
- Cartographie complète des migrations et des transactions (DB_MIGRATIONS_*, DB_TRANSACTION_PLAN, AUDIT_DB_TRANSACTIONS, TRANSACTION_TESTS_PHASE3).
- Scripts de reset et de cleanup pour la lab DB et la V1.

Ce commit fige l’ensemble du travail de stabilisation P0 (UUID, backend, chat et stream) avant les phases suivantes (Coherence Guardian, WS hardening, etc.).
2025-12-06 11:14:38 +01:00

620 lines
18 KiB
Go

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")
}