This commit is contained in:
yujonglee 2026-10-04 23:11:37 +08:00 • committed by GitHub
commit c465386e22
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 53 additions and 12 deletions

View file

@ -23,6 +23,9 @@ import subprocess
import time
import uuid
from collections.abc import Iterator
from pathlib import PurePosixPath
from typing import Final
from urllib.parse import quote
import pytest
@ -130,3 +133,48 @@ def test_ui_serves_as_arbitrary_uid_read_only(ui_container: tuple[str, str]) ->
f"GET {path} returned {page.stdout.strip()!r} as uid {ARBITRARY_UID}.\n"
f"{_container_logs(container)}"
)
@pytest.mark.parametrize("route", ("", "teams", "mcp/oauth/callback"))
def test_ui_navigation_payloads_match_static_export(ui_container: tuple[str, str], route: str) -> None:
network, container = ui_container
export_root: Final = PurePosixPath("/usr/share/nginx/html")
payload_files: Final = _docker(
"exec", container, "find", str(export_root / route), "-maxdepth", "1", "-type", "f", "-name", "*.txt"
).stdout.splitlines()
assert payload_files, f"no navigation payloads exported for {route!r}"
for payload_file in payload_files:
relative_path: Final = PurePosixPath(payload_file).relative_to(export_root)
canonical_path: Final = f"/ui/{relative_path}"
request_paths: Final = (
(canonical_path, f"/ui/{route}.txt" if route else "/ui.txt")
if relative_path.name == "index.txt"
else (canonical_path,)
)
expected_payload: Final = _docker("exec", container, "cat", payload_file).stdout
for request_path in request_paths:
response: Final = _docker(
"run",
"--rm",
"--network",
network,
CURL_IMAGE,
"--silent",
"--show-error",
"--max-time",
"10",
"--write-out",
"\n%{http_code}\n%{content_type}",
f"http://{container}:{UI_PORT}{quote(request_path)}?_rsc=navigation",
)
body, status, content_type = response.stdout.rsplit("\n", 2)
assert status == "200", f"GET {request_path} returned {status}: {body}"
assert content_type.startswith("text/plain"), f"GET {request_path} returned {content_type}"
assert body == expected_payload, f"GET {request_path} did not serve its exported navigation payload"
def test_ui_missing_navigation_payload_returns_404(ui_container: tuple[str, str]) -> None:
network, container = ui_container
response: Final = _probe(network, container, f"/ui/teams/{uuid.uuid4().hex}.txt?_rsc=navigation")
assert response.stdout.strip() == "404", response.stdout

View file

@ -73,18 +73,11 @@ http {
# Probe target — doesn't depend on disk.
location = /healthz { default_type text/plain; return 200 "ok\n"; }
# Next.js App Router (output: "export") emits an RSC/flight payload
# as <route>.txt next to <route>.html, plus __next.*.txt segment
# data. The client router fetches these on soft navigation/prefetch
# (?_rsc=<hash>) — the query string is irrelevant, files resolve by
# $uri. These MUST be served from the export: if they fall through
# to the catch-all 404 below, client-side navigation never settles
# and the login flow spins in an infinite redirect loop
# (/ ⇄ /ui/login). Keep this BEFORE the /ui/ regex — ^/ui/(.+)$ is
# also a regex and nginx takes the first matching one, so a stray
# /ui/<page>.txt would otherwise be rewritten to HTML and break RSC
# for nested routes. A genuinely missing payload must 404 (the
# router degrades to a hard navigation); never fall back to HTML.
location = /ui.txt { try_files /index.txt =404; }
location ~ ^/ui/(.+)\.txt$ {
try_files /$1.txt /$1/index.txt =404;
}
location ~ \.txt$ {
try_files $uri =404;
}