mirror of
https://github.com/snowy-yang/qqbotsdk.git
synced 2026-10-21 12:18:20 +00:00
feat: 按钮快捷构造、event_id 语义修正与 TokenError
This commit is contained in:
1 parent
cff0c142c2
commit
097bc96d8b
16 files changed
+431
-35
No files matched your search
+66
-1
@@ -1,4 +1,9 @@
|
||||
from qqbotsdk.api import BotApi
|
||||
from aiohttp import ClientSession, ClientTimeout, web
|
||||
from aiohttp.test_utils import TestServer
|
||||
|
||||
from qqbotsdk.api import BotApi, button, keyboard
|
||||
from qqbotsdk.config import Config
|
||||
from qqbotsdk.token import AccessToken
|
||||
|
||||
|
||||
def make_api() -> tuple[BotApi, dict]:
|
||||
@@ -163,3 +168,63 @@ async def test_respond_interaction():
|
||||
method, path, kwargs = cap["call"]
|
||||
assert (method, path) == ("PUT", "/interactions/I1")
|
||||
assert kwargs["json"] == {"code": 0}
|
||||
|
||||
|
||||
def test_button_defaults_and_overrides():
|
||||
btn = button("点我", data="d1")
|
||||
assert btn["render_data"] == {"label": "点我", "style": 1}
|
||||
assert btn["action"] == {"type": 1, "permission": {"type": 2}, "data": "d1"}
|
||||
assert len(btn["id"]) == 8 # 未指定 id 时自动生成短 id
|
||||
|
||||
btn2 = button("跳转", data="https://x", type=0, permission=1, style=0, id="b2")
|
||||
assert btn2["id"] == "b2"
|
||||
assert btn2["action"]["type"] == 0
|
||||
assert btn2["action"]["permission"] == {"type": 1}
|
||||
assert btn2["render_data"]["style"] == 0
|
||||
|
||||
|
||||
def test_keyboard_rows():
|
||||
b1, b2, b3 = button("a", data="1"), button("b", data="2"), button("c", data="3")
|
||||
kb = keyboard(b1, [b2, b3])
|
||||
rows = kb["content"]["rows"]
|
||||
assert rows[0]["buttons"] == [b1] # 单按钮独占一行
|
||||
assert rows[1]["buttons"] == [b2, b3] # 列表同行并排
|
||||
assert len(rows) == 2
|
||||
|
||||
|
||||
async def test_request_tolerates_non_json_content_type():
|
||||
"""回应互动等接口 200 响应的 Content-Type 是 text/plain(body 为 JSON),
|
||||
request 不应抛 ContentTypeError(真实 HTTP 栈回归)。"""
|
||||
|
||||
async def handler(request):
|
||||
return web.Response(text="{}", content_type="text/plain")
|
||||
|
||||
app = web.Application()
|
||||
app.router.add_route("PUT", "/interactions/I1", handler)
|
||||
server = TestServer(app)
|
||||
await server.start_server()
|
||||
try:
|
||||
config = Config(
|
||||
app_id="id",
|
||||
app_secret="s",
|
||||
base_url=f"http://{server.host}:{server.port}",
|
||||
timeout=ClientTimeout(total=5),
|
||||
connecter="websocket",
|
||||
intents_file="intents.toml",
|
||||
webhook_host="0.0.0.0",
|
||||
webhook_port=8080,
|
||||
webhook_path="/qqbot/webhook",
|
||||
)
|
||||
|
||||
class FixedToken(AccessToken):
|
||||
async def get_access_token(self) -> str:
|
||||
return "test-token"
|
||||
|
||||
http = ClientSession()
|
||||
try:
|
||||
api = BotApi(config, http, FixedToken(config, http))
|
||||
assert await api.respond_interaction("I1") == {}
|
||||
finally:
|
||||
await http.close()
|
||||
finally:
|
||||
await server.close()
|
||||
@@ -0,0 +1,39 @@
|
||||
"""Event 封装属性:message_id 取自事件体 d.id,event_id 取自信封顶层 id。"""
|
||||
|
||||
from qqbotsdk.events import Event
|
||||
|
||||
|
||||
def make_event() -> Event:
|
||||
return Event(
|
||||
{
|
||||
"op": 0,
|
||||
"id": "evt-1", # 信封顶层,与 op/t/d 平级
|
||||
"t": "GROUP_ADD_ROBOT",
|
||||
"d": {"group_openid": "G1", "op_member_openid": "M1"},
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def test_event_id_from_envelope():
|
||||
event = make_event()
|
||||
assert event.event_id == "evt-1"
|
||||
assert event.group_id == "G1"
|
||||
|
||||
|
||||
def test_message_id_from_event_body():
|
||||
event = Event(
|
||||
{
|
||||
"op": 0,
|
||||
"id": "evt-2",
|
||||
"t": "GROUP_AT_MESSAGE_CREATE",
|
||||
"d": {"id": "msgid-1", "group_openid": "G1", "content": "hi"},
|
||||
}
|
||||
)
|
||||
assert event.message_id == "msgid-1" # 消息事件回复用 msg_id
|
||||
assert event.event_id == "evt-2" # 信封顶层 id 另有其值
|
||||
|
||||
|
||||
def test_event_id_defaults_to_none():
|
||||
event = Event({"op": 0, "t": "READY", "d": {}})
|
||||
assert event.event_id is None
|
||||
assert event.message_id is None
|
||||
@@ -3,6 +3,7 @@ from qqbotsdk.model import Opcode
|
||||
from qqbotsdk.payloads import (
|
||||
C2CMessage,
|
||||
GroupAtMessage,
|
||||
Interaction,
|
||||
Ready,
|
||||
parse_event,
|
||||
)
|
||||
@@ -62,6 +63,27 @@ def test_parse_non_dict_d_returns_dict():
|
||||
assert parse_event("GROUP_AT_MESSAGE_CREATE", d) == 42
|
||||
|
||||
|
||||
def test_interaction_button_data():
|
||||
inter = parse_event(
|
||||
"INTERACTION_CREATE",
|
||||
{
|
||||
"id": "i1",
|
||||
"chat_type": 1,
|
||||
"group_openid": "G1",
|
||||
"data": {"resolved": {"button_data": "btn|like"}},
|
||||
},
|
||||
)
|
||||
assert isinstance(inter, Interaction)
|
||||
assert inter.button_data == "btn|like"
|
||||
|
||||
|
||||
def test_interaction_button_data_defaults_to_empty():
|
||||
inter = Interaction(id="i2", data=None)
|
||||
assert inter.button_data == ""
|
||||
inter = Interaction(id="i3", data={"resolved": None})
|
||||
assert inter.button_data == ""
|
||||
|
||||
|
||||
async def test_typed_annotation_receives_dataclass():
|
||||
queue = EventQueue()
|
||||
emitter = EventEmitter(queue)
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
"""AccessToken:成功换取与失败时抛 TokenError(带 QQ 的 code/message)。"""
|
||||
|
||||
import pytest
|
||||
from aiohttp import ClientTimeout
|
||||
|
||||
from qqbotsdk.config import Config
|
||||
from qqbotsdk.token import AccessToken, TokenError
|
||||
|
||||
|
||||
def make_token(payload) -> AccessToken:
|
||||
class FakeResponse:
|
||||
status = 200
|
||||
|
||||
def __init__(self) -> None:
|
||||
self._payload = payload
|
||||
|
||||
async def json(self, **kwargs):
|
||||
return self._payload
|
||||
|
||||
async def __aenter__(self):
|
||||
return self
|
||||
|
||||
async def __aexit__(self, *exc):
|
||||
return False
|
||||
|
||||
class FakeSession:
|
||||
def post(self, *args, **kwargs):
|
||||
return FakeResponse()
|
||||
|
||||
config = Config(
|
||||
app_id="id",
|
||||
app_secret="s",
|
||||
base_url="https://x",
|
||||
timeout=ClientTimeout(total=5),
|
||||
connecter="websocket",
|
||||
intents_file="intents.toml",
|
||||
webhook_host="0.0.0.0",
|
||||
webhook_port=8080,
|
||||
webhook_path="/qqbot/webhook",
|
||||
)
|
||||
return AccessToken(config, FakeSession()) # type: ignore[arg-type]
|
||||
|
||||
|
||||
async def test_success_caches_token():
|
||||
token = make_token({"access_token": "t1", "expires_in": 7200})
|
||||
assert await token.get_access_token() == "t1"
|
||||
# 缓存期内直接复用,不再发请求(FakeSession 也没有第二次响应的意义)
|
||||
assert await token.get_access_token() == "t1"
|
||||
|
||||
|
||||
async def test_error_payload_raises_token_error():
|
||||
token = make_token({"code": 11253, "message": "invalid appid or secret"})
|
||||
with pytest.raises(TokenError) as exc_info:
|
||||
await token.get_access_token()
|
||||
assert exc_info.value.code == 11253
|
||||
assert exc_info.value.message == "invalid appid or secret"
|
||||
assert "invalid appid or secret" in str(exc_info.value)
|
||||
|
||||
|
||||
async def test_non_dict_payload_raises_token_error():
|
||||
token = make_token("gateway busy")
|
||||
with pytest.raises(TokenError) as exc_info:
|
||||
await token.get_access_token()
|
||||
assert exc_info.value.message == "gateway busy"
|
||||
Reference in new issue
Block a user