230 lines
7.9 KiB
Python
230 lines
7.9 KiB
Python
"""T8.3 企业微信集成测试
|
|
|
|
覆盖:
|
|
- 机器人 webhook 文本通知 payload
|
|
- 应用消息 access_token 获取/缓存与文本消息 payload
|
|
- 上游 errcode / transport 异常统一包装
|
|
- 环境变量缺失 fail-fast
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import os
|
|
import unittest
|
|
from dataclasses import dataclass
|
|
from typing import Optional
|
|
|
|
from data.channel_sync.wecom_adapter import (
|
|
WeComAPIError,
|
|
WeComAppClient,
|
|
WeComAppConfig,
|
|
WeComBotClient,
|
|
WeComBotConfig,
|
|
WeComConfigError,
|
|
)
|
|
|
|
|
|
@dataclass
|
|
class _FakeResponse:
|
|
status: int
|
|
payload: dict
|
|
|
|
|
|
class _FakeTransport:
|
|
def __init__(self, responses: list[_FakeResponse]) -> None:
|
|
self._responses = list(responses)
|
|
self.calls: list[dict] = []
|
|
|
|
def request(
|
|
self,
|
|
method: str,
|
|
url: str,
|
|
*,
|
|
headers: Optional[dict[str, str]] = None,
|
|
body: Optional[bytes] = None,
|
|
timeout: float = 0,
|
|
) -> tuple[int, bytes]:
|
|
self.calls.append(
|
|
{
|
|
"method": method,
|
|
"url": url,
|
|
"headers": headers or {},
|
|
"body": body,
|
|
"timeout": timeout,
|
|
}
|
|
)
|
|
if not self._responses:
|
|
raise AssertionError("no fake response left")
|
|
response = self._responses.pop(0)
|
|
return response.status, json.dumps(response.payload).encode("utf-8")
|
|
|
|
|
|
class _ExplodingTransport:
|
|
def request(
|
|
self,
|
|
method: str,
|
|
url: str,
|
|
*,
|
|
headers: Optional[dict[str, str]] = None,
|
|
body: Optional[bytes] = None,
|
|
timeout: float = 0,
|
|
) -> tuple[int, bytes]:
|
|
raise TimeoutError("network down")
|
|
|
|
|
|
class WeComBotClientTests(unittest.TestCase):
|
|
def test_send_text_posts_robot_webhook_payload(self) -> None:
|
|
transport = _FakeTransport([_FakeResponse(200, {"errcode": 0, "errmsg": "ok"})])
|
|
client = WeComBotClient(
|
|
config=WeComBotConfig(webhook_key="robot-key", timeout_seconds=3.0),
|
|
transport=transport,
|
|
)
|
|
|
|
result = client.send_text(
|
|
content="新订单已支付,请及时跟进",
|
|
mentioned_list=["@all"],
|
|
mentioned_mobile_list=["13800138000"],
|
|
)
|
|
|
|
self.assertEqual(result["errmsg"], "ok")
|
|
self.assertEqual(len(transport.calls), 1)
|
|
self.assertIn("/cgi-bin/webhook/send?key=robot-key", transport.calls[0]["url"])
|
|
payload = json.loads(transport.calls[0]["body"].decode("utf-8"))
|
|
self.assertEqual(payload["msgtype"], "text")
|
|
self.assertEqual(payload["text"]["content"], "新订单已支付,请及时跟进")
|
|
self.assertEqual(payload["text"]["mentioned_list"], ["@all"])
|
|
self.assertEqual(payload["text"]["mentioned_mobile_list"], ["13800138000"])
|
|
|
|
def test_transport_error_wrapped_as_wecom_api_error(self) -> None:
|
|
client = WeComBotClient(
|
|
config=WeComBotConfig(webhook_key="robot-key"),
|
|
transport=_ExplodingTransport(),
|
|
)
|
|
|
|
with self.assertRaises(WeComAPIError) as cm:
|
|
client.send_text(content="hello")
|
|
self.assertEqual(cm.exception.endpoint, "/cgi-bin/webhook/send")
|
|
self.assertIn("transport_error", str(cm.exception))
|
|
|
|
def test_from_env_missing_webhook_key_fails_fast(self) -> None:
|
|
old = os.environ.get("GAOKAO_WECOM_BOT_KEY")
|
|
try:
|
|
os.environ.pop("GAOKAO_WECOM_BOT_KEY", None)
|
|
with self.assertRaises(WeComConfigError):
|
|
WeComBotClient.from_env()
|
|
finally:
|
|
if old is None:
|
|
os.environ.pop("GAOKAO_WECOM_BOT_KEY", None)
|
|
else:
|
|
os.environ["GAOKAO_WECOM_BOT_KEY"] = old
|
|
|
|
|
|
class WeComAppClientTests(unittest.TestCase):
|
|
def test_access_token_cached_until_expiry(self) -> None:
|
|
now = [1000.0]
|
|
transport = _FakeTransport(
|
|
[
|
|
_FakeResponse(200, {"access_token": "token-1", "expires_in": 7200}),
|
|
_FakeResponse(200, {"errcode": 0, "errmsg": "ok", "invaliduser": ""}),
|
|
_FakeResponse(200, {"errcode": 0, "errmsg": "ok", "invaliduser": ""}),
|
|
]
|
|
)
|
|
client = WeComAppClient(
|
|
config=WeComAppConfig(
|
|
corp_id="corp-id",
|
|
corp_secret="corp-secret",
|
|
agent_id=1000002,
|
|
timeout_seconds=3.0,
|
|
token_refresh_skew_seconds=60.0,
|
|
),
|
|
transport=transport,
|
|
clock=lambda: now[0],
|
|
)
|
|
|
|
client.send_text(to_user="zhangsan", content="第一条")
|
|
now[0] += 300
|
|
client.send_text(to_party="party-1", content="第二条")
|
|
|
|
token_fetches = [
|
|
call for call in transport.calls if "/cgi-bin/gettoken?" in call["url"]
|
|
]
|
|
self.assertEqual(len(token_fetches), 1)
|
|
payload = json.loads(transport.calls[-1]["body"].decode("utf-8"))
|
|
self.assertEqual(payload["msgtype"], "text")
|
|
self.assertEqual(payload["agentid"], 1000002)
|
|
self.assertEqual(payload["toparty"], "party-1")
|
|
self.assertEqual(payload["text"]["content"], "第二条")
|
|
|
|
def test_send_text_supports_duplicate_check_and_safe(self) -> None:
|
|
transport = _FakeTransport(
|
|
[
|
|
_FakeResponse(200, {"access_token": "token-1", "expires_in": 7200}),
|
|
_FakeResponse(200, {"errcode": 0, "errmsg": "ok", "invaliduser": ""}),
|
|
]
|
|
)
|
|
client = WeComAppClient(
|
|
config=WeComAppConfig(
|
|
corp_id="corp-id", corp_secret="corp-secret", agent_id=42
|
|
),
|
|
transport=transport,
|
|
)
|
|
|
|
result = client.send_text(
|
|
to_user="lisi",
|
|
content="请尽快联系家长",
|
|
safe=1,
|
|
enable_duplicate_check=1,
|
|
duplicate_check_interval=600,
|
|
)
|
|
|
|
self.assertEqual(result["errmsg"], "ok")
|
|
payload = json.loads(transport.calls[-1]["body"].decode("utf-8"))
|
|
self.assertEqual(payload["touser"], "lisi")
|
|
self.assertEqual(payload["safe"], 1)
|
|
self.assertEqual(payload["enable_duplicate_check"], 1)
|
|
self.assertEqual(payload["duplicate_check_interval"], 600)
|
|
|
|
def test_upstream_errcode_raises_structured_error(self) -> None:
|
|
transport = _FakeTransport(
|
|
[
|
|
_FakeResponse(200, {"access_token": "token-1", "expires_in": 7200}),
|
|
_FakeResponse(200, {"errcode": 81013, "errmsg": "invalid userid"}),
|
|
]
|
|
)
|
|
client = WeComAppClient(
|
|
config=WeComAppConfig(
|
|
corp_id="corp-id", corp_secret="corp-secret", agent_id=42
|
|
),
|
|
transport=transport,
|
|
)
|
|
|
|
with self.assertRaises(WeComAPIError) as cm:
|
|
client.send_text(to_user="bad-user", content="hello")
|
|
self.assertEqual(cm.exception.errcode, 81013)
|
|
self.assertIn("invalid userid", str(cm.exception))
|
|
|
|
def test_from_env_missing_secret_fails_fast(self) -> None:
|
|
old_id = os.environ.get("GAOKAO_WECOM_CORP_ID")
|
|
old_secret = os.environ.get("GAOKAO_WECOM_CORP_SECRET")
|
|
old_agent = os.environ.get("GAOKAO_WECOM_AGENT_ID")
|
|
try:
|
|
os.environ["GAOKAO_WECOM_CORP_ID"] = "corp-id"
|
|
os.environ.pop("GAOKAO_WECOM_CORP_SECRET", None)
|
|
os.environ["GAOKAO_WECOM_AGENT_ID"] = "42"
|
|
with self.assertRaises(WeComConfigError):
|
|
WeComAppClient.from_env()
|
|
finally:
|
|
if old_id is None:
|
|
os.environ.pop("GAOKAO_WECOM_CORP_ID", None)
|
|
else:
|
|
os.environ["GAOKAO_WECOM_CORP_ID"] = old_id
|
|
if old_secret is None:
|
|
os.environ.pop("GAOKAO_WECOM_CORP_SECRET", None)
|
|
else:
|
|
os.environ["GAOKAO_WECOM_CORP_SECRET"] = old_secret
|
|
if old_agent is None:
|
|
os.environ.pop("GAOKAO_WECOM_AGENT_ID", None)
|
|
else:
|
|
os.environ["GAOKAO_WECOM_AGENT_ID"] = old_agent
|