package app import ( "context" "database/sql" "net/url" "os" "sync" "testing" "time" "galaxy/mail/internal/adapters/postgres/migrations" mailconfig "galaxy/mail/internal/config" "galaxy/postgres" testcontainers "github.com/testcontainers/testcontainers-go" tcpostgres "github.com/testcontainers/testcontainers-go/modules/postgres" "github.com/testcontainers/testcontainers-go/wait" ) const ( pkgPGImage = "postgres:16-alpine" pkgPGSuperUser = "galaxy" pkgPGSuperPassword = "galaxy" pkgPGSuperDatabase = "galaxy_mail" pkgPGServiceRole = "mailservice" pkgPGServicePassword = "mailservice" pkgPGServiceSchema = "mail" pkgPGContainerStartup = 90 * time.Second pkgPGOperationTimeout = 10 * time.Second ) var ( pkgPGContainerOnce sync.Once pkgPGContainerErr error pkgPGContainerEnv *runtimePostgresEnv ) type runtimePostgresEnv struct { container *tcpostgres.PostgresContainer dsn string pool *sql.DB } func ensureRuntimePostgresEnv(t testing.TB) *runtimePostgresEnv { t.Helper() pkgPGContainerOnce.Do(func() { pkgPGContainerEnv, pkgPGContainerErr = startRuntimePostgresEnv() }) if pkgPGContainerErr != nil { t.Skipf("postgres container start failed (Docker unavailable?): %v", pkgPGContainerErr) } return pkgPGContainerEnv } func startRuntimePostgresEnv() (*runtimePostgresEnv, error) { ctx := context.Background() container, err := tcpostgres.Run(ctx, pkgPGImage, tcpostgres.WithDatabase(pkgPGSuperDatabase), tcpostgres.WithUsername(pkgPGSuperUser), tcpostgres.WithPassword(pkgPGSuperPassword), testcontainers.WithWaitStrategy( wait.ForLog("database system is ready to accept connections"). WithOccurrence(2). WithStartupTimeout(pkgPGContainerStartup), ), ) if err != nil { return nil, err } baseDSN, err := container.ConnectionString(ctx, "sslmode=disable") if err != nil { _ = testcontainers.TerminateContainer(container) return nil, err } if err := provisionRuntimeRoleAndSchema(ctx, baseDSN); err != nil { _ = testcontainers.TerminateContainer(container) return nil, err } scopedDSN, err := dsnForRuntimeServiceRole(baseDSN) if err != nil { _ = testcontainers.TerminateContainer(container) return nil, err } cfg := postgres.DefaultConfig() cfg.PrimaryDSN = scopedDSN cfg.OperationTimeout = pkgPGOperationTimeout pool, err := postgres.OpenPrimary(ctx, cfg) if err != nil { _ = testcontainers.TerminateContainer(container) return nil, err } if err := postgres.Ping(ctx, pool, pkgPGOperationTimeout); err != nil { _ = pool.Close() _ = testcontainers.TerminateContainer(container) return nil, err } if err := postgres.RunMigrations(ctx, pool, migrations.FS(), "."); err != nil { _ = pool.Close() _ = testcontainers.TerminateContainer(container) return nil, err } return &runtimePostgresEnv{container: container, dsn: scopedDSN, pool: pool}, nil } func provisionRuntimeRoleAndSchema(ctx context.Context, baseDSN string) error { cfg := postgres.DefaultConfig() cfg.PrimaryDSN = baseDSN cfg.OperationTimeout = pkgPGOperationTimeout db, err := postgres.OpenPrimary(ctx, cfg) if err != nil { return err } defer func() { _ = db.Close() }() statements := []string{ `DO $$ BEGIN IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'mailservice') THEN CREATE ROLE mailservice LOGIN PASSWORD 'mailservice'; END IF; END $$;`, `CREATE SCHEMA IF NOT EXISTS mail AUTHORIZATION mailservice;`, `GRANT USAGE ON SCHEMA mail TO mailservice;`, } for _, statement := range statements { if _, err := db.ExecContext(ctx, statement); err != nil { return err } } return nil } func dsnForRuntimeServiceRole(baseDSN string) (string, error) { parsed, err := url.Parse(baseDSN) if err != nil { return "", err } values := url.Values{} values.Set("search_path", pkgPGServiceSchema) values.Set("sslmode", "disable") scoped := url.URL{ Scheme: parsed.Scheme, User: url.UserPassword(pkgPGServiceRole, pkgPGServicePassword), Host: parsed.Host, Path: parsed.Path, RawQuery: values.Encode(), } return scoped.String(), nil } // truncateRuntimeMail clears the mail schema between tests sharing the // container. func truncateRuntimeMail(t *testing.T) { t.Helper() env := ensureRuntimePostgresEnv(t) if env == nil { return } if _, err := env.pool.ExecContext(context.Background(), `TRUNCATE TABLE malformed_commands, dead_letters, delivery_payloads, attempts, delivery_recipients, deliveries RESTART IDENTITY CASCADE`, ); err != nil { t.Fatalf("truncate mail tables: %v", err) } } // runtimeBaseConfig returns a minimum-viable config suitable for runtime // construction, with Redis and Postgres connection coordinates wired up. The // caller still has to fill the templates dir, internal HTTP addr, SMTP mode, // etc. The helper does NOT truncate mail tables — tests that need a clean // slate should call truncateRuntimeMail explicitly (typically once at test // start, not on every runtime restart). func runtimeBaseConfig(t *testing.T, redisAddr string) mailconfig.Config { t.Helper() env := ensureRuntimePostgresEnv(t) cfg := mailconfig.DefaultConfig() cfg.Redis.Conn.MasterAddr = redisAddr cfg.Redis.Conn.Password = "integration" cfg.Postgres.Conn.PrimaryDSN = env.dsn cfg.Postgres.Conn.OperationTimeout = pkgPGOperationTimeout return cfg } // TestMain shuts down the shared container after the test process completes. func TestMain(m *testing.M) { code := m.Run() if pkgPGContainerEnv != nil { if pkgPGContainerEnv.pool != nil { _ = pkgPGContainerEnv.pool.Close() } if pkgPGContainerEnv.container != nil { _ = testcontainers.TerminateContainer(pkgPGContainerEnv.container) } } os.Exit(code) }