#!/usr/bin/env python3 """ 频道模块 - API数据完整性回归验证(改进规范版) 覆盖:字段类型验证、空数据处理、错误码验证 """ import requests import json from datetime import datetime BASE = "http://localhost:18080" API = f"{BASE}/app/api/channels" passed = 0 failed = 0 results_log = [] def login(username="admin", password="admin123"): r = requests.post(f"{BASE}/app/api/auth/login", json={"username": username, "password": password}) if r.status_code == 200: return r.json().get("data", {}).get("access_token", "") return "" def hdr(token): return {"Authorization": f"Bearer {token}"} def check_field(obj, field, expect_type, path=""): full = f"{path}.{field}" if path else field if field not in obj: return False, f"缺少字段 '{full}'" val = obj[field] if expect_type == "int" and not (isinstance(val, int) and not isinstance(val, bool)): return False, f"'{full}' 期望int, 实际{type(val).__name__}={val}" if expect_type == "float_or_int" and not (isinstance(val, (int, float)) and not isinstance(val, bool)): return False, f"'{full}' 期望number, 实际{type(val).__name__}={val}" if expect_type == "str_or_none" and not (val is None or isinstance(val, str)): return False, f"'{full}' 期望str|None, 实际{type(val).__name__}={val}" if expect_type == "bool" and not isinstance(val, bool): return False, f"'{full}' 期望bool, 实际{type(val).__name__}={val}" if expect_type == "dict_or_none" and not (val is None or isinstance(val, dict)): return False, f"'{full}' 期望dict|None, 实际{type(val).__name__}={val}" if expect_type == "list" and not isinstance(val, list): return False, f"'{full}' 期望list, 实际{type(val).__name__}={val}" if expect_type == "str" and not isinstance(val, str): return False, f"'{full}' 期望str, 实际{type(val).__name__}={val}" return True, "" def record(name, method, path, ok, detail): global passed, failed s = "PASS" if ok else "FAIL" print(f"[{s}] {name}") print(f" {method} {path} | {detail}") results_log.append({"name": name, "method": method, "path": path, "passed": ok, "detail": detail}) if ok: passed += 1 else: failed += 1 # ===== 各测试用例 ===== def t1_platform_configs(): """平台凭证配置 - 字段完整性""" tok = login() r = requests.get(f"{API}/platforms/config", headers=hdr(tok)) if r.status_code != 200: return record("t1", "GET", "/platforms/config", False, f"HTTP {r.status_code}") data = r.json() if data.get("code") != 0: return record("t1", "GET", "/platforms/config", False, f"code={data.get('code')}") platforms = data.get("data", {}) expected = ["feishu", "wechat", "dingtalk", "whatsapp", "telegram", "slack"] ok = True details = [] for p in expected: if p not in platforms: ok = False; details.append(f"缺少平台 {p}") continue cfg = platforms[p] for f in ["name", "description", "fields", "doc_url"]: f_ok, f_d = check_field(cfg, f, "str" if f != "fields" else "list", p) if not f_ok: ok = False; details.append(f_d) for i, field in enumerate(cfg.get("fields", [])): for ff in ["key", "label", "type", "placeholder"]: f_ok, f_d = check_field(field, ff, "str", f"{p}.fields[{i}]") if not f_ok: ok = False; details.append(f_d) detail = "6个平台配置完整" if ok else "; ".join(details) record("t1", "GET", "/platforms/config", ok, detail) def t2_list_empty(): """空频道列表场景""" tok = login() # 先清空所有频道 r0 = requests.get(API + "/", headers=hdr(tok)) if r0.status_code == 200: for ch in r0.json().get("data", []): requests.delete(f"{API}/{ch['id']}", headers=hdr(tok)) r = requests.get(API + "/", headers=hdr(tok)) if r.status_code != 200: return record("t2", "GET", "/", False, f"HTTP {r.status_code}") items = r.json().get("data", []) if not isinstance(items, list): record("t2", "GET", "/", False, f"data不是list: {type(items)}") elif len(items) != 0: record("t2", "GET", "/", False, f"清理后仍有{len(items)}条记录") else: record("t2", "GET", "/", True, "空列表返回 [] 正确") def t3_create_all_platforms(): """创建6个平台频道""" tok = login() platforms = [ ("feishu", {"app_id": "t1", "app_secret": "s1"}), ("wechat", {"app_id": "w1", "app_secret": "s2", "token": "t1"}), ("dingtalk", {"app_key": "d1", "app_secret": "s3"}), ("whatsapp", {"phone_number_id": "123", "access_token": "a1"}), ("telegram", {"bot_token": "b1"}), ("slack", {"bot_token": "b2", "signing_secret": "s4"}), ] created = [] ok = True details = [] for p_name, cred in platforms: r = requests.post(API + "/", json={ "name": f"测试-{p_name}", "platform": p_name, "description": f"{p_name}频道", "credential": cred }, headers=hdr(tok)) if r.status_code != 200: ok = False; details.append(f"{p_name}: HTTP {r.status_code}") continue data = r.json() if data.get("code") != 0: ok = False; details.append(f"{p_name}: code={data.get('code')}") continue ch = data.get("data", {}) # 字段检查 for f in ["id", "name", "platform", "status", "is_active", "credential_masked"]: f_ok, f_d = check_field(ch, f, "str" if f != "is_active" else "bool", p_name) if not f_ok: ok = False; details.append(f_d) if ch.get("platform") != p_name: ok = False; details.append(f"{p_name}: platform不匹配={ch.get('platform')}") created.append(ch["id"]) detail = f"{len(created)}/6创建成功" + ("; "+"; ".join(details[-3:]) if details else "") record("t3", "POST", "/", ok, detail) return created def t4_channel_detail(): """频道详情字段完整性""" tok = login() r0 = requests.get(API + "/", headers=hdr(tok)) chs = r0.json().get("data", []) if not chs: return record("t4", "GET", "/{id}", False, "无可用频道") ch_id = chs[0]["id"] r = requests.get(f"{API}/{ch_id}", headers=hdr(tok)) if r.status_code != 200: return record("t4", "GET", f"/{ch_id}", False, f"HTTP {r.status_code}") ch = r.json().get("data", {}) ok = True; details = [] checks = [ ("id","str"),("name","str"),("platform","str"), ("status","str"),("is_active","bool"), ("description","str_or_none"),("webhook_url","str_or_none"), ("role","str_or_none"),("member_count","int"),("message_count","int"), ("credential_masked","dict_or_none") ] for f, t in checks: f_ok, f_d = check_field(ch, f, t) if not f_ok: ok = False; details.append(f_d) detail = "所有字段类型正确" if ok else "; ".join(details) record("t4", "GET", f"/{ch_id}", ok, detail) def t5_update_settings(): """更新频道AI配置""" tok = login() r0 = requests.get(API + "/", headers=hdr(tok)) chs = r0.json().get("data", []) if not chs: return record("t5", "PUT", "/{id}", False, "无可用频道") ch_id = chs[0]["id"] r = requests.put(f"{API}/{ch_id}", json={ "name": "已更新名称", "description": "已更新描述", "settings": { "agent_id": "agent-001", "knowledge_base_id": "kb-001", "skill_ids": ["skill-1"], "welcome_message": "欢迎" } }, headers=hdr(tok)) if r.status_code != 200: return record("t5", "PUT", f"/{ch_id}", False, f"HTTP {r.status_code}") data = r.json() ch = data.get("data", {}) ok = True; details = [] if ch.get("name") != "已更新名称": ok = False; details.append(f"name未更新={ch.get('name')}") if ch.get("default_agent_id") != "agent-001": details.append(f"agent_id同步={ch.get('default_agent_id')}(期望agent-001)") if ch.get("knowledge_base_id") != "kb-001": details.append(f"kb_id同步={ch.get('knowledge_base_id')}(期望kb-001)") detail = "更新成功" if ok else "; ".join(details) record("t5", "PUT", f"/{ch_id}", ok, detail) def t6_connect_disconnect(): """连接/断开频道""" tok = login() r0 = requests.get(API + "/", headers=hdr(tok)) chs = r0.json().get("data", []) if not chs: return record("t6", "POST", "/{id}/connect", False, "无可用频道") ch_id = chs[0]["id"] # 连接 r = requests.post(f"{API}/{ch_id}/connect", headers=hdr(tok)) if r.status_code not in (200, 400): return record("t6", "POST", f"/{ch_id}/connect", False, f"HTTP {r.status_code}") data = r.json() ok = True; details = [] if r.json().get("code") == 0: conn = data.get("data", {}) for f in ["connected", "platform", "status", "message"]: if f not in conn: ok = False; details.append(f"缺少字段{f}") details.append("连接成功") # 断开 r2 = requests.post(f"{API}/{ch_id}/disconnect", headers=hdr(tok)) if r2.status_code == 200 and r2.json().get("code") == 0: details.append("断开成功") else: ok = False; details.append(f"断开失败: HTTP {r2.status_code}") # 重新连接 r3 = requests.post(f"{API}/{ch_id}/connect", headers=hdr(tok)) if r3.status_code == 200 and r3.json().get("code") == 0: details.append("重新连接成功") else: details.append(f"重新连接: HTTP {r3.status_code}") else: details.append(f"连接失败: code={data.get('code')}, msg={data.get('message')}") detail = "; ".join(details) record("t6", "POST", f"/{ch_id}/connect|disconnect", ok, detail) def t7_webhook_message(): """Webhook消息收发""" tok = login() r0 = requests.get(API + "/", headers=hdr(tok)) chs = r0.json().get("data", []) connected = [c for c in chs if c.get("status") == "connected"] if not connected: return record("t7", "POST", "/{id}/webhook", False, "无已连接频道") ch_id = connected[0]["id"] r = requests.post(f"{API}/{ch_id}/webhook", json={ "from_user": "wx_user_001", "content": "你好测试消息", "message_type": "text" }, headers=hdr(tok)) if r.status_code not in (200, 400): return record("t7", "POST", f"/{ch_id}/webhook", False, f"HTTP {r.status_code}") data = r.json() if data.get("code") != 0: return record("t7", "POST", f"/{ch_id}/webhook", False, f"code={data.get('code')}, msg={data.get('message')}") wd = data.get("data", {}) ok = True; details = [] # received检查 if "received" in wd: rc = wd["received"] for f in ["id", "from_user", "content", "message_type", "created_at"]: if f not in rc: ok = False; details.append(f"received缺{f}") else: ok = False; details.append("缺received") # reply检查 if "reply" in wd: rp = wd["reply"] for f in ["id", "content", "role", "created_at"]: if f not in rp: ok = False; details.append(f"reply缺{f}") if rp.get("role") != "assistant": ok = False; details.append(f"role={rp.get('role')}") else: ok = False; details.append("缺reply") detail = "received+reply字段完整" if ok else "; ".join(details) record("t7", "POST", f"/{ch_id}/webhook", ok, detail) def t8_messages_list(): """消息列表字段完整性""" tok = login() r0 = requests.get(API + "/", headers=hdr(tok)) chs = r0.json().get("data", []) connected = [c for c in chs if c.get("status") == "connected"] if not connected: return record("t8", "GET", "/{id}/messages", False, "无已连接频道") ch_id = connected[0]["id"] r = requests.get(f"{API}/{ch_id}/messages", params={"limit": 10}, headers=hdr(tok)) if r.status_code != 200: return record("t8", "GET", f"/{ch_id}/messages", False, f"HTTP {r.status_code}") msgs = r.json().get("data", []) if not isinstance(msgs, list): return record("t8", "GET", f"/{ch_id}/messages", False, f"data不是list: {type(msgs)}") ok = True; details = [f"共{len(msgs)}条"] for i, m in enumerate(msgs): for f in ["id", "channel_id", "user_id", "content", "role"]: f_ok, f_d = check_field(m, f, "str", f"msgs[{i}]") if not f_ok: ok = False; details.append(f_d) detail = "; ".join(details) record("t8", "GET", f"/{ch_id}/messages", ok, detail) def t9_send_message(): """发送消息字段完整性""" tok = login() r0 = requests.get(API + "/", headers=hdr(tok)) chs = r0.json().get("data", []) connected = [c for c in chs if c.get("status") == "connected"] if not connected: return record("t9", "POST", "/{id}/messages", False, "无已连接频道") ch_id = connected[0]["id"] r = requests.post(f"{API}/{ch_id}/messages", json={"content": "测试消息"}, headers=hdr(tok)) if r.status_code != 200: return record("t9", "POST", f"/{ch_id}/messages", False, f"HTTP {r.status_code}, body={r.text[:100]}") data = r.json() if data.get("code") != 0: return record("t9", "POST", f"/{ch_id}/messages", False, f"code={data.get('code')}") wd = data.get("data", {}) ok = True; details = [] for section in ["message", "reply", "webhook_response"]: if section not in wd: ok = False; details.append(f"缺{section}") elif section == "message": for f in ["id", "channel_id", "user_id", "content"]: if f not in wd[section]: ok = False; details.append(f"message缺{f}") elif section == "reply": for f in ["id", "content", "role"]: if f not in wd[section]: ok = False; details.append(f"reply缺{f}") elif section == "webhook_response": for f in ["status", "platform", "message"]: if f not in wd[section]: ok = False; details.append(f"webhook_response缺{f}") detail = "message/reply/webhook_response完整" if ok else "; ".join(details) record("t9", "POST", f"/{ch_id}/messages", ok, detail) def t10_members(): """频道成员管理""" tok = login() r0 = requests.get(API + "/", headers=hdr(tok)) chs = r0.json().get("data", []) if not chs: return record("t10", "GET", "/{id}/members", False, "无可用频道") ch_id = chs[0]["id"] r = requests.get(f"{API}/{ch_id}/members", headers=hdr(tok)) if r.status_code != 200: return record("t10", "GET", f"/{ch_id}/members", False, f"HTTP {r.status_code}") members = r.json().get("data", []) ok = True; details = [f"共{len(members)}名成员"] for i, m in enumerate(members): for f in ["id", "user_id", "username", "role"]: f_ok, f_d = check_field(m, f, "str", f"members[{i}]") if not f_ok: ok = False; details.append(f_d) detail = "; ".join(details) record("t10", "GET", f"/{ch_id}/members", ok, detail) def t11_delete_and_cleanup(): """删除频道""" tok = login() r0 = requests.get(API + "/", headers=hdr(tok)) chs = r0.json().get("data", []) if not chs: return record("t11", "DELETE", "/{id}", True, "无频道需删除,跳过") ch_id = chs[0]["id"] r = requests.delete(f"{API}/{ch_id}", headers=hdr(tok)) ok = r.status_code == 200 and r.json().get("code") == 0 detail = "删除成功" if ok else f"HTTP {r.status_code}" record("t11", "DELETE", f"/{ch_id}", ok, detail) # ===== 主函数 ===== def main(): global passed, failed print("=" * 70) print(f"频道模块 API数据完整性回归 - {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}") print("=" * 70) t1_platform_configs() t2_list_empty() created = t3_create_all_platforms() t4_channel_detail() t5_update_settings() t6_connect_disconnect() t7_webhook_message() t8_messages_list() t9_send_message() t10_members() t11_delete_and_cleanup() print() print("=" * 70) total = passed + failed rate = passed / total * 100 if total > 0 else 0 print(f"汇总: {passed}/{total} PASS, {failed} FAIL, {rate:.1f}%") print("=" * 70) # JSON report report = { "test_time": datetime.now().isoformat(), "module": "频道模块-API数据完整性", "total": total, "passed": passed, "failed": failed, "pass_rate": f"{rate:.1f}%", "results": results_log } print("\n=== JSON_REPORT ===") print(json.dumps(report, ensure_ascii=False, indent=2)) print("=== END_JSON_REPORT ===") if __name__ == "__main__": main()