Files
sub2api-cn-relay-manager/scripts/test/verify_frontend_smoke.sh
phamnazage-jpg 77b7f7f660
Some checks failed
CI / Build & Test (push) Has been cancelled
CI / Lint (push) Has been cancelled
CI / Security Scan (push) Has been cancelled
CI / Docker Build (push) Has been cancelled
CI / Release (push) Has been cancelled
feat: harden runtime import and frontend verification workflows
2026-06-04 20:02:36 +08:00

550 lines
16 KiB
Bash
Executable File

#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
PORTAL_ROOT="$ROOT_DIR/deploy/tksea-portal"
ARTIFACT_DIR="${ARTIFACT_DIR:-$(mktemp -d "/tmp/sub2api-cn-relay-manager-frontend-smoke-XXXXXX")}"
WORK_DIR="$(mktemp -d "/tmp/sub2api-cn-relay-manager-frontend-smoke-work-XXXXXX")"
PORT_FILE="$WORK_DIR/server-port.txt"
SERVER_LOG="$WORK_DIR/server.log"
SERVER_SCRIPT="$WORK_DIR/frontend_smoke_server.py"
CHROMIUM_BIN="${CHROMIUM_BIN:-}"
USER_DATA_DIR="$WORK_DIR/chromium-profile"
cleanup() {
if [[ -n "${SERVER_PID:-}" ]]; then
kill "$SERVER_PID" >/dev/null 2>&1 || true
wait "$SERVER_PID" >/dev/null 2>&1 || true
fi
rm -rf "$WORK_DIR"
}
trap cleanup EXIT
fail() {
echo "FAIL: $*" >&2
exit 1
}
assert_contains_file() {
local file="$1"
local needle="$2"
if ! grep -Fq "$needle" "$file"; then
fail "expected [$needle] in $file"
fi
}
find_chromium() {
if [[ -n "$CHROMIUM_BIN" ]]; then
printf '%s\n' "$CHROMIUM_BIN"
return 0
fi
local candidate
for candidate in chromium chromium-browser google-chrome google-chrome-stable; do
if command -v "$candidate" >/dev/null 2>&1; then
command -v "$candidate"
return 0
fi
done
return 1
}
CHROMIUM_BIN="$(find_chromium)" || fail "missing chromium-compatible browser; set CHROMIUM_BIN explicitly"
[[ -x "$CHROMIUM_BIN" ]] || fail "chromium binary is not executable: $CHROMIUM_BIN"
[[ -d "$PORTAL_ROOT" ]] || fail "missing portal root: $PORTAL_ROOT"
mkdir -p "$ARTIFACT_DIR" "$USER_DATA_DIR"
cat >"$SERVER_SCRIPT" <<'PY'
#!/usr/bin/env python3
import json
import mimetypes
import os
from http.server import SimpleHTTPRequestHandler, ThreadingHTTPServer
from pathlib import Path
from urllib.parse import parse_qs, urlparse
ROOT = Path(os.environ["PORTAL_ROOT"]).resolve()
PORT_FILE = Path(os.environ["PORT_FILE"])
def json_response(handler, payload, status=200):
body = json.dumps(payload, ensure_ascii=False).encode("utf-8")
handler.send_response(status)
handler.send_header("Content-Type", "application/json; charset=utf-8")
handler.send_header("Content-Length", str(len(body)))
handler.end_headers()
handler.wfile.write(body)
def text_response(handler, payload, status=200):
body = payload.encode("utf-8")
handler.send_response(status)
handler.send_header("Content-Type", "text/plain; charset=utf-8")
handler.send_header("Content-Length", str(len(body)))
handler.end_headers()
handler.wfile.write(body)
def sample_portal_logical_groups():
return {
"logical_groups": [
{
"logical_group_id": "smoke-portal-group",
"display_name": "Smoke Portal Group",
"description": "用于最小前端 smoke 的逻辑分组样本。",
"public_models": [{"public_model": "gpt-5.4"}],
"active_route_count": 1,
"visibility_scope": "public",
"package_tier": "standard",
"usage_scenario": "浏览器级 smoke 验证",
"recommendation": "先确认页面可打开,再验证目录与导航。",
"next_step_hint": "如需完整验收,再跑真实宿主 acceptance。",
"purchase_cta_label": "申请测试 Key",
"purchase_cta_url": "/portal/",
}
]
}
def sample_groups_available():
return [
{
"id": 101,
"name": "OpenAI 中转默认分组",
}
]
def sample_subscriptions():
return [
{
"id": 501,
"group_id": 101,
"status": "active",
"expires_at": "2099-12-31T00:00:00Z",
}
]
def sample_keys():
return {
"items": [
{
"id": 1,
"name": "Smoke Key",
"group_id": 101,
"key": "sk-smoke-visible-key",
"status": "active",
"created_at": "2099-01-01T00:00:00Z",
"expires_at": "2099-12-31T00:00:00Z",
}
]
}
def sample_admin_session():
return {
"authenticated": True,
"login_enabled": True,
"username": "smoke-admin",
"expires_at": "2099-12-31T00:00:00Z",
}
def sample_logical_groups():
return {
"logical_groups": [
{
"logical_group_id": "smoke-lg-001",
"display_name": "Smoke Logical Group",
"status": "active",
"description": "Smoke logical group",
"shadow_group_id": "shadow-smoke-group",
"shadow_host_id": "shadow-smoke-host",
"routes": [],
"public_models": [],
}
]
}
def sample_route_health():
return {
"route_health": [
{
"route_id": "smoke-route-primary",
"logical_group_id": "smoke-lg-001",
"runtime_status": "healthy",
"priority": 10,
"weight": 100,
"public_model_count": 1,
"recent_failover_count": 0,
"last_error_class": "",
"cooldown_reason": "",
}
]
}
def sample_provider_accounts():
return {
"provider_accounts": [
{
"id": 2001,
"display_name": "Smoke Provider Account",
"provider_id": "smoke-provider",
"host_id": "host-smoke-001",
"status": "active",
"binding_state": "assigned",
"binding_candidate_count": 1,
"logical_group_id": "smoke-lg-001",
"route_id": "smoke-route-primary",
"shadow_group_id": "shadow-smoke-group",
"shadow_host_id": "shadow-smoke-host",
}
]
}
def sample_packs():
return {
"packs": [
{
"pack_id": "openai-cn-pack",
"display_name": "OpenAI CN Pack",
"provider_count": 1,
}
]
}
def sample_hosts():
return {
"hosts": [
{
"host_id": "host-smoke-001",
"name": "Smoke Host",
"base_url": "https://host-smoke.example.com",
}
]
}
def sample_pack_providers():
return {
"providers": [
{
"provider_id": "smoke-provider",
"display_name": "Smoke Provider",
"platform": "openai",
"base_url": "https://provider-smoke.example.com/v1",
"smoke_test_model": "gpt-5.4",
"supported_models": ["gpt-5.4"],
"host_overlays": 0,
}
]
}
def sample_provider_drafts():
return {"provider_drafts": []}
def sample_batch_run():
return {
"run": {
"run_id": "smoke-run-001",
"status": "succeeded",
"matched_account_state_summary": {"created": 1},
}
}
def sample_batch_items():
return {
"items": [
{
"provider_id": "smoke-provider",
"matched_account_state": "created",
"account_resolution": "created",
"provision_reused": False,
}
]
}
class Handler(SimpleHTTPRequestHandler):
def log_message(self, fmt, *args):
return
def serve_static(self, rel_path):
file_path = (ROOT / rel_path).resolve()
if not file_path.exists() or ROOT not in file_path.parents and file_path != ROOT:
self.send_error(404, "not found")
return
content = file_path.read_bytes()
mime_type, _ = mimetypes.guess_type(str(file_path))
self.send_response(200)
self.send_header("Content-Type", f"{mime_type or 'text/plain'}; charset=utf-8")
self.send_header("Content-Length", str(len(content)))
self.end_headers()
self.wfile.write(content)
def do_GET(self):
parsed = urlparse(self.path)
path = parsed.path
query = parse_qs(parsed.query)
if path == "/healthz":
text_response(self, "ok")
return
if path == "/portal":
self.send_response(302)
self.send_header("Location", "/portal/")
self.end_headers()
return
if path == "/portal/admin":
self.send_response(302)
self.send_header("Location", "/portal/admin/")
self.end_headers()
return
if path == "/portal/":
self.serve_static("index.html")
return
if path == "/portal/admin/":
self.serve_static("admin/index.html")
return
if path.startswith("/portal/"):
rel_path = path[len("/portal/"):]
self.serve_static(rel_path)
return
if path == "/portal-proxy/api/v1/auth/me":
json_response(
self,
{
"code": 0,
"data": {
"id": 42,
"email": "smoke-user@example.com",
"allowed_groups": [101],
},
},
)
return
if path == "/portal-proxy/api/v1/groups/available":
json_response(self, {"code": 0, "data": sample_groups_available()})
return
if path == "/portal-proxy/api/v1/subscriptions":
json_response(self, {"code": 0, "data": sample_subscriptions()})
return
if path == "/portal-proxy/api/v1/keys":
json_response(self, {"code": 0, "data": sample_keys()})
return
if path == "/portal-admin-api/api/portal/logical-groups":
json_response(self, sample_portal_logical_groups())
return
if path == "/portal-admin-api/api/admin/session":
json_response(self, sample_admin_session())
return
if path == "/portal-admin-api/api/logical-groups":
json_response(self, sample_logical_groups())
return
if path == "/portal-admin-api/api/routing/routes/health":
json_response(self, sample_route_health())
return
if path == "/portal-admin-api/api/provider-accounts":
json_response(self, sample_provider_accounts())
return
if path == "/portal-admin-api/api/packs":
json_response(self, sample_packs())
return
if path == "/portal-admin-api/api/hosts":
json_response(self, sample_hosts())
return
if path == "/portal-admin-api/api/provider-drafts":
json_response(self, sample_provider_drafts())
return
if path == "/portal-admin-api/api/batch-import/runs/smoke-run-001":
json_response(self, sample_batch_run())
return
if path == "/portal-admin-api/api/batch-import/runs/smoke-run-001/items":
json_response(self, sample_batch_items())
return
if path.startswith("/portal-admin-api/api/packs/") and path.endswith("/providers"):
json_response(self, sample_pack_providers())
return
if path.startswith("/portal-admin-api/api/provider-accounts/") and path.endswith("/binding-candidates"):
json_response(
self,
{
"binding_candidates": [
{
"logical_group_id": "smoke-lg-001",
"route_id": "smoke-route-primary",
}
]
},
)
return
if path.startswith("/portal-admin-api/api/"):
json_response(self, {"ok": True, "path": path, "query": query})
return
self.send_error(404, "not found")
def do_POST(self):
parsed = urlparse(self.path)
path = parsed.path
if path == "/portal-admin-api/api/admin/session/login":
json_response(self, sample_admin_session())
return
if path == "/portal-admin-api/api/admin/session/logout":
json_response(self, {"ok": True})
return
if path == "/portal-admin-api/api/batch-import/runs":
json_response(
self,
{
"run": {
"run_id": "smoke-run-001",
"status": "created",
}
},
)
return
json_response(self, {"ok": True, "path": path})
server = ThreadingHTTPServer(("127.0.0.1", 0), Handler)
PORT_FILE.write_text(str(server.server_port), encoding="utf-8")
server.serve_forever()
PY
PORTAL_ROOT="$PORTAL_ROOT" PORT_FILE="$PORT_FILE" python3 "$SERVER_SCRIPT" >"$SERVER_LOG" 2>&1 &
SERVER_PID=$!
for _ in $(seq 1 50); do
if [[ -s "$PORT_FILE" ]]; then
break
fi
sleep 0.1
done
if [[ ! -s "$PORT_FILE" ]]; then
if [[ -s "$SERVER_LOG" ]]; then
cat "$SERVER_LOG" >&2 || true
fi
fail "frontend smoke server did not start"
fi
SERVER_PORT="$(cat "$PORT_FILE")"
BASE_URL="http://127.0.0.1:$SERVER_PORT"
for _ in $(seq 1 50); do
if curl -fsS "$BASE_URL/healthz" >/dev/null 2>&1; then
break
fi
sleep 0.1
done
curl -fsS "$BASE_URL/healthz" >/dev/null 2>&1 || fail "frontend smoke server is not healthy"
dump_dom() {
local label="$1"
local url="$2"
local output="$ARTIFACT_DIR/${label}.dom.html"
"$CHROMIUM_BIN" \
--headless \
--disable-gpu \
--no-sandbox \
--user-data-dir="$USER_DATA_DIR/$label" \
--virtual-time-budget=10000 \
--dump-dom \
"$url" >"$output" 2>"$ARTIFACT_DIR/${label}.stderr.txt"
printf '%s\n' "$output"
}
portal_dom="$(dump_dom "00-portal" "$BASE_URL/portal/")"
admin_home_dom="$(dump_dom "01-admin-home" "$BASE_URL/portal/admin/")"
logical_groups_dom="$(dump_dom "02-logical-groups" "$BASE_URL/portal/admin/logical-groups.html")"
route_health_dom="$(dump_dom "03-route-health" "$BASE_URL/portal/admin/route-health.html")"
accounts_dom="$(dump_dom "04-accounts" "$BASE_URL/portal/admin/accounts.html")"
providers_dom="$(dump_dom "05-providers" "$BASE_URL/portal/admin/providers.html")"
batch_dom="$(dump_dom "06-batch-import" "$BASE_URL/portal/admin-batch-import.html")"
compat_batch_dom="$(dump_dom "07-batch-import-compat" "$BASE_URL/portal/admin/batch-import.html")"
assert_contains_file "$portal_dom" "Sub2API 多模型接入中心"
assert_contains_file "$portal_dom" "Smoke Portal Group"
assert_contains_file "$portal_dom" "逻辑分组目录"
assert_contains_file "$portal_dom" "申请测试 Key"
assert_contains_file "$admin_home_dom" "Admin Portal"
assert_contains_file "$admin_home_dom" "/portal/admin/providers.html"
assert_contains_file "$admin_home_dom" "/portal/admin/accounts.html"
assert_contains_file "$logical_groups_dom" "Logical Group Admin"
assert_contains_file "$logical_groups_dom" "smoke-admin"
assert_contains_file "$logical_groups_dom" "Smoke Logical Group"
assert_contains_file "$route_health_dom" "Route Health Admin"
assert_contains_file "$route_health_dom" "smoke-admin"
assert_contains_file "$route_health_dom" "smoke-route-primary"
assert_contains_file "$accounts_dom" "Provider Accounts Admin"
assert_contains_file "$accounts_dom" "smoke-admin"
assert_contains_file "$accounts_dom" "Smoke Provider Account"
assert_contains_file "$providers_dom" "Provider Admin"
assert_contains_file "$providers_dom" "smoke-admin"
assert_contains_file "$providers_dom" "保存到服务端"
assert_contains_file "$batch_dom" "Batch Import Admin"
assert_contains_file "$batch_dom" "smoke-admin"
assert_contains_file "$batch_dom" "matched_account_state"
assert_contains_file "$compat_batch_dom" "Batch Import Admin"
assert_contains_file "$compat_batch_dom" "smoke-admin"
cat >"$ARTIFACT_DIR/99-summary.json" <<EOF
{
"server_port": $SERVER_PORT,
"portal_url": "$BASE_URL/portal/",
"admin_urls": [
"$BASE_URL/portal/admin/",
"$BASE_URL/portal/admin/logical-groups.html",
"$BASE_URL/portal/admin/route-health.html",
"$BASE_URL/portal/admin/accounts.html",
"$BASE_URL/portal/admin/providers.html",
"$BASE_URL/portal/admin-batch-import.html",
"$BASE_URL/portal/admin/batch-import.html"
],
"session_username": "smoke-admin",
"result": "pass"
}
EOF
echo "PASS: frontend browser smoke passed"
echo "artifact dir: $ARTIFACT_DIR"