本次提交包含大量代码优化、功能新增与服务端配置更新: 1. 修复分析报告统计数据,调整CMake策略设置 2. 优化APP权限配置、编辑器与聊天界面组件 3. 更新依赖库版本与pubspec配置 4. 新增文件传输服务端、信令服务器相关配置与脚本 5. 完善用户注销功能与数据库迁移脚本 6. 优化多处动画效果、代码风格与日志输出 7. 新增多种调试与部署脚本,修复已知BUG
550 lines
19 KiB
Python
550 lines
19 KiB
Python
#!/usr/bin/env python3
|
||
"""
|
||
============================================================
|
||
闲言APP — 用户中心API自动化测试脚本
|
||
创建时间: 2026-05-10
|
||
作用: 模拟用户注册/登录/各场景行为,最后注销测试用户
|
||
============================================================
|
||
"""
|
||
|
||
import base64
|
||
import hashlib
|
||
import hmac
|
||
import json
|
||
import os
|
||
import random
|
||
import string
|
||
import sys
|
||
import time
|
||
|
||
import requests
|
||
|
||
BASE_URL = "https://tools.wktyl.com"
|
||
RECEIPT_SECRET = "Xy7kP9mL2qR4wS8v"
|
||
TIMEOUT = 15
|
||
|
||
session = requests.Session()
|
||
session.headers.update({
|
||
"Accept": "application/json",
|
||
"Content-Type": "application/x-www-form-urlencoded",
|
||
})
|
||
|
||
|
||
def generate_receipt(action: str, payload: str) -> dict:
|
||
ts = int(time.time())
|
||
nonce = os.urandom(4).hex()
|
||
payload_digest = hashlib.sha256(payload.encode()).hexdigest()[:16]
|
||
data = {
|
||
"action": action,
|
||
"payload": payload_digest,
|
||
"ts": ts,
|
||
"nonce": nonce,
|
||
}
|
||
receipt = base64.b64encode(json.dumps(data).encode()).decode()
|
||
sig = hmac.new(
|
||
RECEIPT_SECRET.encode(),
|
||
receipt.encode(),
|
||
hashlib.sha256,
|
||
).hexdigest()
|
||
return {"receipt": receipt, "sig": sig}
|
||
|
||
|
||
def random_string(length: int = 8) -> str:
|
||
return "".join(random.choices(string.ascii_lowercase + string.digits, k=length))
|
||
|
||
|
||
def log_test(name: str, success: bool, detail: str = ""):
|
||
status = "✅ PASS" if success else "❌ FAIL"
|
||
msg = f"{status} | {name}"
|
||
if detail:
|
||
msg += f" — {detail}"
|
||
print(msg)
|
||
|
||
|
||
def api_post(path: str, data: dict = None, token: str = None) -> dict:
|
||
headers = {}
|
||
if token:
|
||
headers["token"] = token
|
||
resp = session.post(f"{BASE_URL}{path}", data=data, headers=headers, timeout=TIMEOUT)
|
||
return resp.json()
|
||
|
||
|
||
def api_get(path: str, params: dict = None, token: str = None) -> dict:
|
||
headers = {}
|
||
if token:
|
||
headers["token"] = token
|
||
resp = session.get(f"{BASE_URL}{path}", params=params, headers=headers, timeout=TIMEOUT)
|
||
return resp.json()
|
||
|
||
|
||
# ============================================================
|
||
# 测试场景
|
||
# ============================================================
|
||
|
||
def test_register_and_login():
|
||
"""场景1: 注册新用户 → 密码登录 → 获取用户信息"""
|
||
username = f"test_{random_string()}"
|
||
password = f"Test@{random_string(12)}"
|
||
email = f"{username}@testmail.com"
|
||
|
||
print(f"\n{'='*60}")
|
||
print(f"📋 场景1: 注册+登录 (用户: {username})")
|
||
print(f"{'='*60}")
|
||
|
||
# 1.1 注册
|
||
receipt = generate_receipt("register", email)
|
||
reg_data = {
|
||
"username": username,
|
||
"password": password,
|
||
"email": email,
|
||
"receipt": receipt["receipt"],
|
||
"sig": receipt["sig"],
|
||
}
|
||
reg_resp = api_post("/api/user_security/register", reg_data)
|
||
reg_ok = reg_resp.get("code") == 1
|
||
log_test("1.1 用户注册", reg_ok, reg_resp.get("msg", ""))
|
||
|
||
if not reg_ok:
|
||
print(" ⚠️ 注册失败,尝试直接登录(用户可能已存在)")
|
||
|
||
# 1.2 密码登录
|
||
login_data = {
|
||
"account": username,
|
||
"password": password,
|
||
"device_name": "TestScript",
|
||
"device_model": "Python3",
|
||
"platform": "test",
|
||
"app_name": "xianyan_test",
|
||
"device_id": f"test_{random_string(8)}",
|
||
}
|
||
login_resp = api_post("/api/user_security/login", login_data)
|
||
login_ok = login_resp.get("code") == 1
|
||
log_test("1.2 密码登录", login_ok, login_resp.get("msg", ""))
|
||
|
||
token = None
|
||
user_id = None
|
||
if login_ok:
|
||
userinfo = login_resp.get("data", {}).get("userinfo", {})
|
||
token = userinfo.get("token", "")
|
||
user_id = userinfo.get("id", "")
|
||
print(f" 👤 用户ID: {user_id}, Token: {token[:20]}...")
|
||
|
||
# 1.3 获取用户信息
|
||
if token:
|
||
info_resp = api_get("/api/user_center/index", token=token)
|
||
info_ok = info_resp.get("code") == 1
|
||
log_test("1.3 获取用户信息", info_ok)
|
||
if info_ok:
|
||
user_data = info_resp.get("data", {})
|
||
print(f" 📊 积分: {user_data.get('score', 0)}, 签到天数: {user_data.get('signin_days', 0)}")
|
||
else:
|
||
log_test("1.3 获取用户信息", False, "无Token")
|
||
|
||
return token, user_id, username, password, email
|
||
|
||
|
||
def test_receipt_login(username: str, email: str):
|
||
"""场景2: 回执登录"""
|
||
print(f"\n{'='*60}")
|
||
print(f"📋 场景2: 回执登录")
|
||
print(f"{'='*60}")
|
||
|
||
receipt = generate_receipt("receipt_login", username)
|
||
login_data = {
|
||
"account": username,
|
||
"receipt": receipt["receipt"],
|
||
"sig": receipt["sig"],
|
||
}
|
||
resp = api_post("/api/user_security/receiptLogin", login_data)
|
||
ok = resp.get("code") == 1
|
||
log_test("2.1 回执登录", ok, resp.get("msg", ""))
|
||
|
||
if ok:
|
||
token = resp.get("data", {}).get("userinfo", {}).get("token", "")
|
||
return token
|
||
return None
|
||
|
||
|
||
def test_token_login(token: str):
|
||
"""场景3: Token登录"""
|
||
print(f"\n{'='*60}")
|
||
print(f"📋 场景3: Token登录")
|
||
print(f"{'='*60}")
|
||
|
||
resp = api_post("/api/user_security/tokenLogin", {"token": token})
|
||
ok = resp.get("code") == 1
|
||
log_test("3.1 Token登录", ok, resp.get("msg", ""))
|
||
|
||
if ok:
|
||
new_token = resp.get("data", {}).get("userinfo", {}).get("token", token)
|
||
return new_token
|
||
return token
|
||
|
||
|
||
def test_user_center(token: str):
|
||
"""场景4: 用户中心功能(签到/收藏/笔记/点赞/互动/面板/设备)"""
|
||
print(f"\n{'='*60}")
|
||
print(f"📋 场景4: 用户中心功能")
|
||
print(f"{'='*60}")
|
||
|
||
# 4.1 签到
|
||
signin_resp = api_post("/api/user_center/signin", token=token)
|
||
signin_ok = signin_resp.get("code") == 1 or "已签到" in signin_resp.get("msg", "")
|
||
log_test("4.1 每日签到", signin_ok, signin_resp.get("msg", ""))
|
||
|
||
# 4.2 签到日历
|
||
cal_resp = api_get("/api/user_center/signin_calendar", token=token)
|
||
log_test("4.2 签到日历", cal_resp.get("code") == 1)
|
||
|
||
# 4.3 收藏-添加
|
||
fav_add_resp = api_post("/api/user_center/favorite", {
|
||
"action": "add",
|
||
"target_id": 1,
|
||
"target_type": "article",
|
||
}, token=token)
|
||
log_test("4.3 收藏-添加", fav_add_resp.get("code") == 1, fav_add_resp.get("msg", ""))
|
||
|
||
# 4.4 收藏-检查
|
||
fav_check_resp = api_post("/api/user_center/favorite", {
|
||
"action": "check",
|
||
"target_id": 1,
|
||
"target_type": "article",
|
||
}, token=token)
|
||
log_test("4.4 收藏-检查", fav_check_resp.get("code") == 1)
|
||
|
||
# 4.5 收藏-列表
|
||
fav_list_resp = api_post("/api/user_center/favorite", {
|
||
"action": "list",
|
||
"page": 1,
|
||
"limit": 10,
|
||
}, token=token)
|
||
log_test("4.5 收藏-列表", fav_list_resp.get("code") == 1)
|
||
|
||
# 4.6 收藏-移除
|
||
fav_rm_resp = api_post("/api/user_center/favorite", {
|
||
"action": "remove",
|
||
"target_id": 1,
|
||
"target_type": "article",
|
||
}, token=token)
|
||
log_test("4.6 收藏-移除", fav_rm_resp.get("code") == 1, fav_rm_resp.get("msg", ""))
|
||
|
||
# 4.7 笔记-添加
|
||
note_add_resp = api_post("/api/user_center/note", {
|
||
"action": "add",
|
||
"title": "测试笔记",
|
||
"content": "这是自动化测试创建的笔记",
|
||
"note_type": "note",
|
||
"category": "test",
|
||
}, token=token)
|
||
note_id = None
|
||
if note_add_resp.get("code") == 1:
|
||
note_id = note_add_resp.get("data", {}).get("id")
|
||
log_test("4.7 笔记-添加", note_add_resp.get("code") == 1, note_add_resp.get("msg", ""))
|
||
|
||
# 4.8 笔记-列表
|
||
note_list_resp = api_get("/api/user_center/note", {
|
||
"action": "list",
|
||
"page": 1,
|
||
"limit": 10,
|
||
}, token=token)
|
||
log_test("4.8 笔记-列表", note_list_resp.get("code") == 1)
|
||
|
||
# 4.9 笔记-删除
|
||
if note_id:
|
||
note_del_resp = api_post("/api/user_center/note", {
|
||
"action": "delete",
|
||
"id": note_id,
|
||
}, token=token)
|
||
log_test("4.9 笔记-删除", note_del_resp.get("code") == 1)
|
||
|
||
# 4.10 点赞
|
||
like_resp = api_post("/api/user_center/like", {
|
||
"action": "add",
|
||
"target_id": 1,
|
||
"target_type": "article",
|
||
}, token=token)
|
||
log_test("4.10 点赞", like_resp.get("code") == 1, like_resp.get("msg", ""))
|
||
|
||
# 4.11 互动-记录
|
||
interaction_resp = api_post("/api/user_center/interaction", {
|
||
"action": "view",
|
||
"target_id": 1,
|
||
"target_type": "article",
|
||
}, token=token)
|
||
log_test("4.11 互动-记录", interaction_resp.get("code") == 1)
|
||
|
||
# 4.12 数据面板
|
||
dashboard_resp = api_get("/api/user_center/dashboard", token=token)
|
||
log_test("4.12 数据面板", dashboard_resp.get("code") == 1)
|
||
if dashboard_resp.get("code") == 1:
|
||
d = dashboard_resp.get("data", {})
|
||
print(f" 📊 面板: 积分={d.get('score',0)}, 签到={d.get('signin_days',0)}, "
|
||
f"收藏={d.get('favorite_count',0)}, 笔记={d.get('note_count',0)}, "
|
||
f"评论={d.get('comment_count',0)}, 浏览={d.get('view_count',0)}, "
|
||
f"稍后读={d.get('readlater_count',0)}")
|
||
|
||
# 4.13 热力图
|
||
heatmap_resp = api_get("/api/user_center/heatmap", token=token)
|
||
log_test("4.13 热力图", heatmap_resp.get("code") == 1)
|
||
|
||
# 4.14 金币记录
|
||
coin_resp = api_get("/api/user_center/coin", {"page": 1, "limit": 5}, token=token)
|
||
log_test("4.14 金币记录", coin_resp.get("code") == 1)
|
||
|
||
# 4.15 学习统计
|
||
stats_resp = api_get("/api/user_center/stats", {"type": "overview", "period": "month"}, token=token)
|
||
log_test("4.15 学习统计", stats_resp.get("code") == 1)
|
||
|
||
# 4.16 设备-注册
|
||
device_resp = api_post("/api/user_center/registerDevice", {
|
||
"device_id": f"test_device_{random_string(8)}",
|
||
"device_name": "TestDevice",
|
||
"device_model": "Python3-Script",
|
||
"platform": "test",
|
||
"app_name": "xianyan_test",
|
||
}, token=token)
|
||
log_test("4.16 设备-注册", device_resp.get("code") == 1, device_resp.get("msg", ""))
|
||
|
||
# 4.17 设备-列表
|
||
devices_resp = api_post("/api/user_center/devices", {"action": "list"}, token=token)
|
||
log_test("4.17 设备-列表", devices_resp.get("code") == 1)
|
||
|
||
|
||
def test_profile_update(token: str):
|
||
"""场景5: 修改个人信息"""
|
||
print(f"\n{'='*60}")
|
||
print(f"📋 场景5: 修改个人信息")
|
||
print(f"{'='*60}")
|
||
|
||
# 5.1 修改昵称
|
||
profile_resp = api_post("/api/user_center/profile", {
|
||
"nickname": f"测试用户_{random_string(4)}",
|
||
}, token=token)
|
||
log_test("5.1 修改昵称", profile_resp.get("code") == 1, profile_resp.get("msg", ""))
|
||
|
||
# 5.2 修改签名
|
||
bio_resp = api_post("/api/user_center/profile", {
|
||
"bio": "这是自动化测试修改的签名",
|
||
}, token=token)
|
||
log_test("5.2 修改签名", bio_resp.get("code") == 1)
|
||
|
||
# 5.3 修改邮箱 (回执验证)
|
||
new_email = f"test_{random_string()}@testmail.com"
|
||
receipt = generate_receipt("changeemail", new_email)
|
||
email_resp = api_post("/api/user_security/changeemail", {
|
||
"email": new_email,
|
||
"receipt": receipt["receipt"],
|
||
"sig": receipt["sig"],
|
||
}, token=token)
|
||
log_test("5.3 修改邮箱", email_resp.get("code") == 1, email_resp.get("msg", ""))
|
||
|
||
|
||
def test_password_change(token: str, user_id: str, username: str, old_password: str):
|
||
"""场景6: 修改密码"""
|
||
print(f"\n{'='*60}")
|
||
print(f"📋 场景6: 修改密码")
|
||
print(f"{'='*60}")
|
||
|
||
new_password = f"NewTest@{random_string(12)}"
|
||
|
||
# 先尝试不带回执(测试用户可能不需要)
|
||
pwd_resp = api_post("/api/user_security/changepwd", {
|
||
"oldpassword": old_password,
|
||
"newpassword": new_password,
|
||
}, token=token)
|
||
|
||
if pwd_resp.get("code") != 1:
|
||
# 尝试用username作为回执payload
|
||
receipt = generate_receipt("changepwd", username)
|
||
pwd_resp = api_post("/api/user_security/changepwd", {
|
||
"oldpassword": old_password,
|
||
"newpassword": new_password,
|
||
"receipt": receipt["receipt"],
|
||
"sig": receipt["sig"],
|
||
}, token=token)
|
||
|
||
if pwd_resp.get("code") != 1:
|
||
# 尝试用user_id作为回执payload
|
||
receipt = generate_receipt("changepwd", user_id)
|
||
pwd_resp = api_post("/api/user_security/changepwd", {
|
||
"oldpassword": old_password,
|
||
"newpassword": new_password,
|
||
"receipt": receipt["receipt"],
|
||
"sig": receipt["sig"],
|
||
}, token=token)
|
||
|
||
log_test("6.1 修改密码", pwd_resp.get("code") == 1, pwd_resp.get("msg", ""))
|
||
|
||
return new_password if pwd_resp.get("code") == 1 else old_password
|
||
|
||
|
||
def test_qrcode_flow():
|
||
"""场景7: 二维码登录流程"""
|
||
print(f"\n{'='*60}")
|
||
print(f"📋 场景7: 二维码登录流程")
|
||
print(f"{'='*60}")
|
||
|
||
# 7.1 生成二维码
|
||
qr_resp = api_get("/api/user_security/qrcodeGenerate")
|
||
qr_ok = qr_resp.get("code") == 1
|
||
log_test("7.1 生成二维码", qr_ok, qr_resp.get("msg", ""))
|
||
|
||
if qr_ok:
|
||
qr_data = qr_resp.get("data", {})
|
||
qr_code = qr_data.get("code", "")
|
||
print(f" 📱 二维码Code: {qr_code[:20]}..., 有效期: {qr_data.get('expire_seconds', 0)}s")
|
||
|
||
# 7.2 轮询二维码状态
|
||
poll_resp = api_get("/api/user_security/qrcodePoll", {"code": qr_code})
|
||
poll_ok = poll_resp.get("code") == 1 or poll_resp.get("data", {}).get("status") in ["pending", "expired"]
|
||
log_test("7.2 轮询二维码", poll_ok, f"状态: {poll_resp.get('data', {}).get('status', 'unknown')}")
|
||
|
||
# 7.3 取消二维码
|
||
cancel_resp = api_post("/api/user_security/qrcodeCancel", {"code": qr_code})
|
||
log_test("7.3 取消二维码", cancel_resp.get("code") == 1, cancel_resp.get("msg", ""))
|
||
|
||
|
||
def test_delete_account(token: str, user_id: str):
|
||
"""场景8: 账号注销(申请注销)"""
|
||
print(f"\n{'='*60}")
|
||
print(f"📋 场景8: 账号注销(申请注销)")
|
||
print(f"{'='*60}")
|
||
|
||
# 使用文档定义的正确接口路径
|
||
receipt = generate_receipt("delete_account", user_id)
|
||
try:
|
||
resp = session.post(
|
||
f"{BASE_URL}/api/user_security/requestDeletion",
|
||
data={
|
||
"reason": "自动化测试注销",
|
||
"receipt": receipt["receipt"],
|
||
"sig": receipt["sig"],
|
||
},
|
||
headers={"token": token} if token else {},
|
||
timeout=TIMEOUT,
|
||
)
|
||
|
||
if resp.status_code == 404 or not resp.text.strip():
|
||
log_test("8.1 申请账号注销", False, "接口未部署(404/空响应)")
|
||
print(" ℹ️ requestDeletion接口尚未在服务器端部署")
|
||
print(" ℹ️ 管理端注销审核接口: /admin.php/userdeletion")
|
||
return
|
||
|
||
del_resp = resp.json()
|
||
code = del_resp.get("code", -1)
|
||
msg = del_resp.get("msg", "")
|
||
log_test("8.1 申请账号注销", code == 1, f"code={code}, msg={msg}")
|
||
|
||
if code == 1:
|
||
data = del_resp.get("data", {})
|
||
print(f" 📋 注销记录ID: {data.get('id', 'N/A')}")
|
||
print(f" 📋 状态: {data.get('status_text', 'N/A')}")
|
||
print(f" 📋 倒计时: {data.get('countdown', 'N/A')}")
|
||
|
||
# 查询注销状态
|
||
try:
|
||
status_resp = api_get("/api/user_security/deletionStatus", token=token)
|
||
if status_resp.get("code") == 1:
|
||
status_data = status_resp.get("data", {})
|
||
log_test("8.2 查询注销状态", True, f"pending={status_data.get('has_pending')}")
|
||
else:
|
||
log_test("8.2 查询注销状态", False, "接口未部署")
|
||
except Exception:
|
||
log_test("8.2 查询注销状态", False, "接口未部署")
|
||
|
||
# 取消注销
|
||
try:
|
||
cancel_resp = api_post("/api/user_security/cancelDeletion", token=token)
|
||
log_test("8.3 取消注销", cancel_resp.get("code") == 1, cancel_resp.get("msg", ""))
|
||
except Exception:
|
||
log_test("8.3 取消注销", False, "接口未部署")
|
||
elif "已存在" in msg:
|
||
log_test("8.1 申请账号注销", True, f"已有注销申请: {msg}")
|
||
else:
|
||
print(f" ⚠️ 注销申请失败: {msg}")
|
||
except Exception as e:
|
||
log_test("8.1 申请账号注销", False, f"请求异常: {e}")
|
||
print(" ℹ️ requestDeletion接口可能尚未部署")
|
||
|
||
|
||
def test_logout(token: str):
|
||
"""场景: 退出登录"""
|
||
print(f"\n{'='*60}")
|
||
print(f"📋 退出登录")
|
||
print(f"{'='*60}")
|
||
|
||
resp = api_post("/api/user_security/logout", token=token)
|
||
log_test("退出登录", resp.get("code") == 1 or resp.get("code") == 0, resp.get("msg", ""))
|
||
|
||
|
||
# ============================================================
|
||
# 主流程
|
||
# ============================================================
|
||
|
||
def main():
|
||
print("=" * 60)
|
||
print("🚀 闲言APP 用户中心API自动化测试")
|
||
print(f" 服务器: {BASE_URL}")
|
||
print(f" 时间: {time.strftime('%Y-%m-%d %H:%M:%S')}")
|
||
print("=" * 60)
|
||
|
||
results = {"pass": 0, "fail": 0}
|
||
|
||
try:
|
||
# 场景1: 注册+登录
|
||
token, user_id, username, password, email = test_register_and_login()
|
||
|
||
if not token:
|
||
print("\n❌ 登录失败,无法继续后续测试")
|
||
sys.exit(1)
|
||
|
||
# 场景2: 回执登录
|
||
test_receipt_login(username, email)
|
||
|
||
# 场景3: Token登录
|
||
token = test_token_login(token)
|
||
|
||
# 场景4: 用户中心功能
|
||
test_user_center(token)
|
||
|
||
# 场景5: 修改个人信息
|
||
test_profile_update(token)
|
||
|
||
# 场景6: 修改密码
|
||
new_password = test_password_change(token, str(user_id), username, password)
|
||
|
||
# 密码修改后Token可能失效,重新登录
|
||
if new_password != password:
|
||
print("\n 🔄 密码已修改,重新登录...")
|
||
relogin_data = {
|
||
"account": username,
|
||
"password": new_password,
|
||
}
|
||
relogin_resp = api_post("/api/user_security/login", relogin_data)
|
||
if relogin_resp.get("code") == 1:
|
||
token = relogin_resp.get("data", {}).get("userinfo", {}).get("token", token)
|
||
print(f" ✅ 重新登录成功")
|
||
|
||
# 场景7: 二维码登录流程
|
||
test_qrcode_flow()
|
||
|
||
# 场景8: 账号注销
|
||
test_delete_account(token, str(user_id))
|
||
|
||
except requests.exceptions.ConnectionError as e:
|
||
print(f"\n❌ 网络连接失败: {e}")
|
||
except requests.exceptions.Timeout:
|
||
print(f"\n❌ 请求超时")
|
||
except Exception as e:
|
||
print(f"\n❌ 未知错误: {e}")
|
||
import traceback
|
||
traceback.print_exc()
|
||
|
||
print(f"\n{'='*60}")
|
||
print("🏁 测试完成")
|
||
print(f"{'='*60}")
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main()
|