從 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>
297 lines
9.6 KiB
Bash
Executable File
297 lines
9.6 KiB
Bash
Executable File
#!/usr/bin/env bash
|
||
# e2e-manual-test.sh — 手動端到端驗證腳本(AB13)
|
||
#
|
||
# 目的:真實 spawn visionA-backend(api-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 §11(e2e 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.2(macOS 預設)展開空 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-tool(port ${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-proxy(tunnel=${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-server(port=${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-agent(wails 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
|
||
# 安全萃取 token:JSON 結構不對時印友善錯誤而非 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-tool(X-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
|