package containers import ( "context" "fmt" "strconv" "strings" "testing" "time" "github.com/testcontainers/testcontainers-go/modules/postgres" sharedpostgres "knowfoolery/backend/shared/infra/database/postgres" ) const ( testPostgresUser = "knowfoolery" testPostgresPassword = "knowfoolery" testPostgresDB = "knowfoolery_test" ) // PostgresInstance contains connection details for a disposable Postgres test container. type PostgresInstance struct { Host string Port int User string Password string Database string } // URL returns a connection URL for pgx/stdlib clients. func (p PostgresInstance) URL() string { return fmt.Sprintf( "postgresql://%s:%s@%s:%d/%s?sslmode=disable", p.User, p.Password, p.Host, p.Port, p.Database, ) } // Config converts container details to the shared postgres config type. func (p PostgresInstance) Config() sharedpostgres.Config { cfg := sharedpostgres.DefaultConfig() cfg.Host = p.Host cfg.Port = p.Port cfg.User = p.User cfg.Password = p.Password cfg.Database = p.Database cfg.SSLMode = "disable" return cfg } // StartPostgres boots a disposable Postgres container and registers cleanup with t.Cleanup. func StartPostgres(t *testing.T) PostgresInstance { t.Helper() ctx, cancel := context.WithTimeout(context.Background(), 90*time.Second) t.Cleanup(cancel) var ( container *postgres.PostgresContainer err error panicErr error ) func() { defer func() { if recovered := recover(); recovered != nil { panicErr = fmt.Errorf("%v", recovered) } }() container, err = postgres.Run( ctx, "postgres:16-alpine", postgres.WithDatabase(testPostgresDB), postgres.WithUsername(testPostgresUser), postgres.WithPassword(testPostgresPassword), postgres.BasicWaitStrategies(), ) }() if panicErr != nil { if isDockerUnavailable(panicErr) { t.Skipf("docker unavailable for testcontainers: %v", panicErr) } t.Fatalf("start postgres testcontainer panic: %v", panicErr) } if err != nil { if isDockerUnavailable(err) { t.Skipf("docker unavailable for testcontainers: %v", err) } t.Fatalf("start postgres testcontainer: %v", err) } t.Cleanup(func() { stopCtx, stopCancel := context.WithTimeout(context.Background(), 30*time.Second) defer stopCancel() if termErr := container.Terminate(stopCtx); termErr != nil { t.Fatalf("terminate postgres testcontainer: %v", termErr) } }) host, err := container.Host(ctx) if err != nil { t.Fatalf("resolve postgres host: %v", err) } mappedPort, err := container.MappedPort(ctx, "5432/tcp") if err != nil { t.Fatalf("resolve postgres mapped port: %v", err) } port, err := strconv.Atoi(mappedPort.Port()) if err != nil { t.Fatalf("parse postgres mapped port: %v", err) } return PostgresInstance{ Host: host, Port: port, User: testPostgresUser, Password: testPostgresPassword, Database: testPostgresDB, } } func isDockerUnavailable(err error) bool { msg := strings.ToLower(err.Error()) return strings.Contains(msg, "permission denied") || strings.Contains(msg, "cannot connect to the docker daemon") || strings.Contains(msg, "docker.sock") || strings.Contains(msg, "connection refused") }