"""#1718:PATCH /auth/me 资料更新接口测试。 覆盖: - 正常更新昵称并落库 - strip 生效(前后空白去除) - 纯空白/超长 -> 422(pydantic 校验) - 首次设置昵称 profile_completed False->True - 已完成用户重复提交幂等(仍 True) - 未登录由 get_current_user 依赖保证 401(框架行为,这里验证路由声明了该依赖) - 响应结构 {user: {...}} 含 wechat_bound/profile_completed 全字段 - 微信新建用户 profile_completed 默认 False(wechat_sync _create_wechat_user) """ from __future__ import annotations import asyncio import os import sys from pathlib import Path from types import SimpleNamespace from unittest.mock import MagicMock import pytest from pydantic import ValidationError os.environ.setdefault("JWT_SECRET_KEY", "unit-test-secret-key-for-testing") os.environ.setdefault("DATABASE_URL", "sqlite:///test.db") sys.path.insert(0, str(Path(__file__).resolve().parents[2] / "apps" / "api")) from app.api.routes import auth as auth_route # noqa: E402 from packages.adapters.in_memory.user_repository import InMemoryUserRepository # noqa: E402 from packages.domain.entities import User # noqa: E402 def _auth_user(user): return SimpleNamespace(user=user, session_id="s-1", token_type="user_auth") def _make_user(**kw): defaults = dict( id="u-1", email="user@example.com", username="user", display_name="微信用户", password_hash="x", email_verified=True, profile_completed=False, ) defaults.update(kw) return User(**defaults) # ---------- 请求体校验 ---------- def test_display_name_strips_whitespace(): req = auth_route.UpdateProfileRequest(display_name=" ying123 ") assert req.display_name == "ying123" def test_display_name_blank_rejected(): with pytest.raises(ValidationError) as exc: auth_route.UpdateProfileRequest(display_name=" ") assert "空白" in str(exc.value) def test_display_name_empty_rejected(): with pytest.raises(ValidationError): auth_route.UpdateProfileRequest(display_name="") def test_display_name_too_long_rejected(): with pytest.raises(ValidationError) as exc: auth_route.UpdateProfileRequest(display_name="甲" * 21) assert "1-20" in str(exc.value) def test_display_name_max_length_accepted(): req = auth_route.UpdateProfileRequest(display_name="甲" * 20) assert req.display_name == "甲" * 20 # ---------- 路由逻辑 ---------- def test_patch_me_updates_display_name_and_persists(): user = _make_user() repo = InMemoryUserRepository() repo.save(user) resp = asyncio.run( auth_route.update_current_user_profile( auth_route.UpdateProfileRequest(display_name=" ying123 "), current_user=_auth_user(user), user_repository=repo, ) ) assert resp.user.display_name == "ying123" assert resp.user.profile_completed is True assert resp.user.wechat_bound is False # 落库验证 fresh = repo.find_by_id("u-1") assert fresh.display_name == "ying123" assert fresh.profile_completed is True def test_patch_me_first_time_sets_profile_completed_true(): user = _make_user(profile_completed=False) repo = InMemoryUserRepository() repo.save(user) assert repo.find_by_id("u-1").profile_completed is False asyncio.run( auth_route.update_current_user_profile( auth_route.UpdateProfileRequest(display_name="小虾"), current_user=_auth_user(user), user_repository=repo, ) ) assert repo.find_by_id("u-1").profile_completed is True def test_patch_me_idempotent_for_completed_user(): user = _make_user(display_name="老名字", profile_completed=True) repo = InMemoryUserRepository() repo.save(user) resp = asyncio.run( auth_route.update_current_user_profile( auth_route.UpdateProfileRequest(display_name="新名字"), current_user=_auth_user(user), user_repository=repo, ) ) assert resp.user.profile_completed is True assert resp.user.display_name == "新名字" # 再提交一次同样内容,不报错、状态稳定 resp2 = asyncio.run( auth_route.update_current_user_profile( auth_route.UpdateProfileRequest(display_name="新名字"), current_user=_auth_user(repo.find_by_id("u-1")), user_repository=repo, ) ) assert resp2.user.profile_completed is True def test_patch_me_response_contains_all_me_fields(): user = _make_user(wechat_openid="wx-1", phone="13800000000", phone_verified=True) repo = InMemoryUserRepository() repo.save(user) resp = asyncio.run( auth_route.update_current_user_profile( auth_route.UpdateProfileRequest(display_name="昵称"), current_user=_auth_user(user), user_repository=repo, ) ) payload = resp.user.model_dump() for field in ( "user_id", "email", "username", "display_name", "email_verified", "phone", "phone_verified", "binding_complete", "wechat_bound", "profile_completed", ): assert field in payload, f"missing field {field}" assert payload["wechat_bound"] is True assert payload["phone"] == "13800000000" def test_get_me_includes_profile_completed_flag(): # 未完成 u = _make_user(profile_completed=False) resp = asyncio.run(auth_route.get_current_user_info(authenticated_user=_auth_user(u))) assert resp.profile_completed is False assert resp.wechat_bound is False # 已完成 + 已绑微信 u2 = _make_user(profile_completed=True, wechat_openid="wx-9") resp2 = asyncio.run(auth_route.get_current_user_info(authenticated_user=_auth_user(u2))) assert resp2.profile_completed is True assert resp2.wechat_bound is True def test_patch_me_requires_auth_dependency(): # 路由签名必须依赖 get_current_user,未携带 token 时框架返回 401 params = ( auth_route.update_current_user_profile.__wrapped__ if hasattr(auth_route.update_current_user_profile, "__wrapped__") else auth_route.update_current_user_profile ) import inspect sig = inspect.signature(params) dep = sig.parameters.get("current_user") assert dep is not None assert dep.default is not None and getattr(dep.default, "dependency", None) is auth_route.get_current_user def test_wechat_new_user_created_with_profile_completed_false(): # 微信同步建号:新用户 profile_completed=False(需引导设置昵称) from packages.application.auth.wechat_sync_use_case import ( WechatSyncRequest, WechatSyncUseCase, ) repo = InMemoryUserRepository() # session_store 用 mock,不依赖 redis use_case = WechatSyncUseCase(user_repository=repo, session_store=MagicMock(), jwt_secret_key="test-secret") resp, err = use_case.execute(WechatSyncRequest(openid="wx-new-openid", nickname="微信测试", source="web")) assert err is None user = repo.find_by_id(resp.user_id) assert user.profile_completed is False