"""#1719:已登录用户微信绑定/解绑 Use Case 测试。 覆盖: - bind:幂等重复绑定、未绑定成功、当前账号已绑其他微信、openid/unionid 冲突 409、用户不存在 - unbind:成功清 openid+unionid、未绑定拒绝、无其他登录方式拒绝、密码/手机/真实邮箱各兜底放行、用户不存在 """ from __future__ import annotations from types import SimpleNamespace import pytest from packages.application.auth.wechat_bind_use_case import ( WechatBindRequest, WechatBindUseCase, WechatUnbindUseCase, ) def _user( user_id="u-1", wechat_openid=None, wechat_unionid=None, password_hash="hashed-pw", phone=None, phone_verified=False, email="user@example.com", email_verified=True, ): return SimpleNamespace( id=user_id, wechat_openid=wechat_openid, wechat_unionid=wechat_unionid, password_hash=password_hash, phone=phone, phone_verified=phone_verified, email=email, email_verified=email_verified, ) class _FakeRepo: """内存仓储:按 id/openid/unionid 建索引,save 原地更新。""" def __init__(self, users): self.users = {u.id: u for u in users} self.saved = [] def find_by_id(self, user_id): return self.users.get(user_id) def find_by_wechat_openid(self, openid): for u in self.users.values(): if u.wechat_openid == openid: return u return None def find_by_wechat_unionid(self, unionid): if not unionid: return None for u in self.users.values(): if u.wechat_unionid == unionid: return u return None def save(self, user): self.saved.append(user) # ==================== bind ==================== def test_bind_success_when_not_bound(): user = _user() repo = _FakeRepo([user]) result, err, status = WechatBindUseCase(repo).bind( WechatBindRequest(user_id="u-1", openid="wx-openid-1", unionid="wx-union-1") ) assert err is None assert status == 200 assert result.user.wechat_openid == "wx-openid-1" assert result.user.wechat_unionid == "wx-union-1" assert repo.saved == [user] def test_bind_idempotent_same_openid(): user = _user(wechat_openid="wx-openid-1", wechat_unionid="wx-union-1") repo = _FakeRepo([user]) result, err, status = WechatBindUseCase(repo).bind( WechatBindRequest(user_id="u-1", openid="wx-openid-1", unionid="wx-union-1") ) assert err is None assert status == 200 assert result.user is user assert repo.saved == [] # 幂等不写库 def test_bind_conflict_user_already_bound_other_wechat(): user = _user(wechat_openid="wx-old") repo = _FakeRepo([user]) result, err, status = WechatBindUseCase(repo).bind(WechatBindRequest(user_id="u-1", openid="wx-new")) assert result is None assert status == 409 assert "已绑定微信" in err def test_bind_conflict_openid_used_by_other_user(): user = _user(user_id="u-1") other = _user(user_id="u-2", wechat_openid="wx-openid-1") repo = _FakeRepo([user, other]) result, err, status = WechatBindUseCase(repo).bind(WechatBindRequest(user_id="u-1", openid="wx-openid-1")) assert result is None assert status == 409 assert "已绑定其他账号" in err assert user.wechat_openid is None # 未写库 def test_bind_conflict_unionid_used_by_other_user(): user = _user(user_id="u-1") # openid 不同,但 unionid 指向同一微信主体 other = _user(user_id="u-2", wechat_openid="wx-other", wechat_unionid="wx-union-x") repo = _FakeRepo([user, other]) result, err, status = WechatBindUseCase(repo).bind( WechatBindRequest(user_id="u-1", openid="wx-openid-1", unionid="wx-union-x") ) assert result is None assert status == 409 assert "微信主体" in err def test_bind_missing_openid_returns_400(): repo = _FakeRepo([_user()]) result, err, status = WechatBindUseCase(repo).bind(WechatBindRequest(user_id="u-1", openid="")) assert result is None assert status == 400 assert "openid" in err def test_bind_user_not_found_returns_404(): repo = _FakeRepo([]) result, err, status = WechatBindUseCase(repo).bind(WechatBindRequest(user_id="ghost", openid="wx-openid-1")) assert result is None assert status == 404 def test_bind_fills_unionid_when_existing_user_has_none(): # 用户历史上只绑了 openid(unionid 为空),再次绑定时补齐 unionid 不冲突 user = _user(wechat_openid="wx-openid-1", wechat_unionid=None) repo = _FakeRepo([user]) result, err, status = WechatBindUseCase(repo).bind( WechatBindRequest(user_id="u-1", openid="wx-openid-1", unionid="wx-union-new") ) # openid 相同 → 幂等成功(不覆盖 unionid,保持数据稳定) assert err is None assert status == 200 # ==================== unbind ==================== def test_unbind_success_with_real_verified_email(): # 默认 _user 即 real@example.com 且 email_verified=True user = _user(wechat_openid="wx-openid-1", wechat_unionid="wx-union-1") repo = _FakeRepo([user]) result, err, status = WechatUnbindUseCase(repo).unbind("u-1") assert err is None assert status == 200 assert result.user.wechat_openid is None assert result.user.wechat_unionid is None assert repo.saved == [user] def test_unbind_rejected_when_only_random_password_hash(): # 微信注册用户:随机密码 hash 存在、邮箱是 @wechat.local 占位、无手机 → 不允许解绑 user = _user( wechat_openid="wx-openid-1", password_hash="random-secret-hash", email="abc@wechat.local", email_verified=True, ) repo = _FakeRepo([user]) result, err, status = WechatUnbindUseCase(repo).unbind("u-1") assert result is None assert status == 400 assert "登录方式" in err assert user.wechat_openid == "wx-openid-1" # 未写库 def test_unbind_allowed_with_verified_phone_even_without_password(): user = _user( wechat_openid="wx-openid-1", password_hash="", phone="13800000000", phone_verified=True, email="wx@wechat.local", email_verified=True, ) repo = _FakeRepo([user]) result, err, status = WechatUnbindUseCase(repo).unbind("u-1") assert err is None assert status == 200 assert result.user.wechat_openid is None def test_unbind_rejected_when_no_other_login_method(): # 无手机、邮箱占位 → 唯一登录方式就是微信,禁止解绑 user = _user( wechat_openid="wx-openid-1", password_hash="", email="abc@wechat.local", email_verified=True, ) repo = _FakeRepo([user]) result, err, status = WechatUnbindUseCase(repo).unbind("u-1") assert result is None assert status == 400 assert "登录方式" in err assert user.wechat_openid == "wx-openid-1" # 未写库 def test_unbind_not_bound_returns_400(): user = _user() # 未绑定 repo = _FakeRepo([user]) result, err, status = WechatUnbindUseCase(repo).unbind("u-1") assert result is None assert status == 400 assert "未绑定" in err def test_unbind_user_not_found_returns_404(): repo = _FakeRepo([]) result, err, status = WechatUnbindUseCase(repo).unbind("ghost") assert result is None assert status == 404 def test_unbind_unverified_phone_does_not_count(): # 手机未验证不算有效登录方式 user = _user( wechat_openid="wx-openid-1", password_hash="", phone="13800000000", phone_verified=False, email="abc@wechat.local", email_verified=True, ) repo = _FakeRepo([user]) result, err, status = WechatUnbindUseCase(repo).unbind("u-1") assert result is None assert status == 400