"""#1719:微信绑定/解绑路由层测试(直接驱动路由函数)。 覆盖: - GET /wechat/bind/url:调 oauth 生成链接、记日志 - POST /wechat/bind:oauth 失败→400;绑定成功→success+user.wechat_bound=True; use case 返回冲突→对应状态码透传 - DELETE /wechat/bind:成功→success=True;use case 报错→状态码透传 - /auth/me 返回 wechat_bound 字段 """ 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 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 fastapi import HTTPException # noqa: E402 def _auth_user(user_id="u-1", openid=None): user = SimpleNamespace( id=user_id, wechat_openid=openid, email="user@example.com", email_verified=True, username="user", display_name="用户", phone="", phone_verified=False, profile_completed=True, ) return SimpleNamespace(user=user, session_id="s-1", token_type="user_auth") def _patched_bind(result, error, status): """构造打了补丁的 wechat_bind_use_case 模块""" mod = SimpleNamespace( WechatBindRequest=lambda **kw: SimpleNamespace(**kw), WechatBindUseCase=MagicMock(), WechatUnbindUseCase=MagicMock(), ) fake_bind_uc = MagicMock() fake_bind_uc.bind.return_value = (result, error, status) mod.WechatBindUseCase.return_value = fake_bind_uc return mod def test_get_bind_url_returns_url_and_state(): fake_oauth = MagicMock() fake_oauth.generate_auth_url.return_value = ("https://open.weixin.qq.com/qrconnect?xxx", "state-bind-1") import packages.application.auth.wechat_oauth_service as oauth_mod orig = oauth_mod.get_wechat_oauth_service oauth_mod.get_wechat_oauth_service = MagicMock(return_value=fake_oauth) try: resp = asyncio.run(auth_route.get_wechat_bind_url(current_user=_auth_user())) finally: oauth_mod.get_wechat_oauth_service = orig assert resp.auth_url.startswith("https://open.weixin.qq.com") assert resp.state == "state-bind-1" def test_bind_oauth_error_returns_400(): fake_oauth = MagicMock() fake_oauth.handle_callback.return_value = (None, "无效的 state 参数") import packages.application.auth.wechat_oauth_service as oauth_mod orig = oauth_mod.get_wechat_oauth_service oauth_mod.get_wechat_oauth_service = MagicMock(return_value=fake_oauth) try: with pytest.raises(HTTPException) as exc: asyncio.run( auth_route.wechat_bind( SimpleNamespace(code="c-1", state="s-1"), current_user=_auth_user(), user_repository=MagicMock(), ) ) finally: oauth_mod.get_wechat_oauth_service = orig assert exc.value.status_code == 400 assert "state" in exc.value.detail def test_bind_success_returns_user_with_wechat_bound(): fake_oauth = MagicMock() fake_oauth.handle_callback.return_value = ( SimpleNamespace(openid="wx-openid-1", unionid="wx-union-1"), None, ) bound_user = SimpleNamespace( id="u-1", wechat_openid="wx-openid-1", email="user@example.com", email_verified=True, username="user", display_name="用户", phone="", phone_verified=False, profile_completed=True, ) import packages.application.auth.wechat_oauth_service as oauth_mod from packages.application.auth import wechat_bind_use_case as bind_mod orig_oauth = oauth_mod.get_wechat_oauth_service fake_bind_uc = MagicMock() fake_bind_uc.bind.return_value = (SimpleNamespace(user=bound_user), None, 200) orig_bind = bind_mod.WechatBindUseCase bind_mod.WechatBindUseCase = MagicMock(return_value=fake_bind_uc) oauth_mod.get_wechat_oauth_service = MagicMock(return_value=fake_oauth) try: resp = asyncio.run( auth_route.wechat_bind( SimpleNamespace(code="c-1", state="s-1"), current_user=_auth_user(), user_repository=MagicMock(), ) ) finally: oauth_mod.get_wechat_oauth_service = orig_oauth bind_mod.WechatBindUseCase = orig_bind assert resp.success is True assert resp.user.wechat_bound is True assert resp.user.user_id == "u-1" # 绑定请求应带上当前用户 id 与微信 openid call_kwargs = fake_bind_uc.bind.call_args[0][0] assert call_kwargs.user_id == "u-1" assert call_kwargs.openid == "wx-openid-1" def test_bind_conflict_propagates_409(): fake_oauth = MagicMock() fake_oauth.handle_callback.return_value = ( SimpleNamespace(openid="wx-openid-1", unionid=""), None, ) import packages.application.auth.wechat_oauth_service as oauth_mod from packages.application.auth import wechat_bind_use_case as bind_mod orig_oauth = oauth_mod.get_wechat_oauth_service fake_bind_uc = MagicMock() fake_bind_uc.bind.return_value = (None, "该微信已绑定其他账号,请先在原账号解绑", 409) orig_bind = bind_mod.WechatBindUseCase bind_mod.WechatBindUseCase = MagicMock(return_value=fake_bind_uc) oauth_mod.get_wechat_oauth_service = MagicMock(return_value=fake_oauth) try: with pytest.raises(HTTPException) as exc: asyncio.run( auth_route.wechat_bind( SimpleNamespace(code="c-1", state="s-1"), current_user=_auth_user(), user_repository=MagicMock(), ) ) finally: oauth_mod.get_wechat_oauth_service = orig_oauth bind_mod.WechatBindUseCase = orig_bind assert exc.value.status_code == 409 assert "已绑定其他账号" in exc.value.detail def test_unbind_success_returns_success_true(): unbound_user = SimpleNamespace( id="u-1", wechat_openid=None, email="user@example.com", email_verified=True, username="user", display_name="用户", phone="", phone_verified=False, ) from packages.application.auth import wechat_bind_use_case as bind_mod fake_uc = MagicMock() fake_uc.unbind.return_value = (SimpleNamespace(user=unbound_user), None, 200) orig = bind_mod.WechatUnbindUseCase bind_mod.WechatUnbindUseCase = MagicMock(return_value=fake_uc) try: resp = asyncio.run( auth_route.wechat_unbind(current_user=_auth_user(openid="wx-old"), user_repository=MagicMock()) ) finally: bind_mod.WechatUnbindUseCase = orig assert resp.success is True fake_uc.unbind.assert_called_once_with("u-1") def test_unbind_rejected_no_other_login_propagates_400(): from packages.application.auth import wechat_bind_use_case as bind_mod fake_uc = MagicMock() fake_uc.unbind.return_value = (None, "账号需要至少一种其他登录方式(已验证手机或真实邮箱)后才能解绑微信", 400) orig = bind_mod.WechatUnbindUseCase bind_mod.WechatUnbindUseCase = MagicMock(return_value=fake_uc) try: with pytest.raises(HTTPException) as exc: asyncio.run(auth_route.wechat_unbind(current_user=_auth_user(openid="wx-old"), user_repository=MagicMock())) finally: bind_mod.WechatUnbindUseCase = orig assert exc.value.status_code == 400 assert "登录方式" in exc.value.detail def test_me_includes_wechat_bound_flag(): # 已绑定用户 resp = asyncio.run(auth_route.get_current_user_info(authenticated_user=_auth_user(openid="wx-openid-1"))) assert resp.wechat_bound is True # 未绑定用户 resp2 = asyncio.run(auth_route.get_current_user_info(authenticated_user=_auth_user(openid=None))) assert resp2.wechat_bound is False