123 lines
3.2 KiB
Go
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)
|
|
}
|