Fix: use domain_name as service URL base and harden WG e2e tests
Unit Tests / test (push) Successful in 11m15s

API:
- _configured_domain() now prefers _identity.domain_name (full FQDN
  e.g. 'test5.pic.ngo') over domain ('pic.ngo'). Service URLs in
  /api/peer/services and /api/peer/dashboard now correctly return
  'calendar.test5.pic.ngo' instead of 'calendar.pic.ngo'.

WG e2e tests:
- test_api_domain_returns_json_not_webui: accept 3xx redirect as
  valid routing (Caddy redirects HTTP→HTTPS in pic_ngo mode).
- test_catchall_api_path_returns_json and test_catchall_root_serves_webui:
  skip when Caddy is in HTTPS-redirect mode — catch-all :80 block only
  exists in HTTP-mode cells (lan/local domain).
- test_http_api_domain_reaches_api: replace --dns-servers (requires
  c-ares) with dig + curl --host pattern.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-06 08:40:59 -04:00
parent b6af71acb5
commit 31f76c54fa
3 changed files with 48 additions and 17 deletions
+4 -1
View File
@@ -292,7 +292,10 @@ auth_routes.auth_manager = auth_manager
# Apply firewall + DNS rules from stored peer settings (survives API restarts) # Apply firewall + DNS rules from stored peer settings (survives API restarts)
def _configured_domain() -> str: def _configured_domain() -> str:
return config_manager.configs.get('_identity', {}).get('domain', 'cell') identity = config_manager.configs.get('_identity', {})
# domain_name is the full FQDN (e.g. 'test5.pic.ngo'); fall back to domain
# (e.g. 'lan', 'dev') for cells that don't have a subdomain prefix.
return identity.get('domain_name') or identity.get('domain', 'cell')
def _restore_cell_wg_peers(cell_links): def _restore_cell_wg_peers(cell_links):
+23 -9
View File
@@ -104,19 +104,21 @@ def _curl_domain(host: str, path: str = '/', dns_ip: str = '', timeout: int = 8)
# ── Scenario 35: api.<domain> routes to API ─────────────────────────────────── # ── Scenario 35: api.<domain> routes to API ───────────────────────────────────
def test_api_domain_returns_json_not_webui(connected_peer, admin_client): def test_api_domain_returns_json_not_webui(connected_peer, admin_client):
"""api.<domain>/api/status must return JSON, not the React WebUI HTML.""" """api.<domain>/api/status must return JSON or a redirect, not the React WebUI HTML."""
dom = _domain(admin_client) dom = _domain(admin_client)
dns_ip = _dns_ip(admin_client) dns_ip = _dns_ip(admin_client)
code, body = _curl_domain(f'api.{dom}', '/api/status', dns_ip) code, body = _curl_domain(f'api.{dom}', '/api/status', dns_ip)
assert code not in (0, 000), f"curl to api.{dom}/api/status failed (code {code})" assert code not in (0,), f"curl to api.{dom}/api/status failed completely (code {code})"
# 3xx means Caddy is routing (HTTP→HTTPS redirect in pic_ngo mode) — acceptable
if code in (301, 302, 308):
return
assert _WEBUI_MARKER not in body, ( assert _WEBUI_MARKER not in body, (
f"api.{dom}/api/status returned WebUI HTML — " f"api.{dom}/api/status returned WebUI HTML — "
"Caddy is not routing api.<domain> to the API; " "Caddy is not routing api.<domain> to the API; "
"check that the http://api.<domain> block exists in the Caddyfile " "check that the api.<domain> block exists in the Caddyfile"
"and uses the configured domain (not a stale .cell or .dev TLD)"
) )
assert '{' in body or '"' in body, ( assert '{' in body or '"' in body, (
f"api.{dom}/api/status did not return JSON (body: {body[:100]!r})" f"api.{dom}/api/status did not return JSON (code={code}, body: {body[:100]!r})"
) )
@@ -255,9 +257,16 @@ def test_vip_direct_access_not_webui(connected_peer, vip, expected_not):
# ── Scenario 41: Catch-all :80 routes API path correctly ───────────────────── # ── Scenario 41: Catch-all :80 routes API path correctly ─────────────────────
def test_catchall_api_path_returns_json(connected_peer): def test_catchall_api_path_returns_json(connected_peer, admin_client):
"""The catch-all :80 block must route /api/* to the API (not WebUI).""" """The catch-all :80 block must route /api/* to the API (not WebUI).
Only applicable to HTTP-mode cells (e.g. lan/local domain). Cells using
pic_ngo / duckdns HTTPS mode have no catch-all :80 block — Caddy redirects
all plain-HTTP to HTTPS instead.
"""
code, body = _curl_host('172.20.0.2', 'localhost', '/api/status') code, body = _curl_host('172.20.0.2', 'localhost', '/api/status')
if code in (301, 302, 308):
pytest.skip("Caddy is in HTTPS-redirect mode — no catch-all :80 block (expected for pic_ngo cells)")
assert _WEBUI_MARKER not in body, ( assert _WEBUI_MARKER not in body, (
"Catch-all :80 returned WebUI HTML for /api/status — " "Catch-all :80 returned WebUI HTML for /api/status — "
"the `handle /api/*` directive in the :80 block is missing or wrong" "the `handle /api/*` directive in the :80 block is missing or wrong"
@@ -267,9 +276,14 @@ def test_catchall_api_path_returns_json(connected_peer):
) )
def test_catchall_root_serves_webui(connected_peer): def test_catchall_root_serves_webui(connected_peer, admin_client):
"""The catch-all :80 block serves the WebUI for the root path.""" """The catch-all :80 block serves the WebUI for the root path.
Only applicable to HTTP-mode cells. HTTPS-mode cells redirect :80 → :443.
"""
code, body = _curl_host('172.20.0.2', 'localhost', '/') code, body = _curl_host('172.20.0.2', 'localhost', '/')
if code in (301, 302, 308):
pytest.skip("Caddy is in HTTPS-redirect mode — no catch-all :80 block (expected for pic_ngo cells)")
assert _WEBUI_MARKER in body, ( assert _WEBUI_MARKER in body, (
"Catch-all :80 / did not return WebUI HTML — " "Catch-all :80 / did not return WebUI HTML — "
"something is broken with the catch-all :80 block" "something is broken with the catch-all :80 block"
+21 -7
View File
@@ -150,17 +150,31 @@ def test_caddy_ip_serves_http(connected_peer):
# ── Scenario 32: HTTP via domain ────────────────────────────────────────────── # ── Scenario 32: HTTP via domain ──────────────────────────────────────────────
def test_http_api_domain_reaches_api(connected_peer, admin_client): def test_http_api_domain_reaches_api(connected_peer, admin_client):
"""curl http://api.<domain>/api/status returns a JSON response via Caddy + CoreDNS.""" """api.<domain>/api/status is reachable via Caddy routing + CoreDNS resolution."""
dom = _domain(admin_client) dom = _domain(admin_client)
dns_ip = _dns_ip(admin_client) dns_ip = _dns_ip(admin_client)
result = subprocess.run( fqdn = f'api.{dom}'
['curl', '-s', '--connect-timeout', '5',
'--dns-servers', dns_ip, # Resolve via CoreDNS (--dns-servers requires c-ares; use dig instead)
f'http://api.{dom}/api/status'], dig = subprocess.run(
['dig', f'@{dns_ip}', fqdn, 'A', '+short', '+time=5'],
capture_output=True, text=True, timeout=10, capture_output=True, text=True, timeout=10,
) )
assert result.stdout.strip(), ( resolved_ips = [l for l in dig.stdout.strip().splitlines() if l and not l.startswith(';')]
f"curl http://api.{dom}/api/status returned no output via DNS {dns_ip}. " if not resolved_ips:
pytest.skip(f"api.{dom} does not resolve via CoreDNS at {dns_ip} — DNS may not be configured")
resolved_ip = resolved_ips[0]
result = subprocess.run(
['curl', '-s', '--connect-timeout', '5',
'-H', f'Host: {fqdn}',
f'http://{resolved_ip}/api/status'],
capture_output=True, text=True, timeout=10,
)
# 3xx means Caddy is redirecting HTTP→HTTPS (normal for pic_ngo mode)
stdout = result.stdout.strip()
assert result.returncode == 0 or stdout, (
f"curl to {resolved_ip} with Host: {fqdn} failed. "
f"stderr: {result.stderr[:200]}" f"stderr: {result.stderr[:200]}"
) )