//go:build test package db import ( "context" "fmt" "os" "sync" "testing" "time" ) func resetEnv() { os.Setenv("POSTGRES_USER", "myuser") os.Setenv("POSTGRES_PASSWORD", "mypassword") os.Setenv("POSTGRES_HOST", savedPOSTGRESHost) os.Setenv("POSTGRES_DB", "crussell_test_db") } func closePool() { if Conn != nil { Conn.Pool().Close() Conn = nil } } var testDBName = "crussell_test_db" // ============================================================================= // Happy path — connect + ping // ============================================================================= func TestConnect_Success(t *testing.T) { closePool() resetEnv() err := Connect() if err != nil { t.Fatalf("Connect() failed: %v", err) } defer closePool() if Conn == nil { t.Fatal("Conn is nil after successful Connect") } } func TestConnect_PingViaTestDB(t *testing.T) { closePool() resetEnv() err := Connect() if err != nil { t.Fatalf("Connect() failed: %v", err) } defer closePool() poolConn, err := Conn.Acquire(context.Background()) if err != nil { t.Fatalf("Acquire failed: %v", err) } defer poolConn.Release() var result int err = poolConn.QueryRow(context.Background(), "SELECT 1").Scan(&result) if err != nil { t.Fatalf("Ping query failed: %v", err) } if result != 1 { t.Errorf("expected 1, got %d", result) } } // TestScanLocation_UTC proves the AfterConnect hook: TIMESTAMPTZ values are // scanned into time.Time in UTC, never the host's local timezone. Without the // hook, pgx scans into time.Local, so a London-host dev server would emit // +01:00 JSON offsets while a UTC Docker host emits Z — same instant, but the // documented "backend emits UTC" invariant would silently depend on the // deployment host's TZ. func TestScanLocation_UTC(t *testing.T) { closePool() resetEnv() err := Connect() if err != nil { t.Fatalf("Connect() failed: %v", err) } defer closePool() poolConn, err := Conn.Acquire(context.Background()) if err != nil { t.Fatalf("Acquire failed: %v", err) } defer poolConn.Release() // A known instant: 2026-06-15 00:30 BST = 2026-06-14 23:30 UTC. instant := time.Date(2026, 6, 14, 23, 30, 0, 0, time.UTC) var scanned time.Time if err := poolConn.QueryRow(context.Background(), "SELECT $1::timestamptz", instant).Scan(&scanned); err != nil { t.Fatalf("timestamptz scan failed: %v", err) } if !scanned.Equal(instant) { t.Errorf("scanned instant = %s, expected %s", scanned.Format(time.RFC3339Nano), instant.Format(time.RFC3339Nano)) } if scanned.Location() != time.UTC { t.Errorf("scanned time.Time location = %v, expected time.UTC (host TZ is %v)", scanned.Location(), time.Local) } } // ============================================================================= // Connection failure scenarios // ============================================================================= func TestConnect_InvalidCredentials(t *testing.T) { closePool() os.Setenv("POSTGRES_USER", "wronguser") os.Setenv("POSTGRES_PASSWORD", "wrongpassword") os.Setenv("POSTGRES_DB", "crussell_test") os.Setenv("POSTGRES_HOST", "localhost") err := Connect() if err == nil { t.Error("expected error for invalid credentials, got nil") closePool() } resetEnv() } func TestConnect_RefusedConnection(t *testing.T) { closePool() os.Setenv("POSTGRES_USER", "myuser") os.Setenv("POSTGRES_PASSWORD", "mypassword") os.Setenv("POSTGRES_DB", "crussell_test") os.Setenv("POSTGRES_HOST", "localhost") t.Skip("pgxpool.New is lazy — connection errors surface on Acquire, not Connect") resetEnv() } // ============================================================================= // Concurrent access — pool should handle parallel queries // ============================================================================= func TestConcurrentQueries(t *testing.T) { closePool() resetEnv() err := Connect() if err != nil { t.Fatalf("Connect() failed: %v", err) } defer closePool() var wg sync.WaitGroup errs := make(chan error, 20) for i := 0; i < 20; i++ { wg.Add(1) go func(id int) { defer wg.Done() poolConn, err := Conn.Acquire(context.Background()) if err != nil { errs <- fmt.Errorf("goroutine %d: acquire: %w", id, err) return } defer poolConn.Release() var result int err = poolConn.QueryRow(context.Background(), "SELECT $1::int", id).Scan(&result) if err != nil { errs <- fmt.Errorf("goroutine %d: query: %w", id, err) return } if result != id { errs <- fmt.Errorf("goroutine %d: expected %d, got %d", id, id, result) } }(i) } wg.Wait() close(errs) for e := range errs { t.Error(e) } } // ============================================================================= // getEnv unit tests // ============================================================================= func TestGetEnv_ReturnsValue(t *testing.T) { os.Setenv("TEST_DB_VAR", "expected_value") defer os.Unsetenv("TEST_DB_VAR") if v := getEnv("TEST_DB_VAR"); v != "expected_value" { t.Errorf("expected 'expected_value', got %q", v) } } func TestGetEnv_ReturnsEmptyWhenUnset(t *testing.T) { os.Unsetenv("TEST_DB_MISSING_VAR") if v := getEnv("TEST_DB_MISSING_VAR"); v != "" { t.Errorf("expected empty string, got %q", v) } } // ============================================================================= // Clean-up — restore env after all tests // =============================================================================