Files
common/database/config_test.go
Ask Bjørn Hansen 45308cd4bf feat(database): add PostgreSQL support with native pgx pool
Add PostgreSQL support to database package alongside existing MySQL support.
Both databases share common infrastructure (pool management, metrics,
transactions) while using database-specific connectors.

database/ changes:
- Add PostgresConfig struct and PostgreSQL connector using pgx/stdlib
- Change MySQL config from DBConfig to *MySQLConfig (pointer)
- Add Config.Validate() to prevent multiple database configs
- Add PostgreSQL connector with secure config building (no password in DSN)
- Add field validation and secure defaults (SSLMode="prefer")
- Support legacy flat PostgreSQL config format for backward compatibility
- Add tests for PostgreSQL configs and validation

New database/pgdb/ package:
- Native pgx connection pool support (*pgxpool.Pool)
- OpenPool() and OpenPoolWithConfigFile() APIs
- CreatePoolConfig() for secure config conversion
- PoolOptions for fine-grained pool control
- Full test coverage and documentation

Security:
- Passwords never exposed in DSN strings
- Set passwords separately in pgx config objects
- Validate all configuration before connection

Architecture:
- Shared code in database/ for both MySQL and PostgreSQL (sql.DB)
- database/pgdb/ for PostgreSQL-specific native pool support
2025-09-27 16:55:54 -07:00

197 lines
4.8 KiB
Go

package database
import (
"testing"
"time"
"github.com/prometheus/client_golang/prometheus"
)
func TestDefaultConfigOptions(t *testing.T) {
opts := DefaultConfigOptions()
// Verify expected defaults for API package
if opts.MaxOpenConns != 25 {
t.Errorf("Expected MaxOpenConns=25, got %d", opts.MaxOpenConns)
}
if opts.MaxIdleConns != 10 {
t.Errorf("Expected MaxIdleConns=10, got %d", opts.MaxIdleConns)
}
if opts.ConnMaxLifetime != 3*time.Minute {
t.Errorf("Expected ConnMaxLifetime=3m, got %v", opts.ConnMaxLifetime)
}
if !opts.EnablePoolMonitoring {
t.Error("Expected EnablePoolMonitoring=true")
}
if opts.PrometheusRegisterer != prometheus.DefaultRegisterer {
t.Error("Expected PrometheusRegisterer to be DefaultRegisterer")
}
if len(opts.ConfigFiles) == 0 {
t.Error("Expected ConfigFiles to be non-empty")
}
}
func TestMonitorConfigOptions(t *testing.T) {
opts := MonitorConfigOptions()
// Verify expected defaults for Monitor package
if opts.MaxOpenConns != 10 {
t.Errorf("Expected MaxOpenConns=10, got %d", opts.MaxOpenConns)
}
if opts.MaxIdleConns != 5 {
t.Errorf("Expected MaxIdleConns=5, got %d", opts.MaxIdleConns)
}
if opts.ConnMaxLifetime != 3*time.Minute {
t.Errorf("Expected ConnMaxLifetime=3m, got %v", opts.ConnMaxLifetime)
}
if opts.EnablePoolMonitoring {
t.Error("Expected EnablePoolMonitoring=false")
}
if opts.PrometheusRegisterer != nil {
t.Error("Expected PrometheusRegisterer to be nil")
}
if len(opts.ConfigFiles) == 0 {
t.Error("Expected ConfigFiles to be non-empty")
}
}
func TestConfigStructures(t *testing.T) {
// Test that MySQL configuration structures can be created and populated
config := Config{
MySQL: &MySQLConfig{
DSN: "user:pass@tcp(localhost:3306)/dbname",
User: "testuser",
Pass: "testpass",
DBName: "testdb",
},
}
if config.MySQL.DSN == "" {
t.Error("Expected DSN to be set")
}
if config.MySQL.User != "testuser" {
t.Errorf("Expected User='testuser', got '%s'", config.MySQL.User)
}
if config.MySQL.Pass != "testpass" {
t.Errorf("Expected Pass='testpass', got '%s'", config.MySQL.Pass)
}
if config.MySQL.DBName != "testdb" {
t.Errorf("Expected DBName='testdb', got '%s'", config.MySQL.DBName)
}
}
func TestPostgresConfigStructures(t *testing.T) {
// Test that PostgreSQL configuration structures can be created and populated
config := Config{
Postgres: &PostgresConfig{
Host: "localhost",
Port: 5432,
User: "testuser",
Pass: "testpass",
Name: "testdb",
SSLMode: "require",
},
}
if config.Postgres.Host != "localhost" {
t.Errorf("Expected Host='localhost', got '%s'", config.Postgres.Host)
}
if config.Postgres.Port != 5432 {
t.Errorf("Expected Port=5432, got %d", config.Postgres.Port)
}
if config.Postgres.User != "testuser" {
t.Errorf("Expected User='testuser', got '%s'", config.Postgres.User)
}
if config.Postgres.Pass != "testpass" {
t.Errorf("Expected Pass='testpass', got '%s'", config.Postgres.Pass)
}
if config.Postgres.Name != "testdb" {
t.Errorf("Expected Name='testdb', got '%s'", config.Postgres.Name)
}
if config.Postgres.SSLMode != "require" {
t.Errorf("Expected SSLMode='require', got '%s'", config.Postgres.SSLMode)
}
}
func TestLegacyPostgresConfig(t *testing.T) {
// Test that legacy flat PostgreSQL format can be created
config := Config{
User: "testuser",
Pass: "testpass",
Host: "localhost",
Port: 5432,
Name: "testdb",
SSLMode: "require",
}
if config.User != "testuser" {
t.Errorf("Expected User='testuser', got '%s'", config.User)
}
if config.Name != "testdb" {
t.Errorf("Expected Name='testdb', got '%s'", config.Name)
}
}
func TestConfigValidation(t *testing.T) {
tests := []struct {
name string
config Config
wantErr bool
}{
{
name: "valid mysql config",
config: Config{
MySQL: &MySQLConfig{DSN: "test"},
},
wantErr: false,
},
{
name: "valid postgres config",
config: Config{
Postgres: &PostgresConfig{User: "test", Host: "localhost", Name: "test"},
},
wantErr: false,
},
{
name: "valid legacy postgres config",
config: Config{
User: "test",
Host: "localhost",
Name: "testdb",
},
wantErr: false,
},
{
name: "both mysql and postgres set",
config: Config{
MySQL: &MySQLConfig{DSN: "test"},
Postgres: &PostgresConfig{User: "test"},
},
wantErr: true,
},
{
name: "mysql and legacy postgres set",
config: Config{
MySQL: &MySQLConfig{DSN: "test"},
User: "test",
Name: "testdb",
},
wantErr: true,
},
{
name: "no config set",
config: Config{},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := tt.config.Validate()
if (err != nil) != tt.wantErr {
t.Errorf("Validate() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}