visionA/visionA-backend/scripts/e2e-manual-test.sh
jim800121chen 22f0837ba8 feat(visionA-backend): Phase 0 → 0.7 雲端後端(雙 binary + OIDC BFF + stage 部署)
從 edge-ai-platform POC 轉為正式產品的雲端後端,含以下整合階段:

- Phase 0:雛形骨架 — `cmd/api-server` (REST :3721) + `cmd/remote-proxy`
  (tunnel :3800 / internal :3801) 雙 binary 共用 internal/,沿用 POC 的
  WebSocket+yamux tunnel 協定但解耦 relay 與 API
- Phase 0.6:OIDC BFF 接 Innovedus Member Center
  - internal/oidc package(coreos/go-oidc + PKCE S256 + state + nonce)
  - internal/usersession package(HMAC-SHA256 cookie + RotateSessionID
    防 session fixation, OWASP ASVS V3.2.1)
  - 4 個 OIDC handler(/api/auth/login|callback|me|logout)+ AuthMiddleware
  - 完全拔除 StaticAuthProvider,OIDC 是唯一認證路徑
  - 9 個 ADR(含 ADR-010 BFF / ADR-011 取代 static auth /
    ADR-012 pending session shared cookie / ADR-013 PKCE-only public client)
- Phase 0.7:A1 改造 + security audit 修復
  - OIDC ClientSecret 變選填,支援 stage MC 的 public PKCE-only client
    (AuthStyleInParams 強制 token endpoint 不送 client_secret)
  - 預留 ServiceClient* 欄位給未來 client_credentials grant
  - 移除 13+ 處 resolveUserID(uc, StaticUserID) fallback 改 strict mode
    (Audit C1:multi-tenant 隔離破口)
  - Pairing exchange MarkUsed 失敗 abort + revoke session token(Audit M3)
  - 新增 all_endpoints_require_auth_test 整合測試(51 endpoint × 401)

驗證:go test -race -count=3 ./... 17 packages 全綠 / go vet 0 warning

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 11:21:20 +08:00

297 lines
9.6 KiB
Bash
Executable File
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env bash
# e2e-manual-test.sh — 手動端到端驗證腳本AB13
#
# 目的:真實 spawn visionA-backendapi-server + remote-proxy+ 一個 fake
# local-tool HTTP server搭配 visiona-agent或手動 curl 模擬)驗證整條
# 雲端版架構跑得起來。
#
# 這個 script 不進 CI。CI 用的自動化 e2e 已在
# visionA-backend/cmd/api-server/e2e_full_flow_test.go
# 用 single-process in-memory integration 的方式覆蓋(跨 Go module 太重,
# 不適合 go test
#
# 使用情境:
# - 本機開發時手動驗證 binary build 出來是不是真的能跑
# - 交付雛形前的 smoke test
# - Debug 真實網路 / TLS / CORS 問題in-memory test 無法覆蓋)
#
# 用法:
#
# # Terminal 1 — 啟動 backend + fake local-tool
# bash visionA-backend/scripts/e2e-manual-test.sh backend
#
# # Terminal 2 — 取得 pairing token然後手動把它貼進 visiona-agent UI
# bash visionA-backend/scripts/e2e-manual-test.sh token
#
# # Terminal 3可選 — agent 連上後,模擬前端打 API
# bash visionA-backend/scripts/e2e-manual-test.sh forward
#
# 或直接 `bash ... all` 跑完整流程backend + fake local但 agent 還是得手動跑)。
#
# 對應文件:
# - .autoflow/04-architecture/visiona-agent-tdd.md §11e2e testing
# - .autoflow/04-architecture/tunnel.md §3資料流
set -euo pipefail
# ----------------------------------------------------------------------
# 設定
# ----------------------------------------------------------------------
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
BACKEND_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
# Port 預設值刻意避開:
# - 3721 (local-tool 預設)
# - 常見開發 port (3000/8080/5173)
# 使用者自己有衝突時可用環境變數覆寫。
API_HOST="${API_HOST:-127.0.0.1}"
API_PORT="${API_PORT:-13721}"
TUNNEL_PORT="${TUNNEL_PORT:-13800}"
PROXY_INTERNAL_PORT="${PROXY_INTERNAL_PORT:-13801}"
FAKE_LOCAL_PORT="${FAKE_LOCAL_PORT:-38721}"
API_URL="http://${API_HOST}:${API_PORT}"
RELAY_WS_URL="ws://${API_HOST}:${TUNNEL_PORT}"
# 背景 PID 清單trap 清理用)
PIDS=()
cleanup() {
echo ""
echo "[e2e] 清理中..."
# Bash 3.2macOS 預設)展開空 array 時會觸發 nounset用 +u 暫時放寬。
set +u
for pid in "${PIDS[@]}"; do
if kill -0 "$pid" 2>/dev/null; then
kill "$pid" 2>/dev/null || true
fi
done
set -u
wait 2>/dev/null || true
echo "[e2e] 清理完成"
}
trap cleanup INT TERM EXIT
# ----------------------------------------------------------------------
# 子命令
# ----------------------------------------------------------------------
# 啟 fake local-tool用 Python 內建 http.server 回固定 JSON。
# 避免再建一個 Go binary讓使用者少裝東西。
start_fake_local() {
echo "[e2e] 啟動 fake local-toolport ${FAKE_LOCAL_PORT}..."
python3 - <<PY &
import http.server, json, sys
PORT = ${FAKE_LOCAL_PORT}
class H(http.server.BaseHTTPRequestHandler):
def do_POST(self):
self.send_response(200)
self.send_header('Content-Type','application/json')
self.send_header('X-Backend-Source','e2e-fake-local')
self.end_headers()
self.wfile.write(json.dumps({
'scanned': 2,
'devices': [
{'id':'kl520-e2e-01','type':'kl520','status':'online'},
{'id':'kl730-e2e-02','type':'kl730','status':'online'},
],
'path': self.path,
}).encode())
def do_GET(self):
self.send_response(200)
self.send_header('Content-Type','application/json')
self.end_headers()
self.wfile.write(json.dumps({'ok':True,'path':self.path}).encode())
def log_message(self, fmt, *args):
sys.stderr.write("[fake-local] " + (fmt%args) + "\n")
http.server.HTTPServer(('127.0.0.1', PORT), H).serve_forever()
PY
PIDS+=($!)
sleep 0.3
echo "[e2e] fake local-tool 就緒http://127.0.0.1:${FAKE_LOCAL_PORT}"
}
start_backend() {
echo "[e2e] 建置 api-server + remote-proxy..."
(cd "$BACKEND_DIR" && make build >/dev/null)
echo "[e2e] 啟動 remote-proxytunnel=${TUNNEL_PORT}, internal=${PROXY_INTERNAL_PORT}..."
(
cd "$BACKEND_DIR"
VISIONA_TUNNEL_PORT=$TUNNEL_PORT \
VISIONA_PROXY_INTERNAL_PORT=$PROXY_INTERNAL_PORT \
VISIONA_LOG_LEVEL=info \
./bin/remote-proxy
) &
PIDS+=($!)
sleep 0.5
echo "[e2e] 啟動 api-serverport=${API_PORT}..."
(
cd "$BACKEND_DIR"
VISIONA_HOST=$API_HOST \
VISIONA_API_PORT=$API_PORT \
VISIONA_PROXY_INTERNAL_URL="http://127.0.0.1:${PROXY_INTERNAL_PORT}" \
VISIONA_RELAY_PUBLIC_URL="$RELAY_WS_URL" \
VISIONA_LOG_LEVEL=info \
./bin/api-server
) &
PIDS+=($!)
# bash 3.2 (macOS 預設) 不支援負數索引;用 length-1
API_PID="${PIDS[$((${#PIDS[@]}-1))]}"
# 等 api-server ready — 同時檢查我們 spawn 的 PID 還活著,避免 port 被別人佔。
echo -n "[e2e] 等 api-server 就緒"
ready=0
for i in $(seq 1 30); do
if ! kill -0 "$API_PID" 2>/dev/null; then
echo ""
echo "[e2e] ✗ api-server 進程已結束(可能 port ${API_PORT} 被佔用,試試 API_PORT=xxx $0 backend"
exit 1
fi
if curl -sf "$API_URL/healthz" >/dev/null 2>&1; then
ready=1
echo " ✓"
break
fi
echo -n "."
sleep 0.3
done
if [ "$ready" = "0" ]; then
echo ""
echo "[e2e] ✗ api-server 在 9 秒內沒就緒"
exit 1
fi
}
cmd_backend() {
start_fake_local
start_backend
echo ""
echo "==================================================================="
echo "[e2e] backend 已就緒。下一步:"
echo ""
echo " 1) 在另一個 terminal 執行:"
echo " bash $0 token"
echo " 取得 pairing token"
echo ""
echo " 2) 啟動 visiona-agentwails dev 或 binary把 pairing token 貼進 UI"
echo " 並設定環境:"
echo " VISIONA_RELAY_HTTP_URL=$API_URL"
echo " VISIONA_PAIRING_MOCK=false"
echo " VISIONA_LOCAL_ADDR=127.0.0.1:${FAKE_LOCAL_PORT}"
echo ""
echo " 3) agent online 後,在另一 terminal 執行:"
echo " bash $0 forward"
echo " 模擬前端打 API看能不能透過 tunnel forward 到 fake local-tool"
echo ""
echo " Ctrl+C 結束 backend"
echo "==================================================================="
echo ""
wait
}
cmd_token() {
echo "[e2e] 向 $API_URL 要 pairing token..."
if ! resp=$(curl -sf -X POST "$API_URL/api/pairing/token" 2>/dev/null); then
echo "[e2e] ✗ api-server 沒回應(是否先跑 \`bash $0 backend\`"
exit 1
fi
if [ -z "$resp" ]; then
echo "[e2e] ✗ api-server 回空回應"
exit 1
fi
# 安全萃取 tokenJSON 結構不對時印友善錯誤而非 Python stack trace。
# 用 .get 鏈避免 KeyError空字串再由 shell 檢查。
token=$(echo "$resp" | python3 -c '
import sys, json
try:
data = json.load(sys.stdin)
except json.JSONDecodeError:
sys.exit(0)
print(data.get("data", {}).get("token", "") if isinstance(data, dict) else "")
' 2>/dev/null)
if [ -z "$token" ]; then
echo "[e2e] ✗ 無法從 api-server 回應萃取 pairing token"
echo "[e2e] 原始回應:"
echo "$resp" | head -c 500
echo ""
echo "[e2e] 預期格式:{\"data\": {\"token\": \"...\"}}"
exit 1
fi
echo ""
echo "pairing_token: $token"
echo ""
echo "把這個 token 貼到 agent UI 的「配對」欄位。"
}
cmd_forward() {
echo "[e2e] 打 $API_URL/api/devices/scan需要 agent 已連上 tunnel..."
resp=$(curl -s -w "\nHTTP_STATUS:%{http_code}" -X POST "$API_URL/api/devices/scan" \
-H "Content-Type: application/json")
status=$(echo "$resp" | grep HTTP_STATUS | cut -d: -f2)
body=$(echo "$resp" | sed '/HTTP_STATUS/d')
echo "HTTP $status"
echo "body:"
echo "$body" | python3 -m json.tool 2>/dev/null || echo "$body"
if [ "$status" = "200" ]; then
echo ""
echo "[e2e] ✓ 完整 e2e 鏈路通過:"
echo " browser → api-server → remote-proxy → tunnel → agent → fake local-tool"
if echo "$body" | grep -q "e2e-fake-local"; then
echo "[e2e] ✓ 確認 response 來自 fake local-toolX-Backend-Source 比對通過)"
fi
elif [ "$status" = "502" ]; then
echo ""
echo "[e2e] ✗ 502 — tunnel 未建立。可能原因:"
echo " - agent 還沒啟動 / 還沒配對"
echo " - session token 被清掉backend 重啟過)"
echo " - VISIONA_RELAY_HTTP_URL 沒指對"
else
echo ""
echo "[e2e] ✗ 意外的狀態碼 $status"
fi
}
cmd_health() {
echo "[e2e] GET $API_URL/api/system/health"
curl -sf "$API_URL/api/system/health" | python3 -m json.tool
}
usage() {
cat <<EOF
用法: $0 {backend|token|forward|health|all}
backend 啟動 remote-proxy + api-server + fake local-tool前景 blocking
token 向 api-server 要一個 pairing token貼進 agent UI 用)
forward 打 POST /api/devices/scan 驗證 tunnel forward 通agent 需已連上)
health GET /api/system/health 看 tunnel 連線狀況
all 同 backend
環境變數:
API_HOST api-server 綁的 host預設 127.0.0.1
API_PORT api-server port預設 3721
TUNNEL_PORT remote-proxy tunnel WS port預設 3800
PROXY_INTERNAL_PORT remote-proxy internal HTTP port預設 3801
FAKE_LOCAL_PORT fake local-tool port預設 38721
需要先安裝go, make, python3, curl
EOF
}
# ----------------------------------------------------------------------
# 入口
# ----------------------------------------------------------------------
case "${1:-}" in
backend|all) cmd_backend ;;
token) cmd_token ;;
forward) cmd_forward ;;
health) cmd_health ;;
""|-h|--help|help) usage ;;
*) echo "未知指令:$1"; echo ""; usage; exit 1 ;;
esac