package test import ( "RenewCMS/internal/infrastructure/config" "RenewCMS/internal/infrastructure/persistence/database" "fmt" "net/url" "strings" "testing" "time" "github.com/jackc/pgx/v5/pgconn" ) const postgresTestPassword = "p@ss w'rd/\"x:42%" func setPostgresEnv(t *testing.T) { t.Helper() setBaseSettingsEnv(t) t.Setenv("DB_DRIVER", "postgres") t.Setenv("DB_HOST", "db.local") t.Setenv("DB_USER", "app") t.Setenv("DB_PASSWORD", postgresTestPassword) t.Setenv("DB_NAME", "cms") for _, optional := range []string{"DB_PORT", "DB_TLS", "DB_SSLMODE", "DB_MAX_OPEN_CONNS", "DB_MAX_IDLE_CONNS", "DB_CONN_MAX_LIFETIME_SECONDS", "DATA_DIR"} { t.Setenv(optional, "") } } func TestPostgresDriverSelection(t *testing.T) { t.Run("Driver selection", func(t *testing.T) { cases := []struct { value string driver string }{ {"postgres", "postgres"}, {"postgresql", "postgres"}, {"mysql", "mysql"}, {"oracle", "oracle"}, } for _, tc := range cases { t.Run(fmt.Sprintf("DB_DRIVER %q", tc.value), func(t *testing.T) { setPostgresEnv(t) t.Setenv("DB_DRIVER", tc.value) settings, err := config.NewSettingsFromEnv() if err != nil || settings.Database.Driver != tc.driver { t.Errorf("expected driver %q, got %+v err %v", tc.driver, settings.Database, err) } }) } }) } func TestPostgresEnvironmentValidation(t *testing.T) { complete := map[string]string{"DB_HOST": "h", "DB_USER": "u", "DB_PASSWORD": "p", "DB_NAME": "n"} t.Run("Missing PostgreSQL variable", func(t *testing.T) { for _, driver := range []string{"postgres", "postgresql"} { for _, missing := range []string{"DB_HOST", "DB_USER", "DB_PASSWORD", "DB_NAME"} { t.Run(driver+" "+missing, func(t *testing.T) { values := map[string]string{} for name, value := range complete { if name != missing { values[name] = value } } err := config.MissingEnvVarError(config.DatabaseRequiredEnvVars(driver), lookupFrom(values)) expected := "Environment variable " + missing + " is not set" if err == nil || err.Error() != expected { t.Errorf("expected %q, got %v", expected, err) } }) } } }) t.Run("all PostgreSQL variables set passes validation", func(t *testing.T) { if err := config.MissingEnvVarError(config.DatabaseRequiredEnvVars("postgres"), lookupFrom(complete)); err != nil { t.Errorf("expected success, got %v", err) } }) t.Run("SQLite and MySQL do not require PostgreSQL-only variables", func(t *testing.T) { for _, driver := range []string{"sqlite", "mysql"} { setMySQLEnv(t) t.Setenv("DB_DRIVER", driver) t.Setenv("DB_SSLMODE", "maybe") settings, err := config.NewSettingsFromEnv() if err != nil || settings.Database.SSLMode != "" { t.Errorf("driver %q: DB_SSLMODE must be ignored, got %+v err %v", driver, settings.Database, err) } } }) } func TestPostgresSettingsFromEnv(t *testing.T) { t.Run("defaults apply when the optional variables are empty", func(t *testing.T) { setPostgresEnv(t) settings, err := config.NewSettingsFromEnv() if err != nil { t.Fatalf("settings: %v", err) } database := settings.Database if database.Port != "5432" || database.SSLMode != "disable" || database.MaxOpenConns != 10 || database.MaxIdleConns != 5 || database.ConnMaxLifetime != 300*time.Second { t.Errorf("unexpected defaults %+v", database) } if database.Host != "db.local" || database.User != "app" || database.Password != postgresTestPassword || database.Name != "cms" { t.Errorf("unexpected connection fields %+v", database) } }) t.Run("MySQL keeps its default port", func(t *testing.T) { setMySQLEnv(t) settings, err := config.NewSettingsFromEnv() if err != nil || settings.Database.Port != "3306" { t.Errorf("unexpected settings %+v err %v", settings.Database, err) } }) t.Run("Invalid SSL mode is rejected", func(t *testing.T) { setPostgresEnv(t) t.Setenv("DB_SSLMODE", "maybe") _, err := config.NewSettingsFromEnv() if err == nil { t.Fatal("expected an error") } for _, expected := range []string{"DB_SSLMODE", "disable", "require", "verify-ca", "verify-full"} { if !strings.Contains(err.Error(), expected) { t.Errorf("error %q must mention %q", err, expected) } } }) t.Run("every accepted SSL mode is read", func(t *testing.T) { for _, mode := range []string{"disable", "require", "verify-ca", "verify-full"} { setPostgresEnv(t) t.Setenv("DB_SSLMODE", mode) settings, err := config.NewSettingsFromEnv() if err != nil || settings.Database.SSLMode != mode { t.Errorf("mode %q: settings %+v, err %v", mode, settings.Database, err) } } }) t.Run("Connection pool variables are read", func(t *testing.T) { setPostgresEnv(t) t.Setenv("DB_MAX_OPEN_CONNS", "4") t.Setenv("DB_MAX_IDLE_CONNS", "2") t.Setenv("DB_CONN_MAX_LIFETIME_SECONDS", "60") settings, err := config.NewSettingsFromEnv() database := settings.Database if err != nil || database.MaxOpenConns != 4 || database.MaxIdleConns != 2 || database.ConnMaxLifetime != 60*time.Second { t.Errorf("unexpected settings %+v, err %v", database, err) } }) t.Run("Data directory with PostgreSQL", func(t *testing.T) { setPostgresEnv(t) t.Setenv("DATA_DIR", "/data") t.Setenv("DB_FILE", "./elsewhere/ignored.db") settings, err := config.NewSettingsFromEnv() if err != nil || strings.ReplaceAll(settings.DataDir(), "\\", "/") != "/data" { t.Errorf("unexpected data dir %q err %v", settings.DataDir(), err) } }) } func TestPostgresDSN(t *testing.T) { t.Run("Connection string built from variables", func(t *testing.T) { cases := []struct { host, port, expectedPort, sslmode, expectedSslmode string }{ {"db.local", "", "5432", "", "disable"}, {"db.local", "5433", "5433", "require", "require"}, {"pg", "5432", "5432", "verify-full", "verify-full"}, } for _, tc := range cases { t.Run(tc.host+":"+tc.port+" sslmode "+tc.sslmode, func(t *testing.T) { port, sslmode := tc.port, tc.sslmode if port == "" { port = config.DefaultPostgresPort } if sslmode == "" { sslmode = config.DefaultDatabaseSSLMode } dsn := database.PostgresDSN(database.PostgresConfig{Host: tc.host, Port: port, User: "app", Password: postgresTestPassword, Name: "cms", SSLMode: sslmode}) parsed, err := pgconn.ParseConfig(dsn) if err != nil { t.Fatalf("parse: %v", err) } if fmt.Sprintf("%s:%d", parsed.Host, parsed.Port) != tc.host+":"+tc.expectedPort || parsed.Database != "cms" || parsed.User != "app" { t.Errorf("unexpected target %s:%d db %q user %q", parsed.Host, parsed.Port, parsed.Database, parsed.User) } if parsed.Password != postgresTestPassword { t.Errorf("the password must survive the DSN, got %q", parsed.Password) } if parsed.RuntimeParams["timezone"] != "UTC" { t.Errorf("the session time zone must be UTC, got %v", parsed.RuntimeParams) } if tc.expectedSslmode == "disable" && parsed.TLSConfig != nil { t.Errorf("sslmode disable must not configure TLS") } if tc.expectedSslmode != "disable" && parsed.TLSConfig == nil { t.Errorf("sslmode %s must configure TLS", tc.expectedSslmode) } if query := mustParseQuery(t, dsn); query.Get("sslmode") != tc.expectedSslmode { t.Errorf("expected sslmode %q, got %q", tc.expectedSslmode, query.Get("sslmode")) } if parsed.ConnectTimeout <= 0 || parsed.ConnectTimeout > 10*time.Second { t.Errorf("connect timeout must be short, got %v", parsed.ConnectTimeout) } }) } }) } func mustParseQuery(t *testing.T, dsn string) url.Values { t.Helper() parsed, err := url.Parse(dsn) if err != nil { t.Fatal(err) } return parsed.Query() } func TestPostgresFailFast(t *testing.T) { t.Run("nothing listens on DB_PORT", func(t *testing.T) { port := closedLocalPort(t) dsn := database.PostgresDSN(database.PostgresConfig{Host: "127.0.0.1", Port: port, User: "app", Password: postgresTestPassword, Name: "cms", SSLMode: "disable"}) started := time.Now() _, err := database.Connect(database.Config{Driver: database.DriverPostgres, Source: dsn}) if err == nil { t.Fatal("expected an error") } if !strings.Contains(err.Error(), "127.0.0.1:"+port) { t.Errorf("the error must name the host and port: %v", err) } if strings.Contains(err.Error(), postgresTestPassword) { t.Errorf("the error must not contain the password: %v", err) } if elapsed := time.Since(started); elapsed > 8*time.Second { t.Errorf("startup must fail fast, took %v", elapsed) } }) t.Run("an unusable DSN is rejected without echoing it", func(t *testing.T) { _, err := database.Connect(database.Config{Driver: database.DriverPostgres, Source: "postgres://user:" + postgresTestPassword + "@[broken"}) if err == nil || strings.Contains(err.Error(), postgresTestPassword) { t.Errorf("unexpected error %v", err) } }) } func TestPostgresStartupFailures(t *testing.T) { binary := buildServerBinary(t, "") t.Run("Missing PostgreSQL variable stops the server", func(t *testing.T) { complete := map[string]string{"DB_HOST": "127.0.0.1", "DB_USER": "app", "DB_PASSWORD": postgresTestPassword, "DB_NAME": "cms"} for missing := range complete { t.Run(missing, func(t *testing.T) { extra := []string{"DB_DRIVER=postgresql"} for name, value := range complete { if name != missing { extra = append(extra, name+"="+value) } } output, code := runServerWithEnvironment(t, binary, startupEnvironment(t, extra...)) if code == 0 || !strings.Contains(output, "Environment variable "+missing+" is not set") || strings.Contains(output, postgresTestPassword) { t.Errorf("code %d output %q", code, output) } }) } }) t.Run("an invalid DB_SSLMODE stops the server naming the variable", func(t *testing.T) { output, code := runServerWithEnvironment(t, binary, startupEnvironment(t, "DB_DRIVER=postgres", "DB_HOST=127.0.0.1", "DB_USER=app", "DB_PASSWORD="+postgresTestPassword, "DB_NAME=cms", "DB_SSLMODE=maybe")) if code == 0 || !strings.Contains(output, "DB_SSLMODE") || strings.Contains(output, postgresTestPassword) { t.Errorf("code %d output %q", code, output) } }) t.Run("Fail fast when the server cannot be used", func(t *testing.T) { port := closedLocalPort(t) output, code := runServerWithEnvironment(t, binary, startupEnvironment(t, "DB_DRIVER=postgres", "DB_HOST=127.0.0.1", "DB_PORT="+port, "DB_USER=app", "DB_PASSWORD="+postgresTestPassword, "DB_NAME=cms")) if code == 0 || !strings.Contains(output, "Unable to open the database") { t.Errorf("code %d output %q", code, output) } if !strings.Contains(output, "127.0.0.1:"+port) { t.Errorf("the message must name the host and port: %q", output) } if strings.Contains(output, postgresTestPassword) { t.Errorf("the message must not contain the password: %q", output) } }) t.Run("an omitted DB_PORT targets 5432", func(t *testing.T) { output, code := runServerWithEnvironment(t, binary, startupEnvironment(t, "DB_DRIVER=postgres", "DB_HOST=127.0.0.1", "DB_USER=app", "DB_PASSWORD="+postgresTestPassword, "DB_NAME=cms")) if code == 0 || strings.Contains(output, postgresTestPassword) { t.Errorf("code %d output %q", code, output) } if strings.Contains(output, "Unable to open the database") && !strings.Contains(output, "127.0.0.1:5432") { t.Errorf("the default port must be 5432: %q", output) } }) }