Files
2026-05-06 10:14:55 +03:00

123 lines
3.2 KiB
Go

package testenv
import (
"context"
"fmt"
"net/url"
"strconv"
"testing"
"time"
"github.com/testcontainers/testcontainers-go"
tcpostgres "github.com/testcontainers/testcontainers-go/modules/postgres"
tcnetwork "github.com/testcontainers/testcontainers-go/network"
"github.com/testcontainers/testcontainers-go/wait"
)
const (
pgImage = "postgres:16-alpine"
pgUser = "galaxy"
pgPassword = "galaxy"
pgDatabase = "galaxy_backend"
pgSchema = "backend"
pgStartup = 90 * time.Second
)
// Postgres holds a running Postgres testcontainer reachable from both
// the host (DSN with localhost-mapped port) and from another container
// on the same Docker network (HostInNetworkDSN).
type Postgres struct {
container *tcpostgres.PostgresContainer
HostDSN string
NetworkDSN string
}
// StartPostgres boots a postgres:16-alpine container, returns DSNs for
// both host and in-network access, and registers a t.Cleanup to
// terminate the container.
func StartPostgres(t *testing.T, network string) *Postgres {
t.Helper()
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Minute)
defer cancel()
opts := []testcontainers.ContainerCustomizer{
tcpostgres.WithDatabase(pgDatabase),
tcpostgres.WithUsername(pgUser),
tcpostgres.WithPassword(pgPassword),
testcontainers.WithWaitStrategy(
wait.ForLog("database system is ready to accept connections").
WithOccurrence(2).
WithStartupTimeout(pgStartup),
),
}
if network != "" {
opts = append(opts, tcnetwork.WithNetwork([]string{"postgres"}, &testcontainers.DockerNetwork{Name: network}))
}
container, err := tcpostgres.Run(ctx, pgImage, opts...)
if err != nil {
t.Skipf("postgres testcontainer unavailable: %v", err)
}
t.Cleanup(func() {
if err := testcontainers.TerminateContainer(container); err != nil {
t.Logf("terminate postgres: %v", err)
}
})
hostDSN, err := container.ConnectionString(ctx, "sslmode=disable")
if err != nil {
t.Fatalf("postgres host DSN: %v", err)
}
hostDSN, err = withSearchPath(hostDSN, pgSchema)
if err != nil {
t.Fatalf("postgres host DSN search_path: %v", err)
}
networkDSN := ""
if network != "" {
networkDSN = buildInNetworkDSN("postgres", 5432, pgUser, pgPassword, pgDatabase, pgSchema)
}
return &Postgres{
container: container,
HostDSN: hostDSN,
NetworkDSN: networkDSN,
}
}
func withSearchPath(dsn, schema string) (string, error) {
parsed, err := url.Parse(dsn)
if err != nil {
return "", err
}
q := parsed.Query()
q.Set("search_path", schema)
if q.Get("sslmode") == "" {
q.Set("sslmode", "disable")
}
parsed.RawQuery = q.Encode()
return parsed.String(), nil
}
func buildInNetworkDSN(host string, port int, user, password, db, schema string) string {
u := &url.URL{
Scheme: "postgres",
User: url.UserPassword(user, password),
Host: fmt.Sprintf("%s:%d", host, port),
Path: "/" + db,
RawQuery: "sslmode=disable&search_path=" + schema,
}
return u.String()
}
// HostPort renders a host:port pair so other testenv files can reuse
// the same formatting.
func HostPort(host string, port int) string {
return fmt.Sprintf("%s:%d", host, port)
}
// FormatPort returns the decimal representation of port.
func FormatPort(port int) string {
return strconv.Itoa(port)
}