feat: use postgres

This commit is contained in:
Ilia Denisov
2026-04-26 20:34:39 +02:00
committed by GitHub
parent 48b0056b49
commit fe829285a6
365 changed files with 29223 additions and 24049 deletions
+208
View File
@@ -0,0 +1,208 @@
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)
}