528f56254d
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / PR Build API Image (push) Has been skipped
CI/CD Pipeline / Check push changed paths (pull_request) Has been skipped
CI/CD Pipeline / PR Build Web Image (push) Has been skipped
CI/CD Pipeline / PR Build Worker Image (push) Has been skipped
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (push) Successful in 2s
CI/CD Pipeline / Build Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 3s
CI/CD Pipeline / Build Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (pull_request) Successful in 3s
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / Check push changed paths (push) Successful in 8s
CI/CD Pipeline / Validate - Style (pull_request) Has been skipped
CI/CD Pipeline / Validate - Security (pull_request) Has been skipped
CI/CD Pipeline / Validate - Python (mypy + alembic) (pull_request) Has been skipped
CI/CD Pipeline / Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / Frontend Lint (pull_request) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / Retag skipped Staging Worker 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 Web Image (pull_request) Has been skipped
CI/CD Pipeline / PR Build Web Image (pull_request) Successful in 20s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 21s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 23s
CI/CD Pipeline / Build Staging API Image (push) Successful in 17s
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 Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Web Image (push) Successful in 23s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 33s
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 / Deploy Production (pull_request) Has been skipped
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / Retag skipped Staging API Image (push) Has been skipped
CI/CD Pipeline / Retag skipped Staging Web Image (push) Has been skipped
CI/CD Pipeline / Canary Release to Production (pull_request) Has been skipped
CI/CD Pipeline / Retag skipped Staging Worker Image (push) Has been skipped
CI/CD Pipeline / Validate - Python (mypy + alembic) (push) Successful in 2m0s
CI/CD Pipeline / Integration Tests (push) Successful in 2m17s
CI/CD Pipeline / Validate - Style (push) Successful in 2m28s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m8s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 2m6s
AI Code Review / AI Code Review (pull_request) Successful in 3m27s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 3m38s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 1m30s
CI/CD Pipeline / Validate - Security (push) Successful in 4m54s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 5m17s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m11s
CI/CD Pipeline / Staging E2E Tests (push) Failing after 5m10s
CI/CD Pipeline / Unit Tests (push) Successful in 8m18s
CI/CD Pipeline / Build Production API Image (push) Has been skipped
CI/CD Pipeline / Build Production Web Image (push) Has been skipped
CI/CD Pipeline / Build Production Worker Image (push) Has been skipped
CI/CD Pipeline / CI Gate (push) Has been skipped
CI/CD Pipeline / Deploy Production (push) Has been skipped
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Canary Release to Production (push) Has been skipped
CI/CD Pipeline / CI Gate (pull_request) Failing after 11m48s
Co-authored-by: xiaoxia <dev@xiaoxiajianji.com> Co-committed-by: xiaoxia <dev@xiaoxiajianji.com>
223 lines
7.1 KiB
Python
223 lines
7.1 KiB
Python
"""#1718:PATCH /auth/me 资料更新接口测试。
|
||
|
||
覆盖:
|
||
- 正常更新昵称并落库
|
||
- strip 生效(前后空白去除)
|
||
- 纯空白/超长 -> 422(pydantic 校验)
|
||
- 首次设置昵称 profile_completed False->True
|
||
- 已完成用户重复提交幂等(仍 True)
|
||
- 未登录由 get_current_user 依赖保证 401(框架行为,这里验证路由声明了该依赖)
|
||
- 响应结构 {user: {...}} 含 wechat_bound/profile_completed 全字段
|
||
- 微信新建用户 profile_completed 默认 False(wechat_sync _create_wechat_user)
|
||
"""
|
||
|
||
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
|
||
from pydantic import ValidationError
|
||
|
||
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 packages.adapters.in_memory.user_repository import InMemoryUserRepository # noqa: E402
|
||
from packages.domain.entities import User # noqa: E402
|
||
|
||
|
||
def _auth_user(user):
|
||
return SimpleNamespace(user=user, session_id="s-1", token_type="user_auth")
|
||
|
||
|
||
def _make_user(**kw):
|
||
defaults = dict(
|
||
id="u-1",
|
||
email="user@example.com",
|
||
username="user",
|
||
display_name="微信用户",
|
||
password_hash="x",
|
||
email_verified=True,
|
||
profile_completed=False,
|
||
)
|
||
defaults.update(kw)
|
||
return User(**defaults)
|
||
|
||
|
||
# ---------- 请求体校验 ----------
|
||
|
||
|
||
def test_display_name_strips_whitespace():
|
||
req = auth_route.UpdateProfileRequest(display_name=" ying123 ")
|
||
assert req.display_name == "ying123"
|
||
|
||
|
||
def test_display_name_blank_rejected():
|
||
with pytest.raises(ValidationError) as exc:
|
||
auth_route.UpdateProfileRequest(display_name=" ")
|
||
assert "空白" in str(exc.value)
|
||
|
||
|
||
def test_display_name_empty_rejected():
|
||
with pytest.raises(ValidationError):
|
||
auth_route.UpdateProfileRequest(display_name="")
|
||
|
||
|
||
def test_display_name_too_long_rejected():
|
||
with pytest.raises(ValidationError) as exc:
|
||
auth_route.UpdateProfileRequest(display_name="甲" * 21)
|
||
assert "1-20" in str(exc.value)
|
||
|
||
|
||
def test_display_name_max_length_accepted():
|
||
req = auth_route.UpdateProfileRequest(display_name="甲" * 20)
|
||
assert req.display_name == "甲" * 20
|
||
|
||
|
||
# ---------- 路由逻辑 ----------
|
||
|
||
|
||
def test_patch_me_updates_display_name_and_persists():
|
||
user = _make_user()
|
||
repo = InMemoryUserRepository()
|
||
repo.save(user)
|
||
|
||
resp = asyncio.run(
|
||
auth_route.update_current_user_profile(
|
||
auth_route.UpdateProfileRequest(display_name=" ying123 "),
|
||
current_user=_auth_user(user),
|
||
user_repository=repo,
|
||
)
|
||
)
|
||
assert resp.user.display_name == "ying123"
|
||
assert resp.user.profile_completed is True
|
||
assert resp.user.wechat_bound is False
|
||
# 落库验证
|
||
fresh = repo.find_by_id("u-1")
|
||
assert fresh.display_name == "ying123"
|
||
assert fresh.profile_completed is True
|
||
|
||
|
||
def test_patch_me_first_time_sets_profile_completed_true():
|
||
user = _make_user(profile_completed=False)
|
||
repo = InMemoryUserRepository()
|
||
repo.save(user)
|
||
assert repo.find_by_id("u-1").profile_completed is False
|
||
|
||
asyncio.run(
|
||
auth_route.update_current_user_profile(
|
||
auth_route.UpdateProfileRequest(display_name="小虾"),
|
||
current_user=_auth_user(user),
|
||
user_repository=repo,
|
||
)
|
||
)
|
||
assert repo.find_by_id("u-1").profile_completed is True
|
||
|
||
|
||
def test_patch_me_idempotent_for_completed_user():
|
||
user = _make_user(display_name="老名字", profile_completed=True)
|
||
repo = InMemoryUserRepository()
|
||
repo.save(user)
|
||
|
||
resp = asyncio.run(
|
||
auth_route.update_current_user_profile(
|
||
auth_route.UpdateProfileRequest(display_name="新名字"),
|
||
current_user=_auth_user(user),
|
||
user_repository=repo,
|
||
)
|
||
)
|
||
assert resp.user.profile_completed is True
|
||
assert resp.user.display_name == "新名字"
|
||
# 再提交一次同样内容,不报错、状态稳定
|
||
resp2 = asyncio.run(
|
||
auth_route.update_current_user_profile(
|
||
auth_route.UpdateProfileRequest(display_name="新名字"),
|
||
current_user=_auth_user(repo.find_by_id("u-1")),
|
||
user_repository=repo,
|
||
)
|
||
)
|
||
assert resp2.user.profile_completed is True
|
||
|
||
|
||
def test_patch_me_response_contains_all_me_fields():
|
||
user = _make_user(wechat_openid="wx-1", phone="13800000000", phone_verified=True)
|
||
repo = InMemoryUserRepository()
|
||
repo.save(user)
|
||
|
||
resp = asyncio.run(
|
||
auth_route.update_current_user_profile(
|
||
auth_route.UpdateProfileRequest(display_name="昵称"),
|
||
current_user=_auth_user(user),
|
||
user_repository=repo,
|
||
)
|
||
)
|
||
payload = resp.user.model_dump()
|
||
for field in (
|
||
"user_id",
|
||
"email",
|
||
"username",
|
||
"display_name",
|
||
"email_verified",
|
||
"phone",
|
||
"phone_verified",
|
||
"binding_complete",
|
||
"wechat_bound",
|
||
"profile_completed",
|
||
):
|
||
assert field in payload, f"missing field {field}"
|
||
assert payload["wechat_bound"] is True
|
||
assert payload["phone"] == "13800000000"
|
||
|
||
|
||
def test_get_me_includes_profile_completed_flag():
|
||
# 未完成
|
||
u = _make_user(profile_completed=False)
|
||
resp = asyncio.run(auth_route.get_current_user_info(authenticated_user=_auth_user(u)))
|
||
assert resp.profile_completed is False
|
||
assert resp.wechat_bound is False
|
||
|
||
# 已完成 + 已绑微信
|
||
u2 = _make_user(profile_completed=True, wechat_openid="wx-9")
|
||
resp2 = asyncio.run(auth_route.get_current_user_info(authenticated_user=_auth_user(u2)))
|
||
assert resp2.profile_completed is True
|
||
assert resp2.wechat_bound is True
|
||
|
||
|
||
def test_patch_me_requires_auth_dependency():
|
||
# 路由签名必须依赖 get_current_user,未携带 token 时框架返回 401
|
||
params = (
|
||
auth_route.update_current_user_profile.__wrapped__
|
||
if hasattr(auth_route.update_current_user_profile, "__wrapped__")
|
||
else auth_route.update_current_user_profile
|
||
)
|
||
import inspect
|
||
|
||
sig = inspect.signature(params)
|
||
dep = sig.parameters.get("current_user")
|
||
assert dep is not None
|
||
assert dep.default is not None and getattr(dep.default, "dependency", None) is auth_route.get_current_user
|
||
|
||
|
||
def test_wechat_new_user_created_with_profile_completed_false():
|
||
# 微信同步建号:新用户 profile_completed=False(需引导设置昵称)
|
||
from packages.application.auth.wechat_sync_use_case import (
|
||
WechatSyncRequest,
|
||
WechatSyncUseCase,
|
||
)
|
||
|
||
repo = InMemoryUserRepository()
|
||
# session_store 用 mock,不依赖 redis
|
||
use_case = WechatSyncUseCase(user_repository=repo, session_store=MagicMock(), jwt_secret_key="test-secret")
|
||
resp, err = use_case.execute(WechatSyncRequest(openid="wx-new-openid", nickname="微信测试", source="web"))
|
||
assert err is None
|
||
user = repo.find_by_id(resp.user_id)
|
||
assert user.profile_completed is False
|