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
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:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user