"""新手引导"按 设备+账号 只触发一次"的后端闭环测试。 覆盖: - 全新 (账号, 设备) 登录 → 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