"""channels plugins/wechat_mp 层单元测试共享 fixture。 提供 ``fake_wechat_mp_client`` 桩,模拟 ``WeChatMpClient`` (``yuxi.channels.plugins.wechat_mp.wechat_mp_client.WeChatMpClient``)。 微信公众号适配器测试通过此桩注入客户端依赖,不发起真实 HTTP 请求。 桩使用 ``AsyncMock()``,预设主要方法返回值(errcode=0 响应 / media_id / token / 用户信息等),调用方可按需覆盖。方法签名以 ``WeChatMpClient`` 实现为准。单文件独有的 helper 应保留在对应测试文件内 (见 testing-guidelines.md)。 同时提供 ``fake_config_port`` / ``fake_cache_port`` / ``fake_logger_port`` / ``fake_persistence_port`` 端口桩,供适配器测试注入 DI 依赖。 """ from __future__ import annotations from unittest.mock import AsyncMock, MagicMock import pytest @pytest.fixture def fake_wechat_mp_client() -> AsyncMock: """WeChatMpClient 桩。 ``AsyncMock()`` 模拟 ``WeChatMpClient``,预设主要方法返回值: - ``send_custom_message`` 返回 ``{"errcode": 0, "msgid": "msg_123"}``; - ``upload_media_cached`` / ``upload_media`` 返回 ``"media_id_abc"``; - ``download_media`` 返回 ``b"fake_image_bytes"``; - ``get_user_info`` 返回含 openid / nickname / headimgurl 的字典; - ``get_user_list`` 返回含 total / count / data / next_openid 的字典; - ``get_callback_ip`` 返回 ``{"ip_list": ["127.0.0.1"]}``; - ``refresh_access_token`` / ``get_access_token`` 返回 token 字符串; - ``check_customer_service_window`` / ``consume_customer_service_quota`` 返回 ``True``; - ``mark_user_message`` / ``mark_other_event`` / ``set_active`` / ``attach_http_client`` / ``detach_http_client`` 返回 ``None``; - ``drain`` 返回 ``True``。 """ client = AsyncMock() # 客服消息发送 client.send_custom_message.return_value = {"errcode": 0, "msgid": "msg_123"} # 媒体上传/下载 client.upload_media_cached.return_value = "media_id_abc" client.upload_media.return_value = "media_id_abc" client.download_media.return_value = b"fake_image_bytes" # 用户信息查询 client.get_user_info.return_value = { "openid": "o123", "nickname": "TestUser", "headimgurl": "http://example.com/avatar.jpg", } client.get_user_list.return_value = { "total": 1, "count": 1, "data": {"openid": ["o123", "o456"]}, "next_openid": "o456", } # 通用 API client.get_callback_ip.return_value = {"ip_list": ["127.0.0.1"]} # access_token 中控 client.refresh_access_token.return_value = "fake_access_token" client.get_access_token.return_value = "fake_access_token" # 客服消息窗口与额度 client.check_customer_service_window.return_value = True client.consume_customer_service_quota.return_value = True client.mark_user_message.return_value = None client.mark_other_event.return_value = None # 生命周期管理 client.set_active.return_value = None client.drain.return_value = True client.attach_http_client.return_value = None client.detach_http_client.return_value = None return client @pytest.fixture def fake_config_port() -> AsyncMock: """ConfigPort 桩。 ``ConfigPort`` 的 ``get`` 方法为异步,使用 ``AsyncMock()``。 默认返回 ``None``,调用方可按需覆盖。 """ return AsyncMock() @pytest.fixture def fake_cache_port() -> AsyncMock: """CachePort 桩。 ``CachePort`` 的 ``get`` / ``set`` / ``delete`` / ``acquireAdvisoryLock`` 等方法为异步,使用 ``AsyncMock()``。默认返回 ``None``,调用方可按需覆盖。 """ return AsyncMock() @pytest.fixture def fake_logger_port() -> MagicMock: """LoggerPort 桩。 ``LoggerPort`` 的 ``info`` / ``warn`` / ``error`` / ``exception`` / ``debug`` 方法为异步,使用 ``MagicMock()`` + 按方法挂 ``AsyncMock()`` 以精确模拟同步/异步混合的端口协议。 """ logger = MagicMock() logger.debug = AsyncMock() logger.info = AsyncMock() logger.warn = AsyncMock() logger.error = AsyncMock() logger.exception = AsyncMock() return logger @pytest.fixture def fake_persistence_port() -> AsyncMock: """PersistencePort 桩。 ``PersistencePort`` 的方法为异步,使用 ``AsyncMock()``。 """ return AsyncMock()