From 03b30c0c295a05052d4e05d544b3cff664eeaa0d Mon Sep 17 00:00:00 2001 From: senke Date: Thu, 16 Apr 2026 14:55:18 +0200 Subject: [PATCH] fix(config): refuse boot in production when HYPERSWITCH_ENABLED=false MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit With payments disabled, the marketplace flow still completes: orders are created with status `CREATED`, the download URL is released, and no PSP call is ever made. In other words: on a misconfigured prod instance, every purchase is free. The only signal was a silent `hyperswitch_enabled=false` at boot. `ValidateForEnvironment()` (already wired at `NewConfig` line 513, before the HTTP listener binds) now rejects `APP_ENV=production` with `HyperswitchEnabled=false`. The error message names the failure mode explicitly ("effectively giving away products") rather than a terse "config invalid" — this is a revenue leak, not a typo. Dev and staging are unaffected. Tests: 3 new cases in `validation_test.go` (`TestValidateForEnvironment_HyperswitchRequiredInProduction`) + `TestLoadConfig_ProdValid` updated to set `HyperswitchEnabled: true`. `TestValidateForEnvironment_ClamAVRequiredInProduction` fixture also includes the new field so its "succeeds" sub-test still runs. Co-Authored-By: Claude Opus 4.6 (1M context) --- veza-backend-api/internal/config/config.go | 7 ++ .../internal/config/config_test.go | 10 ++- .../internal/config/validation_test.go | 80 ++++++++++++++++++- 3 files changed, 93 insertions(+), 4 deletions(-) diff --git a/veza-backend-api/internal/config/config.go b/veza-backend-api/internal/config/config.go index c5be6c67a..4a5514207 100644 --- a/veza-backend-api/internal/config/config.go +++ b/veza-backend-api/internal/config/config.go @@ -861,6 +861,13 @@ func (c *Config) ValidateForEnvironment() error { return fmt.Errorf("JWT_ISSUER and JWT_AUDIENCE must be set in production for consistent JWT validation. Set JWT_ISSUER and JWT_AUDIENCE environment variables") } + // 9. Hyperswitch must be enabled in production — otherwise the marketplace + // silently "sells" products without taking payment (orders complete as + // CREATED and files are released for free). + if !c.HyperswitchEnabled { + return fmt.Errorf("HYPERSWITCH_ENABLED must be true in production. With payments disabled, marketplace orders complete without charging, effectively giving away products. Set HYPERSWITCH_ENABLED=true and configure HYPERSWITCH_API_KEY / HYPERSWITCH_WEBHOOK_SECRET") + } + case EnvTest: // TEST: Validation adaptée aux tests // CORS peut être vide ou configuré explicitement diff --git a/veza-backend-api/internal/config/config_test.go b/veza-backend-api/internal/config/config_test.go index 087919763..07f94f64b 100644 --- a/veza-backend-api/internal/config/config_test.go +++ b/veza-backend-api/internal/config/config_test.go @@ -638,6 +638,7 @@ func TestLoadConfig_ProdValid(t *testing.T) { originalEnv := os.Getenv("APP_ENV") originalCORSOrigins := os.Getenv("CORS_ALLOWED_ORIGINS") originalClamAV := os.Getenv("CLAMAV_REQUIRED") + originalRedisURL := os.Getenv("REDIS_URL") // Nettoyer après le test defer func() { @@ -656,11 +657,17 @@ func TestLoadConfig_ProdValid(t *testing.T) { } else { os.Unsetenv("CLAMAV_REQUIRED") } + if originalRedisURL != "" { + os.Setenv("REDIS_URL", originalRedisURL) + } else { + os.Unsetenv("REDIS_URL") + } }() // Configuration pour production valide os.Setenv("APP_ENV", "production") os.Setenv("CLAMAV_REQUIRED", "true") + os.Setenv("REDIS_URL", "redis://:password@prod-redis:6379") // Créer une config minimale valide (tous les champs requis en prod) cfg := &Config{ @@ -671,12 +678,13 @@ func TestLoadConfig_ProdValid(t *testing.T) { JWTIssuer: "veza-api", JWTAudience: "veza-platform", DatabaseURL: "postgresql://test:test@localhost:5432/test_db", - RedisURL: "redis://localhost:6379", + RedisURL: "redis://:password@prod-redis: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 + HyperswitchEnabled: true, // Payments must be on in prod (v1.0.4) } // Créer un logger minimal pour la config diff --git a/veza-backend-api/internal/config/validation_test.go b/veza-backend-api/internal/config/validation_test.go index 733fce4cb..323d4e3ce 100644 --- a/veza-backend-api/internal/config/validation_test.go +++ b/veza-backend-api/internal/config/validation_test.go @@ -389,14 +389,21 @@ func TestValidateNoBypassFlagsInProduction(t *testing.T) { // TestValidateForEnvironment_ClamAVRequiredInProduction verifies that CLAMAV_REQUIRED=false fails in production (P1.4) func TestValidateForEnvironment_ClamAVRequiredInProduction(t *testing.T) { - orig := os.Getenv("CLAMAV_REQUIRED") + origClamAV := os.Getenv("CLAMAV_REQUIRED") + origRedis := os.Getenv("REDIS_URL") defer func() { - if orig != "" { - os.Setenv("CLAMAV_REQUIRED", orig) + if origClamAV != "" { + os.Setenv("CLAMAV_REQUIRED", origClamAV) } else { os.Unsetenv("CLAMAV_REQUIRED") } + if origRedis != "" { + os.Setenv("REDIS_URL", origRedis) + } else { + os.Unsetenv("REDIS_URL") + } }() + os.Setenv("REDIS_URL", "redis://:password@prod-redis:6379") cfg := &Config{ Env: EnvProduction, @@ -412,6 +419,7 @@ func TestValidateForEnvironment_ClamAVRequiredInProduction(t *testing.T) { RateLimitWindow: 60, CORSOrigins: []string{"https://example.com"}, LogLevel: "INFO", + HyperswitchEnabled: true, // v1.0.4: payments must be on in prod } logger, _ := zap.NewDevelopment() cfg.Logger = logger @@ -430,6 +438,72 @@ func TestValidateForEnvironment_ClamAVRequiredInProduction(t *testing.T) { }) } +// TestValidateForEnvironment_HyperswitchRequiredInProduction verifies that +// HYPERSWITCH_ENABLED=false fails in production (v1.0.4 marketplace-gratuit fix). +func TestValidateForEnvironment_HyperswitchRequiredInProduction(t *testing.T) { + origClamAV := os.Getenv("CLAMAV_REQUIRED") + origRedis := os.Getenv("REDIS_URL") + defer func() { + if origClamAV != "" { + os.Setenv("CLAMAV_REQUIRED", origClamAV) + } else { + os.Unsetenv("CLAMAV_REQUIRED") + } + if origRedis != "" { + os.Setenv("REDIS_URL", origRedis) + } else { + os.Unsetenv("REDIS_URL") + } + }() + os.Setenv("CLAMAV_REQUIRED", "true") + os.Setenv("REDIS_URL", "redis://:password@prod-redis:6379") + + baseCfg := func() *Config { + c := &Config{ + Env: EnvProduction, + AppPort: 8080, + JWTSecret: strings.Repeat("a", 32), + ChatJWTSecret: strings.Repeat("b", 32), + OAuthEncryptionKey: strings.Repeat("c", 32), + JWTIssuer: "veza-api", + JWTAudience: "veza-platform", + DatabaseURL: "postgresql://user:pass@localhost:5432/db", + RedisURL: "redis://localhost:6379", + RateLimitLimit: 100, + RateLimitWindow: 60, + CORSOrigins: []string{"https://example.com"}, + LogLevel: "INFO", + } + logger, _ := zap.NewDevelopment() + c.Logger = logger + return c + } + + t.Run("production with HYPERSWITCH_ENABLED=false fails", func(t *testing.T) { + cfg := baseCfg() + cfg.HyperswitchEnabled = false + err := cfg.ValidateForEnvironment() + require.Error(t, err) + assert.Contains(t, err.Error(), "HYPERSWITCH_ENABLED must be true in production") + }) + + t.Run("production with HYPERSWITCH_ENABLED=true succeeds", func(t *testing.T) { + cfg := baseCfg() + cfg.HyperswitchEnabled = true + err := cfg.ValidateForEnvironment() + require.NoError(t, err) + }) + + t.Run("non-production is unaffected", func(t *testing.T) { + cfg := baseCfg() + cfg.Env = EnvDevelopment + cfg.HyperswitchEnabled = false + // Dev doesn't require HyperswitchEnabled — marketplace disabled is fine locally. + err := cfg.ValidateForEnvironment() + require.NoError(t, err) + }) +} + // TestValidateForEnvironment_ChatJWTSecretInProduction verifies CHAT_JWT_SECRET must differ from JWT_SECRET in production (v0.902) func TestValidateForEnvironment_ChatJWTSecretInProduction(t *testing.T) { secret := strings.Repeat("a", 32)