3d07461ef5
- User 新增 force_onboarding 列(仿 debug_trace_enabled)+ 迁移 user_force_onboarding。
- POST /admin/api/users/{id}/force-onboarding(operator 角色 + 审计 user.force_onboarding.set)。
- UserOut(/me 与登录响应)带出该字段;/api/v1/user/onboarding/complete 走完即自动清回 false,只触发一次。
- 测试:admin 设置+审计+角色守卫、走完引导自动清标记。
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: zzhyyyyy <2685922758@qq.com>
Reviewed-on: #43
Co-authored-by: zhuzihao <zhuzihao@wonderable.ai>
Co-committed-by: zhuzihao <zhuzihao@wonderable.ai>
98 lines
3.6 KiB
Python
98 lines
3.6 KiB
Python
"""新手引导"按 设备+账号 只触发一次"的后端闭环测试。
|
|
|
|
覆盖:
|
|
- 全新 (账号, 设备) 登录 → onboarding_completed=False(照常走引导)
|
|
- 标记完成后,同 (账号, 设备) 再登录 → True(跳过引导,跨"重装"= 同 device_id 重新登录)
|
|
- 同账号换设备(device_id 不同)→ False(新设备重新引导)
|
|
- 标记幂等;缺 device_id 时按未完成处理
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
|
|
def _login(client, phone: str, device_id: str | None):
|
|
body = {"phone": phone, "code": "123456"}
|
|
if device_id is not None:
|
|
body["device_id"] = device_id
|
|
r = client.post("/api/v1/auth/sms/login", json=body)
|
|
assert r.status_code == 200, r.text
|
|
return r.json()
|
|
|
|
|
|
def _mark_complete(client, access: str, device_id: str):
|
|
return client.post(
|
|
"/api/v1/user/onboarding/complete",
|
|
json={"device_id": device_id},
|
|
headers={"Authorization": f"Bearer {access}"},
|
|
)
|
|
|
|
|
|
def test_onboarding_once_per_device_and_account(client) -> None:
|
|
phone = "13800138001"
|
|
dev_a = "android-id-aaaa"
|
|
dev_b = "android-id-bbbb"
|
|
|
|
# ① 全新账号+设备:未完成
|
|
data = _login(client, phone, dev_a)
|
|
assert data["onboarding_completed"] is False
|
|
access = data["access_token"]
|
|
|
|
# ② 走完引导 → 标记完成(幂等:再标一次也 200)
|
|
assert _mark_complete(client, access, dev_a).status_code == 200
|
|
assert _mark_complete(client, access, dev_a).json()["ok"] is True
|
|
|
|
# ③ 同账号 + 同设备(模拟卸载重装后重新登录)→ 已完成,跳过引导
|
|
assert _login(client, phone, dev_a)["onboarding_completed"] is True
|
|
|
|
# ④ 同账号 + 新设备 → 仍未完成,新设备重新走引导
|
|
assert _login(client, phone, dev_b)["onboarding_completed"] is False
|
|
|
|
|
|
def test_onboarding_mark_on_one_device_does_not_leak_to_other(client) -> None:
|
|
"""B 设备标记完成,不影响 A 设备(逐设备独立)。"""
|
|
phone = "13800138002"
|
|
data_a = _login(client, phone, "devA")
|
|
assert data_a["onboarding_completed"] is False
|
|
|
|
data_b = _login(client, phone, "devB")
|
|
assert _mark_complete(client, data_b["access_token"], "devB").status_code == 200
|
|
|
|
assert _login(client, phone, "devB")["onboarding_completed"] is True
|
|
assert _login(client, phone, "devA")["onboarding_completed"] is False
|
|
|
|
|
|
def test_onboarding_missing_device_id_treated_as_incomplete(client) -> None:
|
|
"""老客户端不传 device_id:按未完成处理,不报错、不误跳过。"""
|
|
phone = "13800138003"
|
|
data = _login(client, phone, device_id=None)
|
|
assert data["onboarding_completed"] is False
|
|
|
|
|
|
def test_complete_clears_force_onboarding(client) -> None:
|
|
"""运营「一键开启新手引导」(force_onboarding=True)后,用户走完引导即自动清除该标记,
|
|
/me 随之回到 false——否则下次启动会被无限拉回引导。"""
|
|
from app.db.session import SessionLocal
|
|
from app.models.user import User
|
|
|
|
phone = "13800138004"
|
|
dev = "android-id-force"
|
|
data = _login(client, phone, dev)
|
|
access = data["access_token"]
|
|
uid = data["user"]["id"]
|
|
|
|
# 模拟运营在后台一键开启(admin 接口本身另在 test_admin_write 覆盖)
|
|
db = SessionLocal()
|
|
try:
|
|
db.get(User, uid).force_onboarding = True
|
|
db.commit()
|
|
finally:
|
|
db.close()
|
|
|
|
def _me() -> dict:
|
|
return client.get(
|
|
"/api/v1/auth/me", headers={"Authorization": f"Bearer {access}"}
|
|
).json()
|
|
|
|
assert _me()["force_onboarding"] is True
|
|
assert _mark_complete(client, access, dev).status_code == 200
|
|
assert _me()["force_onboarding"] is False
|