diff --git a/veza-backend-api/internal/config/config_test.go b/veza-backend-api/internal/config/config_test.go index 07f94f64b..05b2edf02 100644 --- a/veza-backend-api/internal/config/config_test.go +++ b/veza-backend-api/internal/config/config_test.go @@ -671,20 +671,21 @@ func TestLoadConfig_ProdValid(t *testing.T) { // Créer une config minimale valide (tous les champs requis en prod) cfg := &Config{ - Env: "production", - JWTSecret: "test-jwt-secret-key-minimum-32-characters-long", - ChatJWTSecret: "test-chat-jwt-secret-distinct-from-jwt-secret-key", - OAuthEncryptionKey: "test-oauth-encryption-key-32bytes!", - JWTIssuer: "veza-api", - JWTAudience: "veza-platform", - DatabaseURL: "postgresql://test:test@localhost:5432/test_db", - 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) + Env: "production", + JWTSecret: "test-jwt-secret-key-minimum-32-characters-long", + ChatJWTSecret: "test-chat-jwt-secret-distinct-from-jwt-secret-key", + OAuthEncryptionKey: "test-oauth-encryption-key-32bytes!", + JWTIssuer: "veza-api", + JWTAudience: "veza-platform", + DatabaseURL: "postgresql://test:test@localhost:5432/test_db", + 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) + TrackStorageBackend: "local", // v1.0.8: must be 'local' or 's3' in prod } // 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 737ee9287..88c95827a 100644 --- a/veza-backend-api/internal/config/validation_test.go +++ b/veza-backend-api/internal/config/validation_test.go @@ -406,20 +406,21 @@ func TestValidateForEnvironment_ClamAVRequiredInProduction(t *testing.T) { os.Setenv("REDIS_URL", "redis://:password@prod-redis:6379") cfg := &Config{ - Env: EnvProduction, - AppPort: 8080, - JWTSecret: strings.Repeat("a", 32), - ChatJWTSecret: strings.Repeat("b", 32), // ≠ JWTSecret (required in prod) - 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", - HyperswitchEnabled: true, // v1.0.4: payments must be on in prod + Env: EnvProduction, + AppPort: 8080, + JWTSecret: strings.Repeat("a", 32), + ChatJWTSecret: strings.Repeat("b", 32), // ≠ JWTSecret (required in prod) + 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", + HyperswitchEnabled: true, // v1.0.4: payments must be on in prod + TrackStorageBackend: "local", // v1.0.8: must be 'local' or 's3' in prod } logger, _ := zap.NewDevelopment() cfg.Logger = logger @@ -460,19 +461,20 @@ func TestValidateForEnvironment_HyperswitchRequiredInProduction(t *testing.T) { 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", + 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", + TrackStorageBackend: "local", // v1.0.8: must be 'local' or 's3' in prod } logger, _ := zap.NewDevelopment() c.Logger = logger @@ -524,20 +526,21 @@ func TestValidateForEnvironment_RedisURLRequiredInProduction(t *testing.T) { os.Setenv("CLAMAV_REQUIRED", "true") cfg := &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", // struct field is valid (default) - RateLimitLimit: 100, - RateLimitWindow: 60, - CORSOrigins: []string{"https://example.com"}, - LogLevel: "INFO", - HyperswitchEnabled: true, + 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", // struct field is valid (default) + RateLimitLimit: 100, + RateLimitWindow: 60, + CORSOrigins: []string{"https://example.com"}, + LogLevel: "INFO", + HyperswitchEnabled: true, + TrackStorageBackend: "local", // v1.0.8: must be 'local' or 's3' in prod } logger, _ := zap.NewDevelopment() cfg.Logger = logger diff --git a/veza-backend-api/migrations/986_user_subscriptions_pending_payment_index.sql b/veza-backend-api/migrations/986_user_subscriptions_pending_payment_index.sql index 576e672c8..8dae7a424 100644 --- a/veza-backend-api/migrations/986_user_subscriptions_pending_payment_index.sql +++ b/veza-backend-api/migrations/986_user_subscriptions_pending_payment_index.sql @@ -13,8 +13,18 @@ -- that haven't transitioned yet". A non-partial composite index would -- pay storage cost for every row in the table; partial keeps it -- proportional to the in-flight set. +-- +-- CONCURRENTLY is intentionally not used: the migration runner wraps +-- each migration in a single transaction (database.go:390) and +-- Postgres forbids CREATE INDEX CONCURRENTLY inside a transaction +-- block. The partial WHERE clause keeps this index tiny (only rows +-- currently in pending_payment), so the brief AccessExclusiveLock +-- taken by the non-concurrent variant resolves in milliseconds — +-- comfortably shorter than any human-noticeable migration window. +-- If pending_payment cardinality grows large enough to matter, switch +-- to a manual two-step deploy (DDL outside the migration runner). -CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_user_subscriptions_pending_payment +CREATE INDEX IF NOT EXISTS idx_user_subscriptions_pending_payment ON user_subscriptions (created_at) WHERE status = 'pending_payment';