f11b71361d
CI/CD Pipeline / Check push changed paths (pull_request) Has been skipped
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (pull_request) Successful in 1s
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 1s
CI/CD Pipeline / Frontend Lint (pull_request) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / PR Build Web Image (pull_request) Has been skipped
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 26s
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 / 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 / Retag skipped Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 25s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been skipped
CI/CD Pipeline / Integration Tests (pull_request) Successful in 1m42s
CI/CD Pipeline / Unit Tests (pull_request) Successful in 1m47s
CI/CD Pipeline / Validate - Style (pull_request) Successful in 2m6s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 2m16s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 2m55s
CI/CD Pipeline / Validate - Python (mypy + alembic) (pull_request) Successful in 3m2s
AI Code Review / AI Code Review (pull_request) Successful in 3m53s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Successful in 10m31s
CI/CD Pipeline / Validate - Security (pull_request) Successful in 15m42s
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 / CI Gate (pull_request) Successful in 1s
CI/CD Pipeline / Deploy Production (pull_request) Has been skipped
CI/CD Pipeline / Canary Release to Production (pull_request) Has been skipped
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
ACR Cleanup / ACR Image Cleanup (pull_request_target) Successful in 8s
Preview Cleanup / Cleanup Preview Environment (pull_request) Successful in 1m14s
- state store 改 Redis(复用 celery Redis,key 前缀 wechat:state:, TTL 10 分钟):SET NX EX 写入,Lua 脚本原子 GET+DEL 一次性消费 (兼容 Redis <6.2 无 GETDEL);Redis 不可用时自动降级内存,登录不中断; 容器重启/多实例后 state 不丢,修复 worker 扩容后回调 state 失效 - /wechat/callback 加可观测日志:User-Agent(识别 MicroMessenger 微信内置浏览器)、state 校验结果、失败上下文,便于排查回调停滞 - 修复微信中文昵称乱码:sns/oauth2/access_token 与 sns/userinfo 响应在 .json() 前显式 encoding=utf-8(微信响应头不带 charset, requests 默认 ISO-8859-1 解码导致中文乱码) - 15 个新单测(全 mock/fake,CI 无 redis 也覆盖):Redis state 存取/一次性消费/eval 降级/异常降级内存/ping 失败降级、中文昵称 UTF-8 解析、errcode 透传、callback 路由日志分支、工厂降级分支
116 lines
4.0 KiB
Python
116 lines
4.0 KiB
Python
"""#1718:微信回调路由可观测性日志分支覆盖(UA/state/错误透传)。
|
|
|
|
直接驱动 wechat_callback 路由函数,mock OAuth service 与用户仓储:
|
|
- 成功路径:日志记录 UA、state 校验通过(MicroMessenger 内置浏览器)
|
|
- 失败路径:OAuth 返回错误时记 warning 并抛 400
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import asyncio
|
|
import os
|
|
import sys
|
|
from pathlib import Path
|
|
from types import SimpleNamespace
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
import pytest
|
|
|
|
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 fastapi import HTTPException # noqa: E402
|
|
|
|
|
|
class _FakeRequest:
|
|
def __init__(self, ua: str):
|
|
self.headers = {"User-Agent": ua}
|
|
|
|
|
|
def _wechat_user():
|
|
return SimpleNamespace(
|
|
openid="openid-callback-1",
|
|
unionid="union-callback-1",
|
|
nickname="微信用户",
|
|
avatar_url="http://x/a.png",
|
|
)
|
|
|
|
|
|
def _fake_oauth_factory(success: bool):
|
|
service = MagicMock()
|
|
if success:
|
|
service.handle_callback.return_value = (_wechat_user(), None)
|
|
else:
|
|
service.handle_callback.return_value = (None, "无效的 state 参数,请求可能已过期或被篡改")
|
|
return service
|
|
|
|
|
|
def test_wechat_callback_success_logs_ua_and_state(caplog):
|
|
fake_repo = MagicMock()
|
|
sync_response = SimpleNamespace(
|
|
access_token="at",
|
|
refresh_token="rt",
|
|
user_id="u-1",
|
|
nickname="微信用户",
|
|
avatar_url="",
|
|
is_new_user=False,
|
|
expires_in=1800,
|
|
)
|
|
fake_use_case = MagicMock()
|
|
fake_use_case.execute.return_value = (sync_response, None)
|
|
|
|
user = SimpleNamespace(
|
|
id="u-1",
|
|
phone_verified=True,
|
|
email_verified=True,
|
|
email="u@example.com",
|
|
)
|
|
fake_repo.find_by_id.return_value = user
|
|
|
|
request_obj = SimpleNamespace(code="code-1", state="state-1")
|
|
fake_http = _FakeRequest("Mozilla/5.0 (Linux; Android 13) MicroMessenger/8.0.40 WeChat/8.0.40")
|
|
|
|
import packages.application.auth.wechat_oauth_service as oauth_mod
|
|
import packages.application.auth.wechat_sync_use_case as sync_mod
|
|
|
|
orig_oauth = oauth_mod.get_wechat_oauth_service
|
|
orig_sync = sync_mod.WechatSyncUseCase
|
|
oauth_mod.get_wechat_oauth_service = MagicMock(return_value=_fake_oauth_factory(success=True))
|
|
sync_mod.WechatSyncUseCase = MagicMock(return_value=fake_use_case)
|
|
try:
|
|
with caplog.at_level("INFO", logger="app.api.routes.auth"):
|
|
resp = asyncio.run(auth_route.wechat_callback(request_obj, fake_http, user_repository=fake_repo))
|
|
finally:
|
|
oauth_mod.get_wechat_oauth_service = orig_oauth
|
|
sync_mod.WechatSyncUseCase = orig_sync
|
|
|
|
assert resp.user_id == "u-1"
|
|
assert resp.binding_complete is True
|
|
log_text = " ".join(rec.getMessage() for rec in caplog.records)
|
|
assert "微信回调" in log_text
|
|
assert "MicroMessenger" in log_text or "微信内置浏览器=True" in log_text
|
|
|
|
|
|
def test_wechat_callback_failure_raises_400_with_detail(caplog):
|
|
request_obj = SimpleNamespace(code="code-bad", state="state-bad")
|
|
fake_http = _FakeRequest("Mozilla/5.0 Chrome/127")
|
|
fake_service = _fake_oauth_factory(success=False)
|
|
|
|
import packages.application.auth.wechat_oauth_service as oauth_mod
|
|
|
|
orig = oauth_mod.get_wechat_oauth_service
|
|
oauth_mod.get_wechat_oauth_service = MagicMock(return_value=fake_service)
|
|
try:
|
|
with caplog.at_level("WARNING", logger="app.api.routes.auth"):
|
|
with pytest.raises(HTTPException) as exc_info:
|
|
asyncio.run(auth_route.wechat_callback(request_obj, fake_http, user_repository=MagicMock()))
|
|
finally:
|
|
oauth_mod.get_wechat_oauth_service = orig
|
|
|
|
assert exc_info.value.status_code == 400
|
|
assert "state" in exc_info.value.detail
|
|
assert any("微信回调" in rec.getMessage() for rec in caplog.records)
|