package testenv import ( "context" "io" "testing" "github.com/testcontainers/testcontainers-go" ) // Platform aggregates a fully booted Galaxy stack: shared Docker // network, Postgres, Redis, mailpit, backend and gateway. Tests use // this struct to access HTTP/gRPC endpoints, mailpit and backend // admin without touching testcontainers directly. type Platform struct { Network string Postgres *Postgres Redis *Redis Mailpit *Mailpit Backend *BackendContainer Gateway *GatewayContainer } // BootstrapOptions tunes platform-level knobs that flow into backend // or gateway configuration. The zero value is valid and produces a // stack with sensible defaults for happy-path scenarios. type BootstrapOptions struct { BackendExtra map[string]string GatewayExtra map[string]string } // Bootstrap builds three Docker images (backend, gateway, optionally // the engine in the caller), spins up Postgres, Redis, mailpit, then // boots backend and gateway connected to those services. It registers // t.Cleanup hooks for every component, so callers do not own // teardown. // // The function calls RequireDocker and skips the test gracefully if // the daemon is unreachable, so every scenario can start with a // single Bootstrap call. func Bootstrap(t *testing.T, opts BootstrapOptions) *Platform { t.Helper() RequireDocker(t) net := StartNetwork(t) pg := StartPostgres(t, net.Name) redis := StartRedis(t, net.Name) mp := StartMailpit(t, net.Name) geoip := SyntheticGeoIPDB(t) backend := StartBackend(t, BackendOptions{ NetworkAlias: "backend", NetworkName: net.Name, PostgresDSN: pg.NetworkDSN, MailpitHost: "mailpit", MailpitPort: 1025, GeoIPHostPath: geoip, Extra: opts.BackendExtra, }) gateway := StartGateway(t, GatewayOptions{ NetworkAlias: "gateway", NetworkName: net.Name, BackendHTTPURL: "http://backend:8080", BackendGRPCURL: "backend:8081", RedisAddr: "redis:6379", Extra: opts.GatewayExtra, }) plat := &Platform{ Network: net.Name, Postgres: pg, Redis: redis, Mailpit: mp, Backend: backend, Gateway: gateway, } t.Cleanup(func() { if !t.Failed() { return } dumpLogs(t, "backend", backend.Container) dumpLogs(t, "gateway", gateway.Container) }) return plat } // dumpLogs writes the container's stdout/stderr to test output. Used // only on failure to surface backend / gateway diagnostics. func dumpLogs(t *testing.T, name string, c testcontainers.Container) { t.Helper() if c == nil { return } rc, err := c.Logs(context.Background()) if err != nil { t.Logf("%s logs unavailable: %v", name, err) return } defer rc.Close() body, _ := io.ReadAll(rc) t.Logf("--- %s container logs ---\n%s", name, string(body)) }