From ba2f0f18f4bacc5a86217dec0b0dcb6030c40cb9 Mon Sep 17 00:00:00 2001 From: Kyle Mendell Date: Thu, 27 Nov 2025 14:38:06 -0600 Subject: [PATCH] feat: remove DbProvider env variable and calculate it dynamically (#1114) --- backend/internal/common/env_config.go | 85 +++++++++++----------- backend/internal/common/env_config_test.go | 33 --------- tests/setup/docker-compose-postgres.yml | 1 - 3 files changed, 43 insertions(+), 76 deletions(-) diff --git a/backend/internal/common/env_config.go b/backend/internal/common/env_config.go index 4011a36f..ffd8c154 100644 --- a/backend/internal/common/env_config.go +++ b/backend/internal/common/env_config.go @@ -38,38 +38,42 @@ const ( ) type EnvConfigSchema struct { - AppEnv AppEnv `env:"APP_ENV" options:"toLower"` - LogLevel string `env:"LOG_LEVEL" options:"toLower"` - AppURL string `env:"APP_URL" options:"toLower,trimTrailingSlash"` - DbProvider DbProvider `env:"DB_PROVIDER" options:"toLower"` - DbConnectionString string `env:"DB_CONNECTION_STRING" options:"file"` - FileBackend string `env:"FILE_BACKEND" options:"toLower"` - UploadPath string `env:"UPLOAD_PATH"` - S3Bucket string `env:"S3_BUCKET"` - S3Region string `env:"S3_REGION"` - S3Endpoint string `env:"S3_ENDPOINT"` - S3AccessKeyID string `env:"S3_ACCESS_KEY_ID"` - S3SecretAccessKey string `env:"S3_SECRET_ACCESS_KEY"` - S3ForcePathStyle bool `env:"S3_FORCE_PATH_STYLE"` - S3DisableDefaultIntegrityChecks bool `env:"S3_DISABLE_DEFAULT_INTEGRITY_CHECKS"` - EncryptionKey []byte `env:"ENCRYPTION_KEY" options:"file"` - Port string `env:"PORT"` - Host string `env:"HOST" options:"toLower"` - UnixSocket string `env:"UNIX_SOCKET"` - UnixSocketMode string `env:"UNIX_SOCKET_MODE"` - MaxMindLicenseKey string `env:"MAXMIND_LICENSE_KEY" options:"file"` - GeoLiteDBPath string `env:"GEOLITE_DB_PATH"` - GeoLiteDBUrl string `env:"GEOLITE_DB_URL"` - LocalIPv6Ranges string `env:"LOCAL_IPV6_RANGES"` - UiConfigDisabled bool `env:"UI_CONFIG_DISABLED"` - MetricsEnabled bool `env:"METRICS_ENABLED"` - TracingEnabled bool `env:"TRACING_ENABLED"` - LogJSON bool `env:"LOG_JSON"` - TrustProxy bool `env:"TRUST_PROXY"` - AuditLogRetentionDays int `env:"AUDIT_LOG_RETENTION_DAYS"` - AnalyticsDisabled bool `env:"ANALYTICS_DISABLED"` - AllowDowngrade bool `env:"ALLOW_DOWNGRADE"` - InternalAppURL string `env:"INTERNAL_APP_URL"` + AppEnv AppEnv `env:"APP_ENV" options:"toLower"` + EncryptionKey []byte `env:"ENCRYPTION_KEY" options:"file"` + AppURL string `env:"APP_URL" options:"toLower,trimTrailingSlash"` + DbProvider DbProvider + DbConnectionString string `env:"DB_CONNECTION_STRING" options:"file"` + TrustProxy bool `env:"TRUST_PROXY"` + AuditLogRetentionDays int `env:"AUDIT_LOG_RETENTION_DAYS"` + AnalyticsDisabled bool `env:"ANALYTICS_DISABLED"` + AllowDowngrade bool `env:"ALLOW_DOWNGRADE"` + InternalAppURL string `env:"INTERNAL_APP_URL"` + UiConfigDisabled bool `env:"UI_CONFIG_DISABLED"` + + FileBackend string `env:"FILE_BACKEND" options:"toLower"` + UploadPath string `env:"UPLOAD_PATH"` + S3Bucket string `env:"S3_BUCKET"` + S3Region string `env:"S3_REGION"` + S3Endpoint string `env:"S3_ENDPOINT"` + S3AccessKeyID string `env:"S3_ACCESS_KEY_ID"` + S3SecretAccessKey string `env:"S3_SECRET_ACCESS_KEY"` + S3ForcePathStyle bool `env:"S3_FORCE_PATH_STYLE"` + S3DisableDefaultIntegrityChecks bool `env:"S3_DISABLE_DEFAULT_INTEGRITY_CHECKS"` + + Port string `env:"PORT"` + Host string `env:"HOST" options:"toLower"` + UnixSocket string `env:"UNIX_SOCKET"` + UnixSocketMode string `env:"UNIX_SOCKET_MODE"` + LocalIPv6Ranges string `env:"LOCAL_IPV6_RANGES"` + + MaxMindLicenseKey string `env:"MAXMIND_LICENSE_KEY" options:"file"` + GeoLiteDBPath string `env:"GEOLITE_DB_PATH"` + GeoLiteDBUrl string `env:"GEOLITE_DB_URL"` + + LogLevel string `env:"LOG_LEVEL" options:"toLower"` + MetricsEnabled bool `env:"METRICS_ENABLED"` + TracingEnabled bool `env:"TRACING_ENABLED"` + LogJSON bool `env:"LOG_JSON"` } var EnvConfig = defaultConfig() @@ -130,17 +134,14 @@ func ValidateEnvConfig(config *EnvConfigSchema) error { return errors.New("ENCRYPTION_KEY must be at least 16 bytes long") } - switch config.DbProvider { - case DbProviderSqlite: - if config.DbConnectionString == "" { - config.DbConnectionString = defaultSqliteConnString - } - case DbProviderPostgres: - if config.DbConnectionString == "" { - return errors.New("missing required env var 'DB_CONNECTION_STRING' for Postgres database") - } + switch { + case config.DbConnectionString == "": + config.DbProvider = DbProviderSqlite + config.DbConnectionString = defaultSqliteConnString + case strings.HasPrefix(config.DbConnectionString, "postgres://") || strings.HasPrefix(config.DbConnectionString, "postgresql://"): + config.DbProvider = DbProviderPostgres default: - return errors.New("invalid DB_PROVIDER value. Must be 'sqlite' or 'postgres'") + config.DbProvider = DbProviderSqlite } parsedAppUrl, err := url.Parse(config.AppURL) diff --git a/backend/internal/common/env_config_test.go b/backend/internal/common/env_config_test.go index 6d023586..266e5437 100644 --- a/backend/internal/common/env_config_test.go +++ b/backend/internal/common/env_config_test.go @@ -31,7 +31,6 @@ func TestParseEnvConfig(t *testing.T) { t.Run("should parse valid SQLite config correctly", func(t *testing.T) { EnvConfig = defaultConfig() - t.Setenv("DB_PROVIDER", "SQLITE") // should be lowercased automatically t.Setenv("DB_CONNECTION_STRING", "file:test.db") t.Setenv("APP_URL", "HTTP://LOCALHOST:3000") @@ -43,7 +42,6 @@ func TestParseEnvConfig(t *testing.T) { t.Run("should parse valid Postgres config correctly", func(t *testing.T) { EnvConfig = defaultConfig() - t.Setenv("DB_PROVIDER", "POSTGRES") t.Setenv("DB_CONNECTION_STRING", "postgres://user:pass@localhost/db") t.Setenv("APP_URL", "https://example.com") @@ -52,20 +50,8 @@ func TestParseEnvConfig(t *testing.T) { assert.Equal(t, DbProviderPostgres, EnvConfig.DbProvider) }) - t.Run("should fail with invalid DB_PROVIDER", func(t *testing.T) { - EnvConfig = defaultConfig() - t.Setenv("DB_PROVIDER", "invalid") - t.Setenv("DB_CONNECTION_STRING", "test") - t.Setenv("APP_URL", "http://localhost:3000") - - err := parseAndValidateEnvConfig(t) - require.Error(t, err) - assert.ErrorContains(t, err, "invalid DB_PROVIDER value") - }) - t.Run("should fail when ENCRYPTION_KEY is too short", func(t *testing.T) { EnvConfig = defaultConfig() - t.Setenv("DB_PROVIDER", "sqlite") t.Setenv("DB_CONNECTION_STRING", "file:test.db") t.Setenv("APP_URL", "http://localhost:3000") t.Setenv("ENCRYPTION_KEY", "short") @@ -77,7 +63,6 @@ func TestParseEnvConfig(t *testing.T) { t.Run("should set default SQLite connection string when DB_CONNECTION_STRING is empty", func(t *testing.T) { EnvConfig = defaultConfig() - t.Setenv("DB_PROVIDER", "sqlite") t.Setenv("APP_URL", "http://localhost:3000") err := parseAndValidateEnvConfig(t) @@ -85,19 +70,8 @@ func TestParseEnvConfig(t *testing.T) { assert.Equal(t, defaultSqliteConnString, EnvConfig.DbConnectionString) }) - t.Run("should fail when Postgres DB_CONNECTION_STRING is missing", func(t *testing.T) { - EnvConfig = defaultConfig() - t.Setenv("DB_PROVIDER", "postgres") - t.Setenv("APP_URL", "http://localhost:3000") - - err := parseAndValidateEnvConfig(t) - require.Error(t, err) - assert.ErrorContains(t, err, "missing required env var 'DB_CONNECTION_STRING' for Postgres") - }) - t.Run("should fail with invalid APP_URL", func(t *testing.T) { EnvConfig = defaultConfig() - t.Setenv("DB_PROVIDER", "sqlite") t.Setenv("DB_CONNECTION_STRING", "file:test.db") t.Setenv("APP_URL", "€://not-a-valid-url") @@ -108,7 +82,6 @@ func TestParseEnvConfig(t *testing.T) { t.Run("should fail when APP_URL contains path", func(t *testing.T) { EnvConfig = defaultConfig() - t.Setenv("DB_PROVIDER", "sqlite") t.Setenv("DB_CONNECTION_STRING", "file:test.db") t.Setenv("APP_URL", "http://localhost:3000/path") @@ -119,7 +92,6 @@ func TestParseEnvConfig(t *testing.T) { t.Run("should fail with invalid INTERNAL_APP_URL", func(t *testing.T) { EnvConfig = defaultConfig() - t.Setenv("DB_PROVIDER", "sqlite") t.Setenv("DB_CONNECTION_STRING", "file:test.db") t.Setenv("INTERNAL_APP_URL", "€://not-a-valid-url") @@ -130,7 +102,6 @@ func TestParseEnvConfig(t *testing.T) { t.Run("should fail when INTERNAL_APP_URL contains path", func(t *testing.T) { EnvConfig = defaultConfig() - t.Setenv("DB_PROVIDER", "sqlite") t.Setenv("DB_CONNECTION_STRING", "file:test.db") t.Setenv("INTERNAL_APP_URL", "http://localhost:3000/path") @@ -141,7 +112,6 @@ func TestParseEnvConfig(t *testing.T) { t.Run("should parse boolean environment variables correctly", func(t *testing.T) { EnvConfig = defaultConfig() - t.Setenv("DB_PROVIDER", "sqlite") t.Setenv("DB_CONNECTION_STRING", "file:test.db") t.Setenv("APP_URL", "http://localhost:3000") t.Setenv("UI_CONFIG_DISABLED", "true") @@ -196,7 +166,6 @@ func TestParseEnvConfig(t *testing.T) { t.Run("should parse string environment variables correctly", func(t *testing.T) { EnvConfig = defaultConfig() - t.Setenv("DB_PROVIDER", "postgres") t.Setenv("DB_CONNECTION_STRING", "postgres://test") t.Setenv("APP_URL", "https://prod.example.com") t.Setenv("APP_ENV", "PRODUCTION") @@ -217,7 +186,6 @@ func TestParseEnvConfig(t *testing.T) { t.Run("should normalize file backend and default upload path", func(t *testing.T) { EnvConfig = defaultConfig() - t.Setenv("DB_PROVIDER", "sqlite") t.Setenv("DB_CONNECTION_STRING", "file:test.db") t.Setenv("APP_URL", "http://localhost:3000") t.Setenv("FILE_BACKEND", "FILESYSTEM") @@ -231,7 +199,6 @@ func TestParseEnvConfig(t *testing.T) { t.Run("should fail with invalid FILE_BACKEND value", func(t *testing.T) { EnvConfig = defaultConfig() - t.Setenv("DB_PROVIDER", "sqlite") t.Setenv("DB_CONNECTION_STRING", "file:test.db") t.Setenv("APP_URL", "http://localhost:3000") t.Setenv("FILE_BACKEND", "invalid") diff --git a/tests/setup/docker-compose-postgres.yml b/tests/setup/docker-compose-postgres.yml index 2c534f3d..7f03b6d1 100644 --- a/tests/setup/docker-compose-postgres.yml +++ b/tests/setup/docker-compose-postgres.yml @@ -21,7 +21,6 @@ services: service: pocket-id environment: - APP_ENV=test - - DB_PROVIDER=postgres - DB_CONNECTION_STRING=postgres://postgres:postgres@postgres:5432/pocket-id - FILE_BACKEND=${FILE_BACKEND} depends_on: