veza/veza-backend-api/internal/testutils/setup.go
senke 7cfd48a82a
Some checks failed
Backend API CI / test-unit (push) Failing after 0s
Backend API CI / test-integration (push) Failing after 0s
Stream Server CI / test (push) Failing after 0s
fix(release): v1.0.1 — Conformité complète ROADMAP checklist
- Sécurité: npm 0 CRITICAL, cargo audit 0 vulnérabilités
- OpenAPI: @Param id corrigé pour /tracks/quota/{id}
- Tests: Payment E2E passe, OAuth DATABASE_URL fallback
- Migrations: 000_mark_consolidated.sql
- veza-stream-server: prometheus 0.14, validator 0.19
- docs: SECURITY_SCAN_RC1, V1_SIGNOFF, PROJECT_STATE
2026-03-03 20:17:54 +01:00

142 lines
4 KiB
Go

package testutils
import (
"context"
"fmt"
"os"
"path/filepath"
"runtime"
"sort"
"strings"
"sync"
"time"
"github.com/testcontainers/testcontainers-go"
"github.com/testcontainers/testcontainers-go/modules/postgres"
"github.com/testcontainers/testcontainers-go/wait"
"go.uber.org/zap"
)
var (
pgContainer *postgres.PostgresContainer
pgDSN string
containerOnce sync.Once
pgErr error
)
// GetTestContainerDB ensures the postgres container is running and returns the DSN.
// It uses a singleton pattern to start the container only once per test run.
// If DATABASE_URL is set (e.g. CI with real PostgreSQL), uses it instead of testcontainer.
func GetTestContainerDB(ctx context.Context) (string, error) {
containerOnce.Do(func() {
if dsn := os.Getenv("DATABASE_URL"); dsn != "" {
pgDSN = dsn
pgErr = nil
return
}
pgErr = setupPostgresContainer(ctx)
})
return pgDSN, pgErr
}
func setupPostgresContainer(ctx context.Context) error {
// Find project root relative to this file
// This file is in internal/testutils/setup.go
_, filename, _, _ := runtime.Caller(0)
projectRoot := filepath.Join(filepath.Dir(filename), "../..")
migrationsDir := filepath.Join(projectRoot, "migrations")
// Collect migration files
files, err := os.ReadDir(migrationsDir)
if err != nil {
return fmt.Errorf("failed to read migrations dir: %w", err)
}
var migrationFiles []string
for _, f := range files {
// MOD-P1-001: Exclude cleanup migrations that may fail if tables don't exist yet
// These migrations are meant to be run on existing databases, not fresh ones
if strings.HasSuffix(f.Name(), ".sql") && !strings.Contains(f.Name(), "000000_cleanup") {
migrationFiles = append(migrationFiles, filepath.Join(migrationsDir, f.Name()))
}
}
sort.Strings(migrationFiles) // Ensure alphabetical order (001_, 002_, ...)
// MOD-P1-001: Retry container startup with exponential backoff
// Use a simple logger for testcontainers (zap.L() may not be initialized in tests)
logger := zap.NewNop()
if zap.L() != nil {
logger = zap.L()
}
var containerErr error
maxRetries := 3
retryDelay := 2 * time.Second
for attempt := 1; attempt <= maxRetries; attempt++ {
logger.Info("Starting PostgreSQL testcontainer",
zap.Int("attempt", attempt),
zap.Int("max_retries", maxRetries),
zap.Int("migration_files", len(migrationFiles)),
)
// Start Postgres container with improved wait strategy
pgContainer, containerErr = postgres.Run(ctx,
"postgres:15-alpine",
postgres.WithDatabase("veza_test"),
postgres.WithUsername("veza"),
postgres.WithPassword("veza"),
postgres.WithInitScripts(migrationFiles...),
testcontainers.WithWaitStrategy(
wait.ForLog("database system is ready to accept connections").
WithOccurrence(2).
WithStartupTimeout(90*time.Second)), // Increased timeout from 60s to 90s
)
if containerErr == nil {
logger.Info("PostgreSQL testcontainer started successfully",
zap.Int("attempt", attempt),
)
break // Success
}
// Log retry attempt
logger.Warn("Failed to start PostgreSQL testcontainer, retrying",
zap.Int("attempt", attempt),
zap.Int("max_retries", maxRetries),
zap.Error(containerErr),
)
if attempt < maxRetries {
backoff := retryDelay * time.Duration(attempt) // Exponential backoff
logger.Info("Waiting before retry",
zap.Duration("backoff", backoff),
)
time.Sleep(backoff)
}
}
if containerErr != nil {
logger.Error("Failed to start PostgreSQL testcontainer after all retries",
zap.Int("max_retries", maxRetries),
zap.Error(containerErr),
)
return fmt.Errorf("failed to start postgres container after %d attempts: %w", maxRetries, containerErr)
}
var dsnErr error
pgDSN, dsnErr = pgContainer.ConnectionString(ctx, "sslmode=disable")
if dsnErr != nil {
return fmt.Errorf("failed to get connection string: %w", dsnErr)
}
return nil
}
// TerminateContainer allows manual termination if needed (mostly for cleanup)
func TerminateContainer(ctx context.Context) error {
if pgContainer != nil {
return pgContainer.Terminate(ctx)
}
return nil
}