Ctrl K
ComunidadGuías…

cli/dockercompose/constellation_test.go

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
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)
			}
		})
	}
}