cli/dockercompose/constellation_test.go
Rama main · réplica de lectura del repositorio original.
package dockercompose //nolint:testpackage
import (
"net/url"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/nhost/be/services/mimir/model"
)
const constellationJWTSecret = `{"claims_map":{"x-hasura-allowed-roles":{"path":"$.roles"},"x-hasura-default-role":"viewer","x-hasura-org-id":{"default":"public","path":"$.org"},"x-hasura-user-id":{"path":"$.sub"}},"key":"jwtSecretKey","type":"HS256"}`
// Canonical traefik rule strings produced by graphql.go / constellation.go.
// These are referenced from TestGraphqlIngressWithConstellation so a regression
// in the rule construction is caught by an exact-value comparison.
const (
canonicalHasuraRule = "(HostRegexp(`^.+\\.hasura\\.local\\.nhost\\.run$`) || Host(`local.hasura.nhost.run`))&& ( PathPrefix(`/v1`) || PathPrefix(`/v2`) || PathPrefix(`/api/`) || PathPrefix(`/console/assets`) )"
canonicalGraphqlRule = "(HostRegexp(`^.+\\.graphql\\.local\\.nhost\\.run$`) || Host(`local.graphql.nhost.run`))&& PathPrefix(`/v1`)"
canonicalConstellationRule = "(HostRegexp(`^.+\\.graphql\\.local\\.nhost\\.run$`) || Host(`local.graphql.nhost.run`))"
)
func expectedConstellation(useTLS bool) *Service {
scheme := "http"
if useTLS {
scheme = "https"
}
tlsLabel := "false"
if useTLS {
tlsLabel = "true"
}
return &Service{
Image: "nhost/constellation:0.1.0",
DependsOn: map[string]DependsOn{
"postgres": {Condition: "service_healthy"},
},
EntryPoint: nil,
Command: []string{"serve"},
Environment: map[string]string{
"CONSTELLATION_ADMIN_SECRET": "adminSecret",
"CONSTELLATION_CORS_ALLOWED_ORIGINS": scheme + "://dev.dashboard.local.nhost.run:1337,http://localhost:3000",
"CONSTELLATION_DEBUG": "false",
"CONSTELLATION_DEV_MODE": "false",
"CONSTELLATION_JWT_SECRET": constellationJWTSecret,
"CONSTELLATION_METADATA_DATABASE_URL": "postgres://postgres:postgres@postgres:5432/local",
"CONSTELLATION_SUBSCRIPTION_POLL_INTERVAL": "1s",
"ENV1": "VALUE1",
"ENV2": "VALUE2",
"GRAPHITE_WEBHOOK_SECRET": "webhookSecret",
"HASURA_GRAPHQL_DATABASE_URL": "postgres://postgres:postgres@postgres:5432/local",
"NHOST_ADMIN_SECRET": "adminSecret",
"NHOST_AUTH_URL": scheme + "://dev.auth.local.nhost.run:1337/v1",
"NHOST_FUNCTIONS_URL": "http://functions:3000",
"NHOST_GRAPHQL_DATABASE_URL": "postgres://postgres:postgres@postgres:5432/local",
"NHOST_GRAPHQL_URL": scheme + "://dev.graphql.local.nhost.run:1337/v1",
"NHOST_JWT_SECRET": constellationJWTSecret,
"NHOST_REGION": "local",
"NHOST_STORAGE_URL": scheme + "://dev.storage.local.nhost.run:1337/v1",
"NHOST_SUBDOMAIN": "dev",
"NHOST_WEBHOOK_SECRET": "webhookSecret",
},
ExtraHosts: []string{
"host.docker.internal:host-gateway",
},
HealthCheck: &HealthCheck{
Test: []string{
"CMD",
"wget",
"--spider",
"-S",
"http://localhost:8000/healthz",
},
Timeout: "60s",
Interval: "5s",
StartPeriod: "60s",
},
Labels: map[string]string{
"traefik.enable": "true",
"traefik.http.routers.constellation.entrypoints": "web",
"traefik.http.routers.constellation.rule": canonicalConstellationRule,
"traefik.http.routers.constellation.service": "constellation",
"traefik.http.routers.constellation.tls": tlsLabel,
"traefik.http.services.constellation.loadbalancer.server.port": "8000",
},
Networks: networkAliases("constellation-service"),
Ports: nil,
Restart: "always",
Volumes: []Volume{
{
Type: "bind",
Source: "/path/to/nhost/metadata",
Target: "/metadata",
ReadOnly: new(false),
},
},
WorkingDir: nil,
}
}
func callGetServices(t *testing.T, withConstellation, useTLS bool) map[string]*Service {
t.Helper()
tmp := t.TempDir()
cfg := getConfig()
cfg.Hasura.Version = new("v2.25.0")
if withConstellation {
cfg.Experimental = &model.ConfigExperimental{
Constellation: &model.ConfigConstellation{
Version: new("0.1.0"),
Settings: nil,
},
}
}
dockerURL, err := url.Parse(defaultDockerEndpoint)
if err != nil {
t.Fatalf("parse default Docker endpoint: %v", err)
}
services, err := getServices(
cfg,
dockerURL,
"dev",
"nhost",
1337,
useTLS,
5432,
tmp,
tmp,
tmp,
ExposePorts{},
"main",
"nhost/dashboard:3.9.0",
"2.1.0",
"nhost/cli:dev",
"00000000-0000-0000-0000-000000000000",
false,
"",
)
if err != nil {
t.Fatalf("getServices failed: %v", err)
}
return services
}
func TestGraphqlIngressWithConstellation(t *testing.T) {
t.Parallel()
t.Run("graphql owns local.graphql when constellation disabled", func(t *testing.T) {
t.Parallel()
services := callGetServices(t, false, false)
if _, ok := services["constellation"]; ok {
t.Fatal("constellation service should not be present when disabled")
}
labels := services["graphql"].Labels
if got := labels["traefik.http.routers.graphql.rule"]; got != canonicalGraphqlRule {
t.Errorf(
"graphql router rule drifted from canonical:\n got: %q\n want: %q",
got,
canonicalGraphqlRule,
)
}
if got := labels["traefik.http.routers.hasura.rule"]; got != canonicalHasuraRule {
t.Errorf(
"hasura router rule drifted from canonical:\n got: %q\n want: %q",
got,
canonicalHasuraRule,
)
}
})
t.Run("constellation owns local.graphql when enabled", func(t *testing.T) {
t.Parallel()
assertConstellationOwnsGraphql(t, false)
})
t.Run("constellation owns local.graphql when enabled under TLS", func(t *testing.T) {
t.Parallel()
assertConstellationOwnsGraphql(t, true)
})
}
// assertConstellationOwnsGraphql exercises the constellation-enabled
// `getServices` path and re-asserts the four canonical traefik labels
// (rule, tls, loadbalancer port, no rewrite middleware) plus the
// graphql/hasura sibling invariants. Parameterised on useTLS so the TLS
// integration path is covered without duplicating the assertion block.
func assertConstellationOwnsGraphql(t *testing.T, useTLS bool) {
t.Helper()
services := callGetServices(t, true, useTLS)
c, ok := services["constellation"]
if !ok {
t.Fatal("constellation service should be present when enabled")
}
wantTLS := "false"
if useTLS {
wantTLS = "true"
}
wantLabels := map[string]string{
"traefik.http.routers.constellation.rule": canonicalConstellationRule,
"traefik.http.routers.constellation.tls": wantTLS,
"traefik.http.services.constellation.loadbalancer.server.port": "8000",
}
for k, want := range wantLabels {
if got := c.Labels[k]; got != want {
t.Errorf("constellation label %q drifted (useTLS=%v):\n got: %q\n want: %q",
k, useTLS, got, want)
}
}
if _, ok := c.Labels["traefik.http.middlewares.replace-constellation.replacepathregex.regex"]; ok {
t.Error("constellation router should not register a rewrite middleware")
}
labels := services["graphql"].Labels
if _, ok := labels["traefik.http.routers.graphql.rule"]; ok {
t.Error(
"graphql service must not register its `graphql` router when constellation is enabled — constellation owns local.graphql.local.nhost.run",
)
}
// The hand-rolled hasura ingress in compose.go's constellation branch
// must stay byte-for-byte identical to the canonical one produced by
// graphql.go; this assertion catches drift between the two paths.
if got := labels["traefik.http.routers.hasura.rule"]; got != canonicalHasuraRule {
t.Errorf(
"hasura router rule drifted from canonical (useTLS=%v):\n got: %q\n want: %q",
useTLS, got, canonicalHasuraRule,
)
}
}
// TestDashboardHasuraAPIURLRoutesThroughConstellation locks in the dashboard
// env-var switch: NEXT_PUBLIC_NHOST_HASURA_API_URL points at the `graphql`
// subdomain (Constellation, which proxies the admin/metadata API to Hasura)
// when Constellation is enabled, and at the `hasura` subdomain otherwise. The
// console and migrations URLs must stay on the `hasura` subdomain in both
// cases — that split is intentional (the console UI and migrate API are served
// by the hasura-cli helper containers, not Constellation).
func TestDashboardHasuraAPIURLRoutesThroughConstellation(t *testing.T) {
t.Parallel()
const (
hasuraAPIURL = "http://dev.hasura.local.nhost.run:1337"
graphqlAPIURL = "http://dev.graphql.local.nhost.run:1337"
consoleURL = "http://dev.hasura.local.nhost.run:1337/console"
migrateURL = "http://dev.hasura.local.nhost.run:1337/apis/migrate"
)
cases := []struct {
name string
withConstellation bool
wantHasuraAPIURL string
}{
{
name: "constellation disabled routes to hasura subdomain",
withConstellation: false,
wantHasuraAPIURL: hasuraAPIURL,
},
{
name: "constellation enabled routes to graphql subdomain",
withConstellation: true,
wantHasuraAPIURL: graphqlAPIURL,
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
env := callGetServices(t, tc.withConstellation, false)["dashboard"].Environment
if got := env["NEXT_PUBLIC_NHOST_HASURA_API_URL"]; got != tc.wantHasuraAPIURL {
t.Errorf(
"NEXT_PUBLIC_NHOST_HASURA_API_URL = %q; want %q",
got, tc.wantHasuraAPIURL,
)
}
// The console and migrations API stay on the hasura subdomain
// regardless of whether Constellation owns the admin/metadata API.
if got := env["NEXT_PUBLIC_NHOST_HASURA_CONSOLE_URL"]; got != consoleURL {
t.Errorf(
"NEXT_PUBLIC_NHOST_HASURA_CONSOLE_URL = %q; want %q (must stay on hasura)",
got, consoleURL,
)
}
if got := env["NEXT_PUBLIC_NHOST_HASURA_MIGRATIONS_API_URL"]; got != migrateURL {
t.Errorf(
"NEXT_PUBLIC_NHOST_HASURA_MIGRATIONS_API_URL = %q; want %q (must stay on hasura)",
got,
migrateURL,
)
}
})
}
}
func TestConstellation(t *testing.T) {
t.Parallel()
cases := []struct {
name string
cfg func() *model.ConfigConfig
useTLS bool
expected func(useTLS bool) *Service
}{
{
name: "success",
cfg: getConfig,
useTLS: false,
expected: expectedConstellation,
},
{
name: "with TLS",
cfg: getConfig,
useTLS: true,
expected: expectedConstellation,
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
got, err := constellation(
tc.cfg(),
"dev",
tc.useTLS,
1337,
"/path/to/nhost",
"nhost/constellation:0.1.0",
"",
)
if err != nil {
t.Fatalf("got error: %v", err)
}
if diff := cmp.Diff(tc.expected(tc.useTLS), got); diff != "" {
t.Error(diff)
}
})
}
}
func TestConstellationHostUser(t *testing.T) {
t.Parallel()
cases := []struct {
name string
hostUser string
want *string
}{
{
name: "set",
hostUser: "1000:1000",
want: new("1000:1000"),
},
{
name: "unset",
hostUser: "",
want: nil,
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
got, err := constellation(
getConfig(),
"dev",
false,
1337,
"/path/to/nhost",
"nhost/constellation:0.1.0",
tc.hostUser,
)
if err != nil {
t.Fatalf("got error: %v", err)
}
if diff := cmp.Diff(tc.want, got.User); diff != "" {
t.Error(diff)
}
})
}
}