Files
xiaoxia-saas/tests/unit/test_wechat_callback_logging_1718.py
T
saas-backend-agent 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
feat(#1718): 微信 state 存储 Redis 化 + 回调 UA 日志 + 中文昵称 UTF-8 修复
- 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 路由日志分支、工厂降级分支
2026-09-05 20:45:02 +08:00

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)