feat: backend service
This commit is contained in:
@@ -0,0 +1,122 @@
|
||||
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)
|
||||
}
|
||||
Reference in New Issue
Block a user