#!/usr/bin/env python3 """CDP E2E 综合测试 v3 — 完整端到端验证(登录后测试所有功能)""" import json, time, urllib.request, sys from websocket import create_connection # 配置 BASE_URL = "http://localhost:8080/api/v1" FRONTEND_URL = "http://localhost:5199" CDP_URL = "http://127.0.0.1:9225" CREDENTIALS = {"username": "yeij0942", "password": "Jiang1143218570"} passed = 0 failed = 0 def check(name, condition, detail=""): global passed, failed if condition: passed += 1 print(f" ✅ {name}") else: failed += 1 print(f" ❌ {name} {detail}") # ============================================================ # Part A: Backend API Tests # ============================================================ print("=" * 60) print("Part A: Backend API 测试") print("=" * 60) # A1: Health checks print("\n--- A1: 微服务健康检查 ---") for svc, port in [("gateway", 8080), ("ai-core", 8081), ("memory-service", 8091), ("tool-engine", 8092), ("iot-debug", 8083)]: try: req = urllib.request.Request(f"http://localhost:{port}/api/v1/health", method="GET") resp = urllib.request.urlopen(req, timeout=5) check(f"{svc}:{port} 健康检查", resp.status == 200, f"status={resp.status}") except Exception as e: check(f"{svc}:{port} 健康检查", False, str(e)[:60]) # A2: Login print("\n--- A2: 用户登录 ---") try: req = urllib.request.Request( f"{BASE_URL}/auth/login", data=json.dumps(CREDENTIALS).encode(), headers={"Content-Type": "application/json"}, method="POST" ) resp = urllib.request.urlopen(req, timeout=10) login_data = json.loads(resp.read()) access_token = login_data.get("data", {}).get("token", "") refresh_token_val = login_data.get("data", {}).get("refresh_token", "") user_id = login_data.get("data", {}).get("user_id", "") check("登录成功", bool(access_token), f"token={access_token[:20]}...") check("获取 refresh_token", bool(refresh_token_val)) check("获取 user_id", bool(user_id), f"user_id={user_id}") except Exception as e: check("登录", False, str(e)[:80]) access_token = "" refresh_token_val = "" AUTH_HEADER = {"Authorization": f"Bearer {access_token}", "Content-Type": "application/json"} # A3: Token Refresh if refresh_token_val: print("\n--- A3: Token 刷新 ---") try: req = urllib.request.Request( f"{BASE_URL}/auth/refresh", data=json.dumps({"refresh_token": refresh_token_val}).encode(), headers={"Content-Type": "application/json"}, method="POST" ) resp = urllib.request.urlopen(req, timeout=10) refresh_data = json.loads(resp.read()) new_token = refresh_data.get("data", {}).get("token", "") check("Token 刷新成功", bool(new_token), f"new_token={new_token[:20]}...") if new_token: access_token = new_token AUTH_HEADER = {"Authorization": f"Bearer {access_token}", "Content-Type": "application/json"} except Exception as e: check("Token 刷新", False, str(e)[:80]) # A4: Session CRUD print("\n--- A4: 会话管理 ---") session_id = "" try: # Create session req = urllib.request.Request( f"{BASE_URL}/sessions", data=json.dumps({"user_id": user_id, "title": "CDP E2E Test"}).encode(), headers=AUTH_HEADER, method="POST" ) resp = urllib.request.urlopen(req, timeout=10) session_data = json.loads(resp.read()) session_id = session_data.get("data", {}).get("id", "") check("创建会话", bool(session_id), f"session_id={session_id}") # List sessions req = urllib.request.Request(f"{BASE_URL}/sessions?user_id={user_id}", headers=AUTH_HEADER) resp = urllib.request.urlopen(req, timeout=10) sessions = json.loads(resp.read()) check("列出会话", isinstance(sessions.get("data"), list), f"count={len(sessions.get('data', []))}") except Exception as e: check("会话管理", False, str(e)[:80]) # A5: Memory Search print("\n--- A5: 记忆搜索 ---") try: req = urllib.request.Request( f"{BASE_URL}/memory/search", data=json.dumps({"user_id": user_id, "query": "test"}).encode(), headers=AUTH_HEADER, method="POST" ) resp = urllib.request.urlopen(req, timeout=10) mem_data = json.loads(resp.read()) check("记忆搜索API正常", resp.status in [200, 404], f"status={resp.status}") except Exception as e: check("记忆搜索", False, str(e)[:60]) # A6: IoT Devices print("\n--- A6: IoT 设备 ---") try: req = urllib.request.Request(f"{BASE_URL}/iot/devices", headers=AUTH_HEADER) resp = urllib.request.urlopen(req, timeout=10) iot_data = json.loads(resp.read()) devices = iot_data.get("data", {}).get("devices", iot_data.get("data", [])) check("IoT设备列表API正常", resp.status in [200, 404], f"status={resp.status}, devices={len(devices) if isinstance(devices, list) else 'N/A'}") except Exception as e: check("IoT设备列表", False, str(e)[:60]) print(f"\nBackend 测试结果: {passed} 通过, {failed} 失败") # ============================================================ # Part B: Frontend CDP E2E Tests # ============================================================ print("\n" + "=" * 60) print("Part B: Frontend CDP E2E 测试") print("=" * 60) # B1: 创建新的浏览器页面 (about:blank 确保没有 Service Worker 缓存) print("\n--- B1: 创建新页面 ---") try: req = urllib.request.Request(f"{CDP_URL}/json/new?url=about:blank", method="PUT") resp = urllib.request.urlopen(req, timeout=10) page = json.loads(resp.read()) ws_url = page["webSocketDebuggerUrl"] page_id = page.get("id", "") print(f" Page ID: {page_id}") check("创建空白页面", bool(ws_url)) except Exception as e: print(f" ❌ 创建页面失败: {e}") sys.exit(1) ws = create_connection(ws_url, timeout=15) def cdp(method, params=None, msg_id=1): ws.send(json.dumps({"id": msg_id, "method": method, "params": params or {}})) def recv_all(timeout=3): ws.settimeout(timeout) msgs = [] try: while True: msgs.append(ws.recv()) except: pass return msgs # Enable domains cdp("Page.enable", {}, 1) cdp("Network.enable", {}, 2) cdp("Runtime.enable", {}, 3) cdp("Log.enable", {}, 4) time.sleep(0.5) recv_all(0.5) # B2: 导航到前端页面 print("\n--- B2: 导航到前端 ---") cdp("Page.navigate", {"url": FRONTEND_URL + "/"}, 10) time.sleep(4) msgs = recv_all(3) # 检查 console 日志和错误 has_js_error = False console_logs = [] for m in msgs: d = json.loads(m) method = d.get("method", "") params = d.get("params", {}) if method == "Runtime.exceptionThrown": has_js_error = True exc = params.get("exceptionDetails", {}) print(f" [JS ERROR] {exc.get('text', '')}") elif method == "Runtime.consoleAPICalled": args = params.get("args", []) texts = [str(a.get("value", "") or a.get("description", ""))[:120] for a in args] console_logs.append(" ".join(texts)) elif method == "Log.entryAdded": entry = params.get("entry", {}) console_logs.append(f"[{entry.get('level','log')}] {entry.get('text','')[:120]}") check("页面加载无 JS 异常", not has_js_error) # B3: 检查 localStorage(应该为空或没有 test-token-cyrene) print("\n--- B3: localStorage 检查 ---") cdp("Runtime.evaluate", { "expression": "JSON.stringify({token: localStorage.getItem('token'), user_id: localStorage.getItem('user_id'), cyrene_store_version: localStorage.getItem('cyrene_store_version'), allKeys: Object.keys(localStorage)})" }, 20) time.sleep(0.5) msgs = recv_all(1) ls_data = {} for m in msgs: d = json.loads(m) if d.get("id") == 20: val = d.get("result", {}).get("result", {}).get("value", "") ls_data = json.loads(val) if val else {} print(f" localStorage: {ls_data}") has_test_token = ls_data.get("token") == "test-token-cyrene" check("无 test-token-cyrene", not has_test_token, "发现硬编码token!" if has_test_token else "") # B4: 检查 DOM 渲染(登录页面应出现) print("\n--- B4: DOM 检查 ---") cdp("Runtime.evaluate", { "expression": "document.querySelector('input') ? 'has_input' : 'no_input'" }, 30) time.sleep(0.5) msgs = recv_all(1) for m in msgs: d = json.loads(m) if d.get("id") == 30: val = d.get("result", {}).get("result", {}).get("value", "") check("登录表单已渲染", val == "'has_input'", f"got: {val}") # 截图 cdp("Page.captureScreenshot", {"format": "png"}, 31) time.sleep(1) msgs = recv_all(2) for m in msgs: d = json.loads(m) if d.get("id") == 31: img_data = d.get("result", {}).get("data", "") if img_data: import base64 with open("/tmp/cdp_screenshot_before_login.png", "wb") as f: f.write(base64.b64decode(img_data)) print(" 截图已保存到 /tmp/cdp_screenshot_before_login.png") # B5: 通过 API 直接在页面上设置 token(绕过登录UI) print("\n--- B5: 注入 token 并模拟已登录状态 ---") if access_token: cdp("Runtime.evaluate", { "expression": f""" localStorage.setItem('token', '{access_token}'); localStorage.setItem('user_id', '{user_id}'); localStorage.setItem('cyrene_store_version', '1'); 'token_injected' """ }, 40) time.sleep(0.5) recv_all(0.5) # 重新加载页面 cdp("Page.reload", {}, 41) time.sleep(4) msgs = recv_all(3) # 检查网络请求是否使用正确的 token api_calls = [] console_errors = [] for m in msgs: d = json.loads(m) method = d.get("method", "") params = d.get("params", {}) if method == "Network.requestWillBeSent": req = params.get("request", {}) url = req.get("url", "") headers = req.get("headers", {}) auth = headers.get("Authorization", "") or headers.get("authorization", "") if "/api/" in url: api_calls.append(f"{req.get('method','?')} {url[:100]} Auth={auth[:30]}...") elif method == "Runtime.consoleAPICalled": args = params.get("args", []) if params.get("type") == "error": texts = [str(a.get("value", "") or a.get("description", ""))[:150] for a in args] console_errors.append(" ".join(texts)) elif method == "Runtime.exceptionThrown": exc = params.get("exceptionDetails", {}) console_errors.append(f"JS Exception: {exc.get('text', '')}") print(f" API 调用数: {len(api_calls)}") for c in api_calls[:5]: print(f" {c}") check("API 调用使用正确 token", all("test-token-cyrene" not in c for c in api_calls), "仍使用旧token!" if any("test-token-cyrene" in c for c in api_calls) else "") # 检查 WebSocket 连接状态 ws_connected = any("ws_connect" in c.lower() or "WebSocket" in c for c in console_logs) has_auth_error = any("401" in e or "认证" in e or "无效" in e for e in console_errors) check("无认证错误", not has_auth_error, str(console_errors[:2]) if console_errors else "") # 截图 after login cdp("Page.captureScreenshot", {"format": "png"}, 42) time.sleep(1) msgs = recv_all(2) for m in msgs: d = json.loads(m) if d.get("id") == 42: img_data = d.get("result", {}).get("data", "") if img_data: import base64 with open("/tmp/cdp_screenshot_after_login.png", "wb") as f: f.write(base64.b64decode(img_data)) print(" 截图已保存到 /tmp/cdp_screenshot_after_login.png") # B6: DOM 状态检查 - 应该看到侧边栏和聊天界面 print("\n--- B6: 登录后 UI 状态 ---") checks_js = """ (() => { const hasSidebar = !!document.querySelector('aside, [class*="sidebar"], [class*="Sidebar"]'); const hasChatInput = !!document.querySelector('textarea, [contenteditable="true"], input[type="text"]'); const hasHeader = !!document.querySelector('header, [class*="header"], [class*="Header"]'); const bodyText = document.body.innerText.substring(0, 200); return JSON.stringify({hasSidebar, hasChatInput, hasHeader, bodyText}); })() """ cdp("Runtime.evaluate", {"expression": checks_js}, 50) time.sleep(0.5) msgs = recv_all(1) for m in msgs: d = json.loads(m) if d.get("id") == 50: val = d.get("result", {}).get("result", {}).get("value", "") try: ui = json.loads(val) print(f" UI状态: sidebar={ui.get('hasSidebar')}, chatInput={ui.get('hasChatInput')}, header={ui.get('hasHeader')}") print(f" Body文本: {ui.get('bodyText', '')[:100]}") check("侧边栏已渲染", ui.get("hasSidebar", False)) check("聊天输入框存在", ui.get("hasChatInput", False)) except: pass ws.close() # ============================================================ # Summary # ============================================================ print("\n" + "=" * 60) print(f"测试完成: {passed} 通过, {failed} 失败, 总计 {passed+failed}") print("=" * 60)