Files
qihuaiflow/tests/channel_regression_api.py
T
qyyfw 78ff261b5a 初始提交: Qihu-AIFlow v2.0.1 完整项目
- 前端 React+TypeScript (源码+构建产物)
- 后端 Python FastAPI
- 数据库迁移 001-015
- Docker部署配置
- 微信ClawBot API频道绑定
- 项目文档 README.md
2026-07-02 14:40:46 +08:00

437 lines
17 KiB
Python

#!/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()