209 lines
5.7 KiB
Go
209 lines
5.7 KiB
Go
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)
|
|
}
|