style: auto-format with black + isort + prettier [skip ci-format-check]
CI/CD Pipeline / Check push changed paths (pull_request) Has been skipped
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 6s
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (pull_request) Successful in 5s
CI/CD Pipeline / Frontend Lint (pull_request) Has been skipped
CI/CD Pipeline / PR Build Web Image (pull_request) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / Build Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 11s
CI/CD Pipeline / Retag skipped Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Retag skipped Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Retag skipped Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been skipped
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 18s
CI/CD Pipeline / Unit Tests (pull_request) Successful in 1m15s
CI/CD Pipeline / Integration Tests (pull_request) Successful in 1m27s
CI/CD Pipeline / Validate - Python (mypy + alembic) (pull_request) Successful in 1m29s
CI/CD Pipeline / Validate - Style (pull_request) Successful in 1m56s
AI Code Review / AI Code Review (pull_request) Successful in 2m14s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 2m48s
CI/CD Pipeline / Validate - Security (pull_request) Successful in 4m20s
CI/CD Pipeline / Build Production API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Deploy Production (pull_request) Has been skipped
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / Canary Release to Production (pull_request) Has been skipped
CI/CD Pipeline / CI Gate (pull_request) Successful in 1s
ACR Cleanup / ACR Image Cleanup (pull_request_target) Successful in 7s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Successful in 2m15s
Preview Cleanup / Cleanup Preview Environment (pull_request) Successful in 15s
Preview Deploy / Deploy Preview Environment (pull_request) Failing after 10m19s

This commit is contained in:
CI Bot
2026-09-06 07:04:19 +00:00
parent 0fa5b31f4f
commit 3fba310b9e
2 changed files with 69 additions and 66 deletions
@@ -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
+66 -55
View File
@@ -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 命中 uaunionid 首次查找(优先级查询)命中 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