550 lines
16 KiB
Bash
Executable File
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"
|