mirror of
https://github.com/snowy-yang/qqbotsdk.git
synced 2026-10-21 15:54:48 +00:00
BaseProtocol 从 emitter.py 移入新模块 protocol.py,接口由 register(emitter) 改为 async on_frame(payload)(返回需回送的响应体);协议处理器不再注册进 emitter,而由 run_loop 注入对应的 connecter,由适配器逐帧调用。 破坏性变更:只有 op=0 的业务事件进事件队列交给 EventEmitter 分发, HELLO/READY/INVALID_SESSION/op=13 等协议帧在连接器内就地处理——用户 handler 不再收到协议帧(此前 HELLO/INVALID_SESSION 等会路由到 handler)。 EventEmitter.register_protocol()/handle() 与 emitter 持有协议层的机制删除, 协议类不再用 register() 注册 handler。 附带修掉一处时序耦合:INVALID_SESSION 的会话重置此前依赖分发泵在重连前 消费掉该事件(慢 handler 会推迟它,导致带着已死 session_id 去 RESUME), 现在连接器在 break 重连前同步完成重置。 同时做的事件流优化: - emit() 先取事件名、无 listener 时不构造 Event(未订阅事件省一次对象与拷贝) - Event.data 改为惰性拷贝(只用 .typed/.raw 的 handler 不再触发 dict(d)) - ws 重连收尾 await 被取消的 reply/heartbeat 任务,避免异常无人取用 - EventQueue.put_event/put_reply 去掉多余的 return await 测试 80 全绿(新增协议帧处理测试组、ws 集成断言协议帧不触达 handler), ruff/pyright 干净。文档 ARCHITECTURE/API/GUIDE/README/CHANGELOG 同步。
123 lines
3.9 KiB
Python
123 lines
3.9 KiB
Python
"""Webhook 接入的端到端集成测试:真实起 WebhookConnecter 的 http server,
|
||
覆盖 op=13 验证应答、合法签名放行、坏签名 401、按 id 去重。"""
|
||
|
||
import asyncio
|
||
import json
|
||
from typing import Any
|
||
|
||
import pytest
|
||
from aiohttp.test_utils import TestClient, TestServer
|
||
from cryptography.hazmat.primitives.asymmetric import ed25519
|
||
|
||
from qqbotsdk.config import Config
|
||
from qqbotsdk.connecter import WebhookConnecter
|
||
from qqbotsdk.crypto import build_seed
|
||
from qqbotsdk.emitter import EventEmitter
|
||
from qqbotsdk.payloads import GroupAtMessage
|
||
from qqbotsdk.webhook_protocol import WebhookProtocol
|
||
|
||
SECRET = "webhook-test-secret"
|
||
|
||
|
||
def make_config(port: int) -> Config:
|
||
return Config(
|
||
app_id="app",
|
||
app_secret=SECRET,
|
||
base_url="https://api.bot.qq.com",
|
||
timeout=None, # type: ignore[arg-type]
|
||
connecter="webhook",
|
||
intents_file="no-such-intents.toml",
|
||
webhook_host="127.0.0.1",
|
||
webhook_port=port,
|
||
webhook_path="/qqbot/webhook",
|
||
)
|
||
|
||
|
||
def sign(secret: str, timestamp: str, body: bytes) -> str:
|
||
key = ed25519.Ed25519PrivateKey.from_private_bytes(build_seed(secret))
|
||
return key.sign(timestamp.encode() + body).hex()
|
||
|
||
|
||
def event_body(event_id: str, content: str) -> dict[str, Any]:
|
||
return {
|
||
"op": 0,
|
||
"id": event_id,
|
||
"t": "GROUP_AT_MESSAGE_CREATE",
|
||
"d": {"content": content, "group_openid": "G1"},
|
||
}
|
||
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_webhook_end_to_end():
|
||
config = make_config(0)
|
||
emitter = EventEmitter()
|
||
connecter = WebhookConnecter(config, emitter.queue, WebhookProtocol(config))
|
||
|
||
received: list[GroupAtMessage] = []
|
||
|
||
@emitter.on("GROUP_AT_MESSAGE_CREATE")
|
||
async def on_msg(msg: GroupAtMessage) -> None:
|
||
received.append(msg)
|
||
|
||
dispatch_task = asyncio.create_task(emitter.dispatch())
|
||
server = TestServer(connecter._make_app())
|
||
client = TestClient(server)
|
||
await client.start_server()
|
||
try:
|
||
# 1. op=13 验证请求(不走验签),应答 plain_token + 对 event_ts+plain_token 的签名
|
||
resp = await client.post(
|
||
"/qqbot/webhook",
|
||
json={"op": 13, "d": {"plain_token": "pt", "event_ts": "1726000000"}},
|
||
)
|
||
assert resp.status == 200
|
||
ret = await resp.json()
|
||
assert ret["plain_token"] == "pt"
|
||
assert sign(SECRET, "1726000000", b"pt") == ret["signature"]
|
||
|
||
# 2. 合法签名的事件推送 → 放行 → handler 收到 dataclass
|
||
body = json.dumps(event_body("evt-1", "hello")).encode()
|
||
resp = await client.post(
|
||
"/qqbot/webhook",
|
||
data=body,
|
||
headers={
|
||
"X-Signature-Timestamp": "1726000001",
|
||
"X-Signature-Ed25519": sign(SECRET, "1726000001", body),
|
||
},
|
||
)
|
||
assert resp.status == 200
|
||
assert await resp.json() == {"opcode": 12}
|
||
for _ in range(50):
|
||
if received:
|
||
break
|
||
await asyncio.sleep(0.02)
|
||
assert len(received) == 1
|
||
assert received[0].content == "hello"
|
||
|
||
# 3. 坏签名 → 401,事件不入队
|
||
resp = await client.post(
|
||
"/qqbot/webhook",
|
||
data=body,
|
||
headers={
|
||
"X-Signature-Timestamp": "1726000002",
|
||
"X-Signature-Ed25519": "00" * 64,
|
||
},
|
||
)
|
||
assert resp.status == 401
|
||
|
||
# 4. 同 id 重复推送 → 去重跳过,handler 不再收到
|
||
resp = await client.post(
|
||
"/qqbot/webhook",
|
||
data=body,
|
||
headers={
|
||
"X-Signature-Timestamp": "1726000001",
|
||
"X-Signature-Ed25519": sign(SECRET, "1726000001", body),
|
||
},
|
||
)
|
||
assert resp.status == 200
|
||
await asyncio.sleep(0.1)
|
||
assert len(received) == 1
|
||
finally:
|
||
await client.close()
|
||
dispatch_task.cancel()
|
||
await asyncio.gather(dispatch_task, return_exceptions=True)
|