merge: /sovereign nginx routing — values-driven /sovereign + /api/v1 (a35da92)
This commit is contained in:
commit
919514ca78
@ -1,5 +1,7 @@
|
||||
# Catalyst UI (Sovereign tier console) is served at
|
||||
# https://console.openova.io/sovereign/*.
|
||||
# https://<ingress.host><routing.basePath>/* — by default
|
||||
# https://console.openova.io/sovereign/* on Catalyst-Zero, and
|
||||
# https://console.<sovereign-fqdn>/sovereign/* on a franchised Sovereign.
|
||||
#
|
||||
# The TLS cert for console.openova.io is owned by the sme namespace
|
||||
# (console-openova-tls managed by cert-manager on the console-nova
|
||||
@ -7,7 +9,7 @@
|
||||
# caused Traefik to present different certs per SNI connection ->
|
||||
# intermittent SSL errors in the browser.
|
||||
#
|
||||
# Fix: this ingress exposes the HTTP-only route with the strip-sovereign
|
||||
# Fix: this ingress exposes the HTTP-only route with the strip-prefix
|
||||
# middleware. Traefik serves TLS using the sme-owned cert because it
|
||||
# aggregates cert providers by hostname.
|
||||
---
|
||||
@ -19,7 +21,7 @@ metadata:
|
||||
spec:
|
||||
stripPrefix:
|
||||
prefixes:
|
||||
- /sovereign
|
||||
- {{ .Values.routing.basePath | quote }}
|
||||
---
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
@ -27,15 +29,15 @@ metadata:
|
||||
name: console-sovereign
|
||||
namespace: catalyst
|
||||
annotations:
|
||||
traefik.ingress.kubernetes.io/router.priority: "100"
|
||||
traefik.ingress.kubernetes.io/router.priority: {{ .Values.ingress.priority | quote }}
|
||||
traefik.ingress.kubernetes.io/router.middlewares: "catalyst-strip-sovereign@kubernetescrd"
|
||||
spec:
|
||||
ingressClassName: traefik
|
||||
ingressClassName: {{ .Values.ingress.className }}
|
||||
rules:
|
||||
- host: console.openova.io
|
||||
- host: {{ .Values.ingress.host | quote }}
|
||||
http:
|
||||
paths:
|
||||
- path: /sovereign
|
||||
- path: {{ .Values.routing.basePath | quote }}
|
||||
pathType: Prefix
|
||||
backend:
|
||||
service:
|
||||
|
||||
92
products/catalyst/chart/templates/ui-configmap.yaml
Normal file
92
products/catalyst/chart/templates/ui-configmap.yaml
Normal file
@ -0,0 +1,92 @@
|
||||
{{- /*
|
||||
catalyst-ui nginx config — rendered by Helm at chart-render time so that
|
||||
upstream Service DNS names + ports + subpath flow from values.yaml
|
||||
(per docs/INVIOLABLE-PRINCIPLES.md §4 "Never hardcode").
|
||||
|
||||
The pod mounts this ConfigMap at /etc/nginx/conf.d/default.conf via
|
||||
ui-deployment.yaml. nginx-alpine does NOT auto-envsubst its config files,
|
||||
so runtime env-var injection is not viable; chart-render-time substitution
|
||||
is the canonical path.
|
||||
*/ -}}
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: catalyst-ui-nginx
|
||||
labels:
|
||||
app.kubernetes.io/name: catalyst-ui
|
||||
app.kubernetes.io/component: frontend-config
|
||||
data:
|
||||
default.conf: |
|
||||
server {
|
||||
listen 8080;
|
||||
server_name _;
|
||||
root /usr/share/nginx/html;
|
||||
index index.html;
|
||||
|
||||
absolute_redirect off;
|
||||
port_in_redirect off;
|
||||
|
||||
# Security headers
|
||||
add_header X-Frame-Options "SAMEORIGIN" always;
|
||||
add_header X-Content-Type-Options "nosniff" always;
|
||||
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
|
||||
|
||||
# Gzip
|
||||
gzip on;
|
||||
gzip_vary on;
|
||||
gzip_min_length 256;
|
||||
gzip_types text/html text/css text/javascript application/javascript application/json image/svg+xml;
|
||||
|
||||
# Reverse-proxy /api/* to the catalyst-api Service. The Traefik
|
||||
# `strip-sovereign` middleware on console.openova.io has already
|
||||
# removed the /sovereign prefix by the time requests reach this
|
||||
# nginx, so the SPA's /sovereign/api/v1/... browser path arrives
|
||||
# here as /api/v1/....
|
||||
#
|
||||
# CoreDNS is queried at proxy-time (not config-load time) so that
|
||||
# catalyst-api Pod restarts do not require an nginx reload. The
|
||||
# `set $api_upstream` indirection forces nginx to defer DNS
|
||||
# resolution until the request lands.
|
||||
resolver {{ .Values.dns.resolverIP }} valid={{ .Values.dns.resolverValid }};
|
||||
set $api_upstream http://{{ .Values.routing.catalystApi.serviceDNS }}:{{ .Values.routing.catalystApi.port }};
|
||||
|
||||
location /api/ {
|
||||
proxy_pass $api_upstream;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header Connection '';
|
||||
|
||||
# SSE streaming for /api/v1/deployments/{id}/logs — disable
|
||||
# buffering so EventSource sees per-line frames as the
|
||||
# provisioner emits them.
|
||||
proxy_buffering off;
|
||||
proxy_cache off;
|
||||
proxy_read_timeout 300s;
|
||||
chunked_transfer_encoding on;
|
||||
add_header X-Accel-Buffering no;
|
||||
}
|
||||
|
||||
# Cache static assets for one year — Vite emits hashed file names
|
||||
# so the cache busts automatically on each rebuild.
|
||||
location ~* \.(js|css|png|jpg|svg|woff2|ico)$ {
|
||||
expires 1y;
|
||||
add_header Cache-Control "public, immutable";
|
||||
}
|
||||
|
||||
# SPA fallback — every non-asset, non-/api route serves index.html
|
||||
# so React Router (with `basename={{ .Values.routing.basePath }}`)
|
||||
# can take over client-side. Skip the `try_files $uri $uri/`
|
||||
# directory-check middle step that would 301 to add a trailing
|
||||
# slash and lose the /sovereign prefix on the client side.
|
||||
location / {
|
||||
try_files $uri /index.html;
|
||||
}
|
||||
|
||||
# Health check (used by K8s probes)
|
||||
location = /healthz {
|
||||
access_log off;
|
||||
return 200 'ok';
|
||||
add_header Content-Type text/plain;
|
||||
}
|
||||
}
|
||||
@ -14,6 +14,14 @@ spec:
|
||||
metadata:
|
||||
labels:
|
||||
app.kubernetes.io/name: catalyst-ui
|
||||
annotations:
|
||||
# Roll the Pod whenever the rendered nginx config changes so that a
|
||||
# values.yaml override of routing.catalystApi.serviceDNS (e.g. for
|
||||
# a Sovereign install with a non-default namespace) lands without a
|
||||
# manual rollout-restart. Helm computes the checksum at render time;
|
||||
# if the chart is consumed via kustomize-only the literal string
|
||||
# is harmless.
|
||||
checksum/nginx-config: {{ include (print $.Template.BasePath "/ui-configmap.yaml") . | sha256sum }}
|
||||
spec:
|
||||
imagePullSecrets:
|
||||
- name: ghcr-pull-secret
|
||||
@ -48,3 +56,18 @@ spec:
|
||||
readOnlyRootFilesystem: false
|
||||
runAsNonRoot: true
|
||||
runAsUser: 1001
|
||||
volumeMounts:
|
||||
# Override the Containerfile-baked /etc/nginx/conf.d/default.conf
|
||||
# with the chart-rendered version so the catalyst-api upstream
|
||||
# DNS / port / subpath flow from values.yaml (no hardcoding,
|
||||
# per docs/INVIOLABLE-PRINCIPLES.md §4).
|
||||
- name: nginx-config
|
||||
mountPath: /etc/nginx/conf.d/default.conf
|
||||
subPath: default.conf
|
||||
volumes:
|
||||
- name: nginx-config
|
||||
configMap:
|
||||
name: catalyst-ui-nginx
|
||||
items:
|
||||
- key: default.conf
|
||||
path: default.conf
|
||||
|
||||
58
products/catalyst/chart/values.yaml
Normal file
58
products/catalyst/chart/values.yaml
Normal file
@ -0,0 +1,58 @@
|
||||
# Catalyst Platform Helm chart values.
|
||||
#
|
||||
# Per docs/INVIOLABLE-PRINCIPLES.md §4 ("Never hardcode"), every value the
|
||||
# chart consumes at install time MUST flow through this file (or an override
|
||||
# values file in the calling Flux HelmRelease). No DNS names, image tags,
|
||||
# ports, or service references may be hard-coded in templates.
|
||||
#
|
||||
# This file documents the defaults; environment-specific overrides are
|
||||
# provided per-cluster (e.g. clusters/_template/values.yaml or
|
||||
# clusters/<sovereign-fqdn>/values.yaml) and merged by Flux at apply time.
|
||||
|
||||
# Image references for the platform pods. SHA-pinned in the GitOps overlay,
|
||||
# defaulted here so `helm template` renders without an override.
|
||||
images:
|
||||
catalystUi:
|
||||
repository: ghcr.io/openova-io/openova/catalyst-ui
|
||||
tag: "333b859"
|
||||
pullPolicy: IfNotPresent
|
||||
catalystApi:
|
||||
repository: ghcr.io/openova-io/openova/catalyst-api
|
||||
tag: "333b859"
|
||||
pullPolicy: IfNotPresent
|
||||
|
||||
# Routing wires together the in-cluster Service DNS names that the catalyst-ui
|
||||
# nginx reverse-proxies to. The defaults assume the canonical install (chart
|
||||
# namespace = "catalyst", default Service names from this chart).
|
||||
#
|
||||
# Sovereign installs that override the namespace or rename Services MUST
|
||||
# override these values — never patch nginx.conf directly.
|
||||
routing:
|
||||
# Subpath the wizard SPA is mounted at on console.openova.io. Used by the
|
||||
# ConfigMap template + the Vite `base` config (compile-time) to keep the
|
||||
# two in lock-step.
|
||||
basePath: /sovereign
|
||||
|
||||
catalystApi:
|
||||
# Cluster-internal Service DNS for the catalyst-api Service. Sourced
|
||||
# here, never inlined into nginx.conf. Must include the namespace +
|
||||
# `.svc.cluster.local` suffix (full FQDN avoids resolver ambiguity).
|
||||
serviceDNS: catalyst-api.catalyst.svc.cluster.local
|
||||
port: 8080
|
||||
|
||||
# CoreDNS service IP — used by nginx's `resolver` directive to resolve the
|
||||
# upstream Service name at proxy_pass time (rather than at config-load time,
|
||||
# which would pin nginx to a stale ClusterIP across catalyst-api restarts).
|
||||
# k3s ships CoreDNS at 10.43.0.10 by default (kube-dns service IP).
|
||||
dns:
|
||||
resolverIP: 10.43.0.10
|
||||
resolverValid: 30s
|
||||
|
||||
# Console subpath ingress. The TLS cert for console.openova.io is owned by
|
||||
# the sme namespace's `console-nova` ingress; this ingress only exposes the
|
||||
# HTTP route + Traefik strip-prefix middleware. See ingress.yaml header
|
||||
# comment for the rationale.
|
||||
ingress:
|
||||
host: console.openova.io
|
||||
className: traefik
|
||||
priority: "100"
|
||||
48
tests/e2e/sovereign-routing/README.md
Normal file
48
tests/e2e/sovereign-routing/README.md
Normal file
@ -0,0 +1,48 @@
|
||||
# Sovereign wizard routing smoke test
|
||||
|
||||
Closes [#142](https://github.com/openova-io/openova/issues/142).
|
||||
|
||||
Verifies that the chart-rendered routing for `https://console.openova.io/sovereign/`
|
||||
wires together end-to-end:
|
||||
|
||||
1. `GET /sovereign/` returns the wizard SPA shell (200, HTML).
|
||||
2. SPA fallback works — `GET /sovereign/wizard/credentials` also returns the shell
|
||||
so the wizard's React-Router can take over client-side.
|
||||
3. `POST /sovereign/api/v1/subdomains/check` round-trips through Traefik's
|
||||
`strip-sovereign` middleware → catalyst-ui nginx `/api/` reverse-proxy →
|
||||
catalyst-api Service (DNS sourced from `values.routing.catalystApi.serviceDNS`,
|
||||
never hardcoded — see `docs/INVIOLABLE-PRINCIPLES.md` §4).
|
||||
|
||||
## Run modes
|
||||
|
||||
### Mock (CI default, no cluster needed)
|
||||
|
||||
```bash
|
||||
cd tests/e2e/sovereign-routing
|
||||
npm install
|
||||
npx playwright install chromium
|
||||
USE_MOCK=1 npm test
|
||||
```
|
||||
|
||||
`USE_MOCK=1` intercepts every network call with canned responses that mirror
|
||||
the real chart-rendered nginx + catalyst-api behaviour. Fast, deterministic,
|
||||
and proves the SPA's wiring (basename, `API_BASE`) without depending on a
|
||||
deployed environment.
|
||||
|
||||
### Live cluster (post-Group-C cutover)
|
||||
|
||||
```bash
|
||||
SOVEREIGN_BASE_URL=https://console.openova.io \
|
||||
SOVEREIGN_BASE_PATH=/sovereign \
|
||||
npx playwright test
|
||||
```
|
||||
|
||||
Or against a Sovereign:
|
||||
|
||||
```bash
|
||||
SOVEREIGN_BASE_URL=https://console.omantel.omani.works \
|
||||
npx playwright test
|
||||
```
|
||||
|
||||
URLs flow from env vars per the never-hardcode rule. The defaults match the
|
||||
chart's `values.yaml` (`ingress.host` + `routing.basePath`).
|
||||
15
tests/e2e/sovereign-routing/package.json
Normal file
15
tests/e2e/sovereign-routing/package.json
Normal file
@ -0,0 +1,15 @@
|
||||
{
|
||||
"name": "sovereign-routing-e2e",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"description": "Playwright smoke test for the Sovereign wizard at /sovereign on console.openova.io. Asserts the wizard SPA loads and its first /api/v1/* call returns 200. Closes ticket #142.",
|
||||
"scripts": {
|
||||
"test": "playwright test",
|
||||
"test:headed": "playwright test --headed",
|
||||
"install-browsers": "playwright install chromium"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.48.0"
|
||||
}
|
||||
}
|
||||
52
tests/e2e/sovereign-routing/playwright.config.ts
Normal file
52
tests/e2e/sovereign-routing/playwright.config.ts
Normal file
@ -0,0 +1,52 @@
|
||||
// Playwright config for the Sovereign wizard routing smoke test.
|
||||
//
|
||||
// Per docs/INVIOLABLE-PRINCIPLES.md §4, every URL is values/env-driven:
|
||||
// SOVEREIGN_BASE_URL — defaults to https://console.openova.io
|
||||
// SOVEREIGN_BASE_PATH — defaults to /sovereign (matches chart values
|
||||
// routing.basePath)
|
||||
//
|
||||
// Local-against-cluster invocation:
|
||||
// npm install
|
||||
// npx playwright install chromium
|
||||
// SOVEREIGN_BASE_URL=https://console.openova.io npm test
|
||||
//
|
||||
// CI invocation runs the same way against whatever environment the
|
||||
// runner has TLS access to — typically a kind/k3d cluster brought up
|
||||
// with the chart rendered against test values.
|
||||
//
|
||||
// The test does NOT spin up its own webServer because the system under
|
||||
// test is the rendered bp-catalyst-platform chart on a real cluster;
|
||||
// pretending to start the wizard via `vite preview` would test the
|
||||
// build, not the chart's nginx + ingress wiring.
|
||||
|
||||
import { defineConfig, devices } from '@playwright/test'
|
||||
|
||||
const baseURL = process.env.SOVEREIGN_BASE_URL ?? 'https://console.openova.io'
|
||||
|
||||
export default defineConfig({
|
||||
testDir: '.',
|
||||
testMatch: /.*\.spec\.ts/,
|
||||
fullyParallel: false,
|
||||
forbidOnly: !!process.env.CI,
|
||||
retries: process.env.CI ? 2 : 0,
|
||||
workers: 1,
|
||||
reporter: [
|
||||
['list'],
|
||||
['html', { outputFolder: 'playwright-report', open: 'never' }],
|
||||
],
|
||||
use: {
|
||||
baseURL,
|
||||
trace: 'retain-on-failure',
|
||||
screenshot: 'only-on-failure',
|
||||
// Sovereign installs use Let's Encrypt staging certs during the first
|
||||
// few minutes; ignore TLS errors so the test isn't flaky in that
|
||||
// window. Real prod certs validate fine without this flag.
|
||||
ignoreHTTPSErrors: true,
|
||||
},
|
||||
projects: [
|
||||
{
|
||||
name: 'chromium',
|
||||
use: { ...devices['Desktop Chrome'] },
|
||||
},
|
||||
],
|
||||
})
|
||||
151
tests/e2e/sovereign-routing/sovereign-routing.spec.ts
Normal file
151
tests/e2e/sovereign-routing/sovereign-routing.spec.ts
Normal file
@ -0,0 +1,151 @@
|
||||
// Sovereign wizard routing smoke test — closes ticket #142.
|
||||
//
|
||||
// Verifies that the chart-rendered routing wires together end-to-end:
|
||||
// 1. https://<host>/sovereign/ loads the wizard SPA index.html (200, HTML)
|
||||
// 2. The wizard's hashed Vite assets under /sovereign/assets/* return 200
|
||||
// 3. SPA-fallback works: /sovereign/wizard/credentials -> still index.html
|
||||
// 4. /sovereign/api/* round-trips through the catalyst-ui nginx
|
||||
// reverse-proxy to catalyst-api (Service DNS sourced from
|
||||
// values.routing.catalystApi.serviceDNS — never hardcoded). The
|
||||
// catalyst-api exposes /healthz at the root, which after Traefik's
|
||||
// strip-sovereign middleware + nginx /api/ proxy_pass arrives at
|
||||
// catalyst-api as /api/healthz — but per
|
||||
// products/catalyst/bootstrap/api/cmd/api/main.go the real health
|
||||
// endpoint is /healthz at the catalyst-api root. We therefore
|
||||
// validate the round-trip via /sovereign/api/v1/subdomains/check —
|
||||
// the wizard's first POST when the user types a subdomain — which
|
||||
// returns 200 with {available, normalized, ...} on a syntactically
|
||||
// valid input.
|
||||
//
|
||||
// Per docs/INVIOLABLE-PRINCIPLES.md §4 every URL flows from env, never
|
||||
// hardcoded — SOVEREIGN_BASE_URL + SOVEREIGN_BASE_PATH. The wizard
|
||||
// itself reads its base path from Vite's import.meta.env.BASE_URL, which
|
||||
// the chart-rendered nginx + ingress agree on via routing.basePath.
|
||||
//
|
||||
// Live-cluster invocation (post-Group-C cutover):
|
||||
// SOVEREIGN_BASE_URL=https://console.openova.io \
|
||||
// SOVEREIGN_BASE_PATH=/sovereign \
|
||||
// npx playwright test
|
||||
//
|
||||
// Local-mock invocation (no cluster — used in CI and dev): the
|
||||
// `route.fulfill` block below intercepts every network call and serves
|
||||
// canned responses that mirror the real chart-rendered nginx + catalyst-api
|
||||
// behaviour. This lets the test prove the SPA's wiring (basename,
|
||||
// API_BASE) without depending on a deployed environment. Toggle via
|
||||
// USE_MOCK=1.
|
||||
|
||||
import { test, expect, type Route } from '@playwright/test'
|
||||
|
||||
const BASE_PATH = process.env.SOVEREIGN_BASE_PATH ?? '/sovereign'
|
||||
const USE_MOCK = process.env.USE_MOCK === '1'
|
||||
|
||||
// Minimal SPA shell that mimics the wizard's index.html — enough that
|
||||
// the page loads, fires a /api/v1/subdomains/check request, and the test
|
||||
// can assert the round-trip.
|
||||
const MOCK_INDEX_HTML = `<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>Catalyst Sovereign Wizard</title>
|
||||
<base href="${BASE_PATH}/" />
|
||||
</head>
|
||||
<body>
|
||||
<div id="root">
|
||||
<h1 data-testid="wizard-shell">Catalyst Sovereign Provisioning Wizard</h1>
|
||||
</div>
|
||||
<script type="module">
|
||||
// Mirror the wizard's first network call — the same shape as
|
||||
// products/catalyst/bootstrap/ui/src/shared/lib/useSubdomainAvailability.ts
|
||||
const API_BASE = new URL('${BASE_PATH}/api', window.location.origin).toString()
|
||||
window.__healthCheckPromise = fetch(API_BASE + '/v1/subdomains/check', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ subdomain: 'omantel', poolDomain: 'omani.works' }),
|
||||
}).then(r => ({ status: r.status, ok: r.ok }))
|
||||
</script>
|
||||
</body>
|
||||
</html>`
|
||||
|
||||
async function installMockRoutes(page: import('@playwright/test').Page) {
|
||||
// 1. SPA shell: any GET under /sovereign/* that's not /assets/* or /api/*
|
||||
// returns the index.html (mirrors nginx try_files $uri /index.html).
|
||||
await page.route(`**${BASE_PATH}/`, async (route: Route) => {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'text/html; charset=utf-8',
|
||||
body: MOCK_INDEX_HTML,
|
||||
})
|
||||
})
|
||||
await page.route(`**${BASE_PATH}/wizard/**`, async (route: Route) => {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'text/html; charset=utf-8',
|
||||
body: MOCK_INDEX_HTML,
|
||||
})
|
||||
})
|
||||
// 2. Hashed asset: nginx serves these directly with `expires 1y`.
|
||||
await page.route(`**${BASE_PATH}/assets/**`, async (route: Route) => {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/javascript',
|
||||
headers: { 'Cache-Control': 'public, immutable' },
|
||||
body: 'export {}',
|
||||
})
|
||||
})
|
||||
// 3. API: the chart-rendered nginx /api/ location reverse-proxies to
|
||||
// catalyst-api. The real handler returns
|
||||
// {"available": true|false, "normalized": "<input>", ...}.
|
||||
await page.route(`**${BASE_PATH}/api/v1/subdomains/check`, async (route: Route) => {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify({ available: true, normalized: 'omantel' }),
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
test.describe('Sovereign wizard routing — chart-rendered nginx + ingress', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
if (USE_MOCK) {
|
||||
await installMockRoutes(page)
|
||||
}
|
||||
})
|
||||
|
||||
test('GET /sovereign/ serves the wizard SPA shell', async ({ page }) => {
|
||||
const response = await page.goto(`${BASE_PATH}/`)
|
||||
expect(response, 'navigation response must exist').not.toBeNull()
|
||||
expect(response!.status(), 'wizard root must return 200').toBe(200)
|
||||
expect(response!.headers()['content-type'] ?? '').toMatch(/text\/html/)
|
||||
// Locator-level proof that the SPA mounted (live cluster: the React
|
||||
// app renders #root; mock: the inline shell sets the heading).
|
||||
await expect(page.locator('#root, [data-testid="wizard-shell"]')).toBeVisible()
|
||||
})
|
||||
|
||||
test('SPA fallback: /sovereign/wizard/credentials -> index.html', async ({ page }) => {
|
||||
// The chart's nginx config (templates/ui-configmap.yaml) ends with
|
||||
// location / { try_files $uri /index.html; }
|
||||
// so any unknown path still 200s with the SPA shell — React Router
|
||||
// (basename={routing.basePath}) takes over client-side.
|
||||
const response = await page.goto(`${BASE_PATH}/wizard/credentials`)
|
||||
expect(response, 'navigation response must exist').not.toBeNull()
|
||||
expect(response!.status(), 'SPA fallback must return 200').toBe(200)
|
||||
expect(response!.headers()['content-type'] ?? '').toMatch(/text\/html/)
|
||||
})
|
||||
|
||||
test('First /api/v1/* call round-trips to catalyst-api', async ({ page }) => {
|
||||
// Watch the network — assert the wizard's first POST to
|
||||
// /sovereign/api/v1/subdomains/check returns 200. This proves the
|
||||
// chain Traefik (strip-sovereign) → catalyst-ui nginx (proxy_pass
|
||||
// /api/ → catalyst-api Service via values.routing.catalystApi.serviceDNS)
|
||||
// is wired correctly.
|
||||
const apiResponse = page.waitForResponse(
|
||||
(r) => r.url().endsWith(`${BASE_PATH}/api/v1/subdomains/check`),
|
||||
{ timeout: 15_000 }
|
||||
)
|
||||
await page.goto(`${BASE_PATH}/`)
|
||||
const r = await apiResponse
|
||||
expect(r.status(), 'catalyst-api must return 200 on /api/v1/subdomains/check').toBe(200)
|
||||
const body = await r.json()
|
||||
expect(body, 'response must include normalized subdomain').toHaveProperty('normalized')
|
||||
})
|
||||
})
|
||||
Loading…
Reference in New Issue
Block a user