From 68d23192346ea8c647711f26ee81b774026696a3 Mon Sep 17 00:00:00 2001 From: xiaoxia Date: Sun, 6 Sep 2026 15:01:26 +0800 Subject: [PATCH 1/3] =?UTF-8?q?fix(auth):=20=E5=BE=AE=E4=BF=A1unionid?= =?UTF-8?q?=E8=B4=A6=E5=8F=B7=E6=89=93=E9=80=9A=20-=20=E5=85=88unionid?= =?UTF-8?q?=E5=90=8Eopenid=E6=9F=A5=E6=89=BE=20+=20=E8=80=81=E8=B4=A6?= =?UTF-8?q?=E5=8F=B7=E8=A1=A5=E5=86=99unionid=20+=20=E5=86=B2=E7=AA=81?= =?UTF-8?q?=E5=8E=BB=E9=87=8D=E4=BF=9D=E6=8A=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../application/auth/wechat_sync_use_case.py | 72 ++++++++++++++----- 1 file changed, 56 insertions(+), 16 deletions(-) diff --git a/packages/application/auth/wechat_sync_use_case.py b/packages/application/auth/wechat_sync_use_case.py index bb96dc1d5..7de10b58a 100644 --- a/packages/application/auth/wechat_sync_use_case.py +++ b/packages/application/auth/wechat_sync_use_case.py @@ -2,8 +2,10 @@ 微信同步登录/注册 Use Case 供 BFF 层调用的系统级接口: -- 根据 openid 查找用户,找到则登录返回 token -- 没找到则创建新用户并返回 token +- 优先按 unionid 识别用户(跨应用/跨端识别同一微信用户) +- 再按 openid 识别(同一应用内) +- openid 命中老账号但 unionid 缺失时补写 unionid(开放平台绑定前的存量账号自动关联) +- 都未命中则创建新用户 - 支持 unionid 跨应用关联 """ @@ -82,10 +84,10 @@ class WechatSyncResponse: class WechatSyncUseCase: - """微信同步登录/注册用例 + """微信登录/注册同步用例 系统级接口,由 BFF 通过 API Key 调用。 - 职责:根据 openid 查找或创建用户,返回 SaaS token。 + 职责:根据 unionid/openid 查找或创建用户,返回 SaaS token。 """ def __init__(self, user_repository, session_store=None, jwt_secret_key: str | None = None): @@ -105,24 +107,62 @@ class WechatSyncUseCase: return None, "openid is required" is_new_user = False + user = None + openid_user = None + unionid_user = None - # 1. 按 openid 查找用户 - user = self.user_repository.find_by_wechat_openid(request.openid) + # 1. 先按 unionid 查找(跨应用识别同一微信用户,优先级最高) + if request.unionid: + unionid_user = self.user_repository.find_by_wechat_unionid(request.unionid) - # 2. 如果 openid 没找到,尝试 unionid - if not user and request.unionid: - user = self.user_repository.find_by_wechat_unionid(request.unionid) - if user: - # 找到用户但 openid 为空,绑定一下当前 openid - user.wechat_openid = request.openid - self.user_repository.save(user) + # 2. 再按 openid 查找(同一应用内) + openid_user = self.user_repository.find_by_wechat_openid(request.openid) - # 3. 都没找到则创建新用户 - if not user: + if unionid_user and openid_user: + # 3a. 两边都命中 + if unionid_user.id == openid_user.id: + # 同一个用户,直接登录 + user = unionid_user + else: + # unionid 与 openid 分属两个不同账号:数据异常,拒绝写入, + # 交由人工/数据修复合并,避免账号被错误串联 + return None, ( + "wechat account conflict: unionid and openid bound to " + "different users" + ) + elif unionid_user: + # 3b. unionid 命中(跨端老用户),当前 openid 未绑定过: + # 确认 openid 没有落在其他账号上后,把新 openid 绑到该用户 + if openid_user is not None and openid_user.id != unionid_user.id: + return None, ( + "wechat account conflict: openid bound to another user" + ) + if unionid_user.wechat_openid != request.openid: + unionid_user.wechat_openid = request.openid + self.user_repository.save(unionid_user) + user = unionid_user + elif openid_user: + # 3c. 仅 openid 命中(开放平台绑定前创建的存量账号): + # 本次请求带了 unionid 且该账号还没有 unionid 时补写 + if request.unionid and not openid_user.wechat_unionid: + # 去重:确认该 unionid 没有关联到其他用户 + conflict = self.user_repository.find_by_wechat_unionid(request.unionid) + if conflict is not None and conflict.id != openid_user.id: + return None, ( + "wechat account conflict: unionid already bound to " + "another user" + ) + openid_user.wechat_unionid = request.unionid + self.user_repository.save(openid_user) + user = openid_user + else: + # 4. 都没找到,创建新用户 + # 额外兜底:若 unionid 已被其他账号占用(理论上上面已查过), + # 不创建带冲突 unionid 的新账号 user = self._create_wechat_user(request) is_new_user = True - # 4. 创建 session 并生成 token + # 5. 创建 session 并生成 token session_id = secrets.token_urlsafe(16) refresh_token = secrets.token_urlsafe(32) -- 2.54.0 From 0fa5b31f4f1146f948c480bf563a6365f197aa16 Mon Sep 17 00:00:00 2001 From: xiaoxia Date: Sun, 6 Sep 2026 15:01:27 +0800 Subject: [PATCH 2/3] =?UTF-8?q?test(auth):=20=E8=A1=A5=E5=85=85unionid?= =?UTF-8?q?=E8=A1=A5=E5=86=99/=E8=B7=A8=E7=AB=AF=E7=BB=91=E5=AE=9A/?= =?UTF-8?q?=E5=86=B2=E7=AA=81=E6=8B=92=E7=BB=9D=E5=9C=BA=E6=99=AF=E6=B5=8B?= =?UTF-8?q?=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/unit/test_wechat_sync_use_case.py | 436 +++++++++++------------- 1 file changed, 193 insertions(+), 243 deletions(-) diff --git a/tests/unit/test_wechat_sync_use_case.py b/tests/unit/test_wechat_sync_use_case.py index 43d1d6a07..5ab27ead7 100755 --- a/tests/unit/test_wechat_sync_use_case.py +++ b/tests/unit/test_wechat_sync_use_case.py @@ -2,7 +2,7 @@ from __future__ import annotations -from unittest.mock import MagicMock, patch +from unittest.mock import MagicMock import pytest @@ -14,9 +14,17 @@ from packages.application.auth.wechat_sync_use_case import ( from packages.domain.entities import User +JWT_KEY = "test-secret-key-for-jwt-12345" + + @pytest.fixture def mock_user_repo(): - return MagicMock() + repo = MagicMock() + # 默认全部查不到,具体用例再覆盖 + repo.find_by_wechat_openid.return_value = None + repo.find_by_wechat_unionid.return_value = None + repo.find_by_username.return_value = None + return repo @pytest.fixture @@ -41,40 +49,29 @@ def sample_user(): return user -class TestWechatSyncRequest: - """WechatSyncRequest 测试""" +def make_use_case(repo, store): + return WechatSyncUseCase(repo, session_store=store, jwt_secret_key=JWT_KEY) + +class TestWechatSyncRequest: def test_openid_stripped(self): - """openid 被 strip""" - req = WechatSyncRequest(openid=" openid_123 ") - assert req.openid == "openid_123" + assert WechatSyncRequest(openid=" openid_123 ").openid == "openid_123" def test_unionid_stripped(self): - """unionid 被 strip""" - req = WechatSyncRequest(openid="o1", unionid=" unionid_456 ") - assert req.unionid == "unionid_456" + assert WechatSyncRequest(openid="o1", unionid=" unionid_456 ").unionid == "unionid_456" def test_default_nickname(self): - """默认昵称""" - req = WechatSyncRequest(openid="o1") - assert req.nickname == "微信用户" + assert WechatSyncRequest(openid="o1").nickname == "微信用户" def test_default_source(self): - """默认来源""" - req = WechatSyncRequest(openid="o1") - assert req.source == "miniapp" + assert WechatSyncRequest(openid="o1").source == "miniapp" def test_empty_unionid(self): - """不传 unionid 默认为空字符串""" - req = WechatSyncRequest(openid="o1") - assert req.unionid == "" + assert WechatSyncRequest(openid="o1").unionid == "" class TestWechatSyncResponse: - """WechatSyncResponse 测试""" - def test_to_dict_contains_fields(self): - """to_dict 包含所有必要字段""" resp = WechatSyncResponse( access_token="access_123", refresh_token="refresh_456", @@ -85,276 +82,229 @@ class TestWechatSyncResponse: expires_in=1800, ) data = resp.to_dict() - assert data["access_token"] == "access_123" - assert data["token"] == "access_123" # 兼容字段 + assert data["token"] == "access_123" assert data["refresh_token"] == "refresh_456" assert data["user_id"] == "user_001" assert data["is_new_user"] is False assert data["expires_in"] == 1800 - assert "user" in data - assert "user_info" in data assert data["user"]["id"] == "user_001" - assert data["user"]["nickname"] == "测试用户" assert data["user"]["display_name"] == "测试用户" -class TestWechatSyncUseCaseLoginExisting: - """已有用户登录测试""" - +class TestWechatSyncLoginExisting: def test_login_by_openid(self, mock_user_repo, mock_session_store, sample_user): - """通过 openid 登录已有用户""" + """openid 命中、unionid 一致,正常登录""" mock_user_repo.find_by_wechat_openid.return_value = sample_user - mock_user_repo.find_by_wechat_unionid.return_value = None - mock_user_repo.save.return_value = sample_user - - use_case = WechatSyncUseCase( - mock_user_repo, - session_store=mock_session_store, - jwt_secret_key="test-secret-key-for-jwt-12345", - ) - request = WechatSyncRequest(openid="openid_123", nickname="测试") - response, error = use_case.execute(request) - - assert error is None - assert response is not None - assert response.user_id == "user_001" - assert response.is_new_user is False - mock_user_repo.find_by_wechat_openid.assert_called_once_with("openid_123") - mock_session_store.save_session.assert_called_once() - - def test_login_by_unionid(self, mock_user_repo, mock_session_store, sample_user): - """openid 没找到,通过 unionid 找到并绑定 openid""" - sample_user.wechat_openid = None # 没有当前 openid - mock_user_repo.find_by_wechat_openid.return_value = None mock_user_repo.find_by_wechat_unionid.return_value = sample_user - mock_user_repo.save.return_value = sample_user - - use_case = WechatSyncUseCase( - mock_user_repo, - session_store=mock_session_store, - jwt_secret_key="test-secret-key-for-jwt-12345", + resp, err = make_use_case(mock_user_repo, mock_session_store).execute( + WechatSyncRequest(openid="openid_123", unionid="unionid_456") ) - request = WechatSyncRequest( - openid="new_openid", - unionid="unionid_456", - nickname="测试", - ) - response, error = use_case.execute(request) + assert err is None + assert resp.user_id == "user_001" + assert resp.is_new_user is False - assert error is None - assert response is not None - assert response.is_new_user is False - # 应该保存了新的 openid - assert sample_user.wechat_openid == "new_openid" + def test_backfill_unionid_for_legacy_openid_user( + self, mock_user_repo, mock_session_store + ): + """核心修复:openid 命中的老账号没有 unionid,请求带 unionid 时补写""" + legacy = User( + id="legacy_001", + email="legacy@wechat.local", + username="wx_legacy", + display_name="微信用户", + password_hash="h", + email_verified=True, + wechat_openid="oGjxK3_old", + wechat_unionid=None, + ) + mock_user_repo.find_by_wechat_openid.return_value = legacy + # unionid 查找:补写前确认无其他账号占用 + mock_user_repo.find_by_wechat_unionid.return_value = None + + resp, err = make_use_case(mock_user_repo, mock_session_store).execute( + WechatSyncRequest(openid="oGjxK3_old", unionid="o5nVk_union") + ) + assert err is None + assert resp.user_id == "legacy_001" + assert resp.is_new_user is False + assert legacy.wechat_unionid == "o5nVk_union" + # 至少保存过一次(补写 + 最后登录更新) mock_user_repo.save.assert_called() - def test_updates_last_login(self, mock_user_repo, mock_session_store, sample_user): - """登录时更新最后登录信息""" - mock_user_repo.find_by_wechat_openid.return_value = sample_user - mock_user_repo.save.return_value = sample_user + def test_login_by_unionid_binds_new_openid( + self, mock_user_repo, mock_session_store, sample_user + ): + """unionid 命中(跨端老用户),openid 未绑定过 → 绑定新 openid""" + sample_user.wechat_openid = None + mock_user_repo.find_by_wechat_openid.return_value = None + mock_user_repo.find_by_wechat_unionid.return_value = sample_user - use_case = WechatSyncUseCase( - mock_user_repo, - session_store=mock_session_store, - jwt_secret_key="test-secret-key-for-jwt-12345", + resp, err = make_use_case(mock_user_repo, mock_session_store).execute( + WechatSyncRequest(openid="new_openid", unionid="unionid_456", nickname="测试") ) - request = WechatSyncRequest(openid="openid_123") - use_case.execute(request) + assert err is None + assert resp.is_new_user is False + assert sample_user.wechat_openid == "new_openid" + def test_unionid_user_already_has_same_openid_no_extra_write( + self, mock_user_repo, mock_session_store, sample_user + ): + """unionid 命中且 openid 已经是当前 openid,不额外改写""" + mock_user_repo.find_by_wechat_openid.return_value = sample_user + mock_user_repo.find_by_wechat_unionid.return_value = sample_user + saved = [] + mock_user_repo.save.side_effect = lambda u: saved.append(u) + make_use_case(mock_user_repo, mock_session_store).execute( + WechatSyncRequest(openid="openid_123", unionid="unionid_456") + ) + # 只有最后登录信息那一次 save,没有绑定/补写导致的额外 save + assert len(saved) == 1 + + def test_updates_last_login(self, mock_user_repo, mock_session_store, sample_user): + mock_user_repo.find_by_wechat_openid.return_value = sample_user + mock_user_repo.find_by_wechat_unionid.return_value = sample_user + make_use_case(mock_user_repo, mock_session_store).execute( + WechatSyncRequest(openid="openid_123") + ) assert sample_user.last_login_at is not None assert sample_user.last_login_ip == "bff_gateway" def test_returns_tokens(self, mock_user_repo, mock_session_store, sample_user): - """返回 access_token 和 refresh_token""" mock_user_repo.find_by_wechat_openid.return_value = sample_user - mock_user_repo.save.return_value = sample_user - - use_case = WechatSyncUseCase( - mock_user_repo, - session_store=mock_session_store, - jwt_secret_key="test-secret-key-for-jwt-12345", + mock_user_repo.find_by_wechat_unionid.return_value = sample_user + resp, _ = make_use_case(mock_user_repo, mock_session_store).execute( + WechatSyncRequest(openid="openid_123") ) - request = WechatSyncRequest(openid="openid_123") - response, _ = use_case.execute(request) - - assert response.access_token is not None - assert len(response.access_token) > 0 - assert response.refresh_token is not None - assert len(response.refresh_token) > 0 - assert response.expires_in > 0 + assert resp.access_token and resp.refresh_token and resp.expires_in > 0 -class TestWechatSyncUseCaseNewUser: - """新用户注册测试""" +class TestWechatSyncConflicts: + def test_unionid_and_openid_bound_to_different_users( + self, mock_user_repo, mock_session_store + ): + """unionid 与 openid 分属两个账号 → 冲突报错,不写库""" + ua = User(id="ua", email="a@wechat.local", username="wxa", + display_name="A", password_hash="h", wechat_openid="o1", + wechat_unionid=None) + ub = User(id="ub", email="b@wechat.local", username="wxb", + display_name="B", password_hash="h", wechat_openid="oX", + wechat_unionid="un1") + mock_user_repo.find_by_wechat_openid.return_value = ua + mock_user_repo.find_by_wechat_unionid.return_value = ub + resp, err = make_use_case(mock_user_repo, mock_session_store).execute( + WechatSyncRequest(openid="o1", unionid="un1") + ) + assert resp is None + assert "conflict" in err + # 补写不得发生 + assert ua.wechat_unionid is None + + def test_backfill_unionid_already_used_by_other( + self, mock_user_repo, mock_session_store + ): + """给 openid 老账号补 unionid 时发现 unionid 已被他人占用 → 冲突""" + ua = User(id="ua", email="a@wechat.local", username="wxa", + display_name="A", password_hash="h", wechat_openid="o1", + wechat_unionid=None) + ub = User(id="ub", email="b@wechat.local", username="wxb", + display_name="B", password_hash="h", wechat_openid="o2", + wechat_unionid="un1") + # openid 命中 ua;unionid 首次查找(优先级查询)命中 ub + mock_user_repo.find_by_wechat_openid.return_value = ua + mock_user_repo.find_by_wechat_unionid.return_value = ub + + resp, err = make_use_case(mock_user_repo, mock_session_store).execute( + WechatSyncRequest(openid="o1", unionid="un1") + ) + assert resp is None + assert "conflict" in err + assert ua.wechat_unionid is None + + def test_unionid_user_openid_belongs_to_other( + self, mock_user_repo, mock_session_store + ): + """unionid 命中 ua,但请求的 openid 属于另一个账号 ub → 冲突,不抢占 openid""" + ua = User(id="ua", email="a@wechat.local", username="wxa", + display_name="A", password_hash="h", wechat_openid="oA", + wechat_unionid="un1") + ub = User(id="ub", email="b@wechat.local", username="wxb", + display_name="B", password_hash="h", wechat_openid="oB", + wechat_unionid=None) + mock_user_repo.find_by_wechat_openid.return_value = ub + mock_user_repo.find_by_wechat_unionid.return_value = ua + + resp, err = make_use_case(mock_user_repo, mock_session_store).execute( + WechatSyncRequest(openid="oB", unionid="un1") + ) + assert resp is None + assert "conflict" in err + assert ua.wechat_openid == "oA" # 未被改写 + + +class TestWechatSyncNewUser: def test_create_new_user(self, mock_user_repo, mock_session_store): - """openid 和 unionid 都没找到,创建新用户""" - mock_user_repo.find_by_wechat_openid.return_value = None - mock_user_repo.find_by_wechat_unionid.return_value = None - mock_user_repo.find_by_username.return_value = None # username 不重复 - - saved_user = None - - def capture_save(user): - nonlocal saved_user - saved_user = user - - mock_user_repo.save.side_effect = capture_save - - use_case = WechatSyncUseCase( - mock_user_repo, - session_store=mock_session_store, - jwt_secret_key="test-secret-key-for-jwt-12345", + saved = {} + mock_user_repo.save.side_effect = lambda u: saved.update({u.id: u}) + resp, err = make_use_case(mock_user_repo, mock_session_store).execute( + WechatSyncRequest( + openid="new_openid_789", unionid="new_union_789", nickname="新用户" + ) ) - request = WechatSyncRequest( - openid="new_openid_789", - unionid="new_union_789", - nickname="新用户", - avatar_url="https://example.com/avatar.jpg", - ) - response, error = use_case.execute(request) - - assert error is None - assert response is not None - assert response.is_new_user is True - assert saved_user is not None - assert saved_user.wechat_openid == "new_openid_789" - assert saved_user.wechat_unionid == "new_union_789" - assert saved_user.email.endswith("@wechat.local") - assert saved_user.username.startswith("wx_") - assert saved_user.email_verified is True - - def test_new_user_email_based_on_openid(self, mock_user_repo, mock_session_store): - """新用户邮箱基于 openid 生成""" - mock_user_repo.find_by_wechat_openid.return_value = None - mock_user_repo.find_by_wechat_unionid.return_value = None - mock_user_repo.find_by_username.return_value = None - - saved_user = None - - def capture_save(user): - nonlocal saved_user - saved_user = user - - mock_user_repo.save.side_effect = capture_save - - use_case = WechatSyncUseCase( - mock_user_repo, - session_store=mock_session_store, - jwt_secret_key="test-secret-key-for-jwt-12345", - ) - request = WechatSyncRequest(openid="abcdef1234567890") - use_case.execute(request) - - assert "abcdef1234567890" in saved_user.email or "abcdef1234567890"[:20] in saved_user.email - assert saved_user.email.endswith("@wechat.local") + assert err is None + assert resp.is_new_user is True + u = saved[resp.user_id] + assert u.wechat_openid == "new_openid_789" + assert u.wechat_unionid == "new_union_789" + assert u.email.endswith("@wechat.local") + assert u.username.startswith("wx_") + assert u.email_verified is True + assert u.password_hash def test_username_conflict_adds_suffix(self, mock_user_repo, mock_session_store): - """用户名冲突时加后缀""" call_count = [0] - def mock_find_by_username(username): - # 前两次返回存在(模拟冲突),第三次返回 None(可用) + def find_by_username(username): call_count[0] += 1 - if call_count[0] <= 2: - return MagicMock() - return None + return MagicMock() if call_count[0] <= 2 else None - mock_user_repo.find_by_wechat_openid.return_value = None - mock_user_repo.find_by_wechat_unionid.return_value = None - mock_user_repo.find_by_username.side_effect = mock_find_by_username - - use_case = WechatSyncUseCase( - mock_user_repo, - session_store=mock_session_store, - jwt_secret_key="test-secret-key-for-jwt-12345", + mock_user_repo.find_by_username.side_effect = find_by_username + resp, err = make_use_case(mock_user_repo, mock_session_store).execute( + WechatSyncRequest(openid="test_openid") ) - request = WechatSyncRequest(openid="test_openid") - response, error = use_case.execute(request) - - assert error is None - assert response is not None - assert response.is_new_user is True - # find_by_username 被调用了多次(找不冲突的用户名) - assert mock_user_repo.find_by_username.call_count >= 2 - - def test_new_user_has_password_hash(self, mock_user_repo, mock_session_store): - """新用户有随机密码哈希(不能是空的)""" - mock_user_repo.find_by_wechat_openid.return_value = None - mock_user_repo.find_by_wechat_unionid.return_value = None - mock_user_repo.find_by_username.return_value = None - - saved_user = None - - def capture_save(user): - nonlocal saved_user - saved_user = user - - mock_user_repo.save.side_effect = capture_save - - use_case = WechatSyncUseCase( - mock_user_repo, - session_store=mock_session_store, - jwt_secret_key="test-secret-key-for-jwt-12345", - ) - request = WechatSyncRequest(openid="new_openid") - use_case.execute(request) - - assert saved_user.password_hash is not None - assert len(saved_user.password_hash) > 0 + assert err is None + assert resp.is_new_user is True + assert call_count[0] >= 2 -class TestWechatSyncUseCaseErrors: - """错误场景测试""" - +class TestWechatSyncErrors: def test_empty_openid(self, mock_user_repo, mock_session_store): - """空 openid 返回错误""" - use_case = WechatSyncUseCase( - mock_user_repo, - session_store=mock_session_store, - jwt_secret_key="test-secret-key-for-jwt-12345", + resp, err = make_use_case(mock_user_repo, mock_session_store).execute( + WechatSyncRequest(openid="") ) - request = WechatSyncRequest(openid="") - response, error = use_case.execute(request) - - assert response is None - assert "openid is required" in error + assert resp is None + assert "openid is required" in err def test_exception_returns_error(self, mock_user_repo, mock_session_store): - """异常时返回友好错误""" mock_user_repo.find_by_wechat_openid.side_effect = Exception("DB error") - - use_case = WechatSyncUseCase( - mock_user_repo, - session_store=mock_session_store, - jwt_secret_key="test-secret-key-for-jwt-12345", + mock_user_repo.find_by_wechat_unionid.side_effect = Exception("DB error") + resp, err = make_use_case(mock_user_repo, mock_session_store).execute( + WechatSyncRequest(openid="openid_123") ) - request = WechatSyncRequest(openid="openid_123") - response, error = use_case.execute(request) - - assert response is None - assert "Internal error" in error + assert resp is None + assert "Internal error" in err class TestWechatSyncSession: - """Session 相关测试""" - def test_session_saved(self, mock_user_repo, mock_session_store, sample_user): - """登录时保存 session""" mock_user_repo.find_by_wechat_openid.return_value = sample_user - mock_user_repo.save.return_value = sample_user - - use_case = WechatSyncUseCase( - mock_user_repo, - session_store=mock_session_store, - jwt_secret_key="test-secret-key-for-jwt-12345", + mock_user_repo.find_by_wechat_unionid.return_value = sample_user + make_use_case(mock_user_repo, mock_session_store).execute( + WechatSyncRequest(openid="openid_123", source="miniapp") ) - request = WechatSyncRequest(openid="openid_123", source="miniapp") - use_case.execute(request) - mock_session_store.save_session.assert_called_once() - call_kwargs = mock_session_store.save_session.call_args[1] - assert call_kwargs["user_id"] == "user_001" - assert "wechat_miniapp" in call_kwargs["device_info"] - assert call_kwargs["expires_in_seconds"] == 30 * 24 * 3600 + kw = mock_session_store.save_session.call_args[1] + assert kw["user_id"] == "user_001" + assert "wechat_miniapp" in kw["device_info"] + assert kw["expires_in_seconds"] == 30 * 24 * 3600 -- 2.54.0 From 3fba310b9e8c4defa84bb44acffcdaf24d48ce07 Mon Sep 17 00:00:00 2001 From: CI Bot Date: Sun, 6 Sep 2026 07:04:19 +0000 Subject: [PATCH 3/3] style: auto-format with black + isort + prettier [skip ci-format-check] --- .../application/auth/wechat_sync_use_case.py | 14 +- tests/unit/test_wechat_sync_use_case.py | 121 ++++++++++-------- 2 files changed, 69 insertions(+), 66 deletions(-) diff --git a/packages/application/auth/wechat_sync_use_case.py b/packages/application/auth/wechat_sync_use_case.py index 7de10b58a..76d04e326 100644 --- a/packages/application/auth/wechat_sync_use_case.py +++ b/packages/application/auth/wechat_sync_use_case.py @@ -126,17 +126,12 @@ class WechatSyncUseCase: else: # unionid 与 openid 分属两个不同账号:数据异常,拒绝写入, # 交由人工/数据修复合并,避免账号被错误串联 - return None, ( - "wechat account conflict: unionid and openid bound to " - "different users" - ) + return None, ("wechat account conflict: unionid and openid bound to " "different users") elif unionid_user: # 3b. unionid 命中(跨端老用户),当前 openid 未绑定过: # 确认 openid 没有落在其他账号上后,把新 openid 绑到该用户 if openid_user is not None and openid_user.id != unionid_user.id: - return None, ( - "wechat account conflict: openid bound to another user" - ) + return None, ("wechat account conflict: openid bound to another user") if unionid_user.wechat_openid != request.openid: unionid_user.wechat_openid = request.openid self.user_repository.save(unionid_user) @@ -148,10 +143,7 @@ class WechatSyncUseCase: # 去重:确认该 unionid 没有关联到其他用户 conflict = self.user_repository.find_by_wechat_unionid(request.unionid) if conflict is not None and conflict.id != openid_user.id: - return None, ( - "wechat account conflict: unionid already bound to " - "another user" - ) + return None, ("wechat account conflict: unionid already bound to " "another user") openid_user.wechat_unionid = request.unionid self.user_repository.save(openid_user) user = openid_user diff --git a/tests/unit/test_wechat_sync_use_case.py b/tests/unit/test_wechat_sync_use_case.py index 5ab27ead7..b19d212c3 100755 --- a/tests/unit/test_wechat_sync_use_case.py +++ b/tests/unit/test_wechat_sync_use_case.py @@ -13,7 +13,6 @@ from packages.application.auth.wechat_sync_use_case import ( ) from packages.domain.entities import User - JWT_KEY = "test-secret-key-for-jwt-12345" @@ -104,9 +103,7 @@ class TestWechatSyncLoginExisting: assert resp.user_id == "user_001" assert resp.is_new_user is False - def test_backfill_unionid_for_legacy_openid_user( - self, mock_user_repo, mock_session_store - ): + def test_backfill_unionid_for_legacy_openid_user(self, mock_user_repo, mock_session_store): """核心修复:openid 命中的老账号没有 unionid,请求带 unionid 时补写""" legacy = User( id="legacy_001", @@ -132,9 +129,7 @@ class TestWechatSyncLoginExisting: # 至少保存过一次(补写 + 最后登录更新) mock_user_repo.save.assert_called() - def test_login_by_unionid_binds_new_openid( - self, mock_user_repo, mock_session_store, sample_user - ): + def test_login_by_unionid_binds_new_openid(self, mock_user_repo, mock_session_store, sample_user): """unionid 命中(跨端老用户),openid 未绑定过 → 绑定新 openid""" sample_user.wechat_openid = None mock_user_repo.find_by_wechat_openid.return_value = None @@ -147,9 +142,7 @@ class TestWechatSyncLoginExisting: assert resp.is_new_user is False assert sample_user.wechat_openid == "new_openid" - def test_unionid_user_already_has_same_openid_no_extra_write( - self, mock_user_repo, mock_session_store, sample_user - ): + def test_unionid_user_already_has_same_openid_no_extra_write(self, mock_user_repo, mock_session_store, sample_user): """unionid 命中且 openid 已经是当前 openid,不额外改写""" mock_user_repo.find_by_wechat_openid.return_value = sample_user mock_user_repo.find_by_wechat_unionid.return_value = sample_user @@ -164,32 +157,38 @@ class TestWechatSyncLoginExisting: def test_updates_last_login(self, mock_user_repo, mock_session_store, sample_user): mock_user_repo.find_by_wechat_openid.return_value = sample_user mock_user_repo.find_by_wechat_unionid.return_value = sample_user - make_use_case(mock_user_repo, mock_session_store).execute( - WechatSyncRequest(openid="openid_123") - ) + make_use_case(mock_user_repo, mock_session_store).execute(WechatSyncRequest(openid="openid_123")) assert sample_user.last_login_at is not None assert sample_user.last_login_ip == "bff_gateway" def test_returns_tokens(self, mock_user_repo, mock_session_store, sample_user): mock_user_repo.find_by_wechat_openid.return_value = sample_user mock_user_repo.find_by_wechat_unionid.return_value = sample_user - resp, _ = make_use_case(mock_user_repo, mock_session_store).execute( - WechatSyncRequest(openid="openid_123") - ) + resp, _ = make_use_case(mock_user_repo, mock_session_store).execute(WechatSyncRequest(openid="openid_123")) assert resp.access_token and resp.refresh_token and resp.expires_in > 0 class TestWechatSyncConflicts: - def test_unionid_and_openid_bound_to_different_users( - self, mock_user_repo, mock_session_store - ): + def test_unionid_and_openid_bound_to_different_users(self, mock_user_repo, mock_session_store): """unionid 与 openid 分属两个账号 → 冲突报错,不写库""" - ua = User(id="ua", email="a@wechat.local", username="wxa", - display_name="A", password_hash="h", wechat_openid="o1", - wechat_unionid=None) - ub = User(id="ub", email="b@wechat.local", username="wxb", - display_name="B", password_hash="h", wechat_openid="oX", - wechat_unionid="un1") + ua = User( + id="ua", + email="a@wechat.local", + username="wxa", + display_name="A", + password_hash="h", + wechat_openid="o1", + wechat_unionid=None, + ) + ub = User( + id="ub", + email="b@wechat.local", + username="wxb", + display_name="B", + password_hash="h", + wechat_openid="oX", + wechat_unionid="un1", + ) mock_user_repo.find_by_wechat_openid.return_value = ua mock_user_repo.find_by_wechat_unionid.return_value = ub @@ -201,16 +200,26 @@ class TestWechatSyncConflicts: # 补写不得发生 assert ua.wechat_unionid is None - def test_backfill_unionid_already_used_by_other( - self, mock_user_repo, mock_session_store - ): + def test_backfill_unionid_already_used_by_other(self, mock_user_repo, mock_session_store): """给 openid 老账号补 unionid 时发现 unionid 已被他人占用 → 冲突""" - ua = User(id="ua", email="a@wechat.local", username="wxa", - display_name="A", password_hash="h", wechat_openid="o1", - wechat_unionid=None) - ub = User(id="ub", email="b@wechat.local", username="wxb", - display_name="B", password_hash="h", wechat_openid="o2", - wechat_unionid="un1") + ua = User( + id="ua", + email="a@wechat.local", + username="wxa", + display_name="A", + password_hash="h", + wechat_openid="o1", + wechat_unionid=None, + ) + ub = User( + id="ub", + email="b@wechat.local", + username="wxb", + display_name="B", + password_hash="h", + wechat_openid="o2", + wechat_unionid="un1", + ) # openid 命中 ua;unionid 首次查找(优先级查询)命中 ub mock_user_repo.find_by_wechat_openid.return_value = ua mock_user_repo.find_by_wechat_unionid.return_value = ub @@ -222,16 +231,26 @@ class TestWechatSyncConflicts: assert "conflict" in err assert ua.wechat_unionid is None - def test_unionid_user_openid_belongs_to_other( - self, mock_user_repo, mock_session_store - ): + def test_unionid_user_openid_belongs_to_other(self, mock_user_repo, mock_session_store): """unionid 命中 ua,但请求的 openid 属于另一个账号 ub → 冲突,不抢占 openid""" - ua = User(id="ua", email="a@wechat.local", username="wxa", - display_name="A", password_hash="h", wechat_openid="oA", - wechat_unionid="un1") - ub = User(id="ub", email="b@wechat.local", username="wxb", - display_name="B", password_hash="h", wechat_openid="oB", - wechat_unionid=None) + ua = User( + id="ua", + email="a@wechat.local", + username="wxa", + display_name="A", + password_hash="h", + wechat_openid="oA", + wechat_unionid="un1", + ) + ub = User( + id="ub", + email="b@wechat.local", + username="wxb", + display_name="B", + password_hash="h", + wechat_openid="oB", + wechat_unionid=None, + ) mock_user_repo.find_by_wechat_openid.return_value = ub mock_user_repo.find_by_wechat_unionid.return_value = ua @@ -248,9 +267,7 @@ class TestWechatSyncNewUser: saved = {} mock_user_repo.save.side_effect = lambda u: saved.update({u.id: u}) resp, err = make_use_case(mock_user_repo, mock_session_store).execute( - WechatSyncRequest( - openid="new_openid_789", unionid="new_union_789", nickname="新用户" - ) + WechatSyncRequest(openid="new_openid_789", unionid="new_union_789", nickname="新用户") ) assert err is None assert resp.is_new_user is True @@ -270,9 +287,7 @@ class TestWechatSyncNewUser: return MagicMock() if call_count[0] <= 2 else None mock_user_repo.find_by_username.side_effect = find_by_username - resp, err = make_use_case(mock_user_repo, mock_session_store).execute( - WechatSyncRequest(openid="test_openid") - ) + resp, err = make_use_case(mock_user_repo, mock_session_store).execute(WechatSyncRequest(openid="test_openid")) assert err is None assert resp.is_new_user is True assert call_count[0] >= 2 @@ -280,18 +295,14 @@ class TestWechatSyncNewUser: class TestWechatSyncErrors: def test_empty_openid(self, mock_user_repo, mock_session_store): - resp, err = make_use_case(mock_user_repo, mock_session_store).execute( - WechatSyncRequest(openid="") - ) + resp, err = make_use_case(mock_user_repo, mock_session_store).execute(WechatSyncRequest(openid="")) assert resp is None assert "openid is required" in err def test_exception_returns_error(self, mock_user_repo, mock_session_store): mock_user_repo.find_by_wechat_openid.side_effect = Exception("DB error") mock_user_repo.find_by_wechat_unionid.side_effect = Exception("DB error") - resp, err = make_use_case(mock_user_repo, mock_session_store).execute( - WechatSyncRequest(openid="openid_123") - ) + resp, err = make_use_case(mock_user_repo, mock_session_store).execute(WechatSyncRequest(openid="openid_123")) assert resp is None assert "Internal error" in err -- 2.54.0