Files
Auto_Bangumi/examples/plugins/onebot-notifier/tests/test_onebot_notifier.py
Estrella Pan 667f769a2d fix(plugins): 处理 Codex 审查的六个问题
- apprise:urls 含令牌,改为 secret_field;secret_field 支持 default_factory,可声明列表 / 字典秘密字段
- run-command:stderr 改为自建管道边读边丢,只保留最后 2000 字节;完成仍只看 proc.wait(),结束后给读取 0.5 秒再关闭管道
- run-command:{files} 按原始路径列表展开,含换行的路径不再拆成两个参数
- 通知:插件渠道 extra 里的已声明字段与内置渠道一样展开 $VAR
- slack:只在 HTTP 400 invalid_blocks 时退回纯文字重试,其它失败不重发
- onebot:先发文字,再单独发海报;结果只看文字,去掉重试
- strm-writer:media_root 为 "/" 时正确映射完整路径
2026-10-21 11:26:23 +02:00

149 lines
5.0 KiB
Python

import asyncio
import json
from collections.abc import Callable
import httpx2
import pytest
from onebot_notifier import OneBotNotifier
from ab_sdk.notify import NotificationMessage, NotifierSettings
from ab_sdk.testing import NotifierContract, create_plugin
BASE = {"url": "http://bot:3000/", "target": 123}
POSTER = "https://ab.example.com/posters/a.jpg"
def channel(
status: int,
body: dict | str | Callable[[dict], httpx2.Response],
options: dict | None = None,
seen=None,
):
def handler(request: httpx2.Request) -> httpx2.Response:
if seen is not None:
seen.append(request)
if callable(body):
return body(json.loads(request.content))
if isinstance(body, str):
return httpx2.Response(status, text=body)
return httpx2.Response(status, json=body)
plugin, _ = create_plugin(OneBotNotifier, BASE if options is None else options)
plugin.client_factory = lambda: httpx2.AsyncClient( # type: ignore[method-assign]
transport=httpx2.MockTransport(handler)
)
return plugin.channel()(NotifierSettings())
def episode(poster_url: str | None = None) -> NotificationMessage:
return NotificationMessage(
kind="episode", title="番剧", body="番剧 第1集", poster_url=poster_url
)
class TestChannel(NotifierContract):
def create(self):
return channel(200, {"status": "ok", "retcode": 0})
def create_failing(self):
return channel(200, {"status": "failed", "retcode": 100, "wording": "不是好友"})
@pytest.mark.parametrize(
("message_type", "path", "id_key"),
[
("private", "/send_private_msg", "user_id"),
("group", "/send_group_msg", "group_id"),
],
)
def test_send_posts_to_endpoint_with_target(message_type, path, id_key):
seen: list[httpx2.Request] = []
ch = channel(200, {"retcode": 0}, {**BASE, "message_type": message_type}, seen)
assert asyncio.run(ch.send(episode())) is True
[request] = seen
assert request.url.path == path
assert json.loads(request.content) == {
id_key: 123,
"message": [{"type": "text", "data": {"text": "番剧 第1集"}}],
}
TEXT = [{"type": "text", "data": {"text": "番剧 第1集"}}]
IMAGE = [{"type": "image", "data": {"file": POSTER}}]
@pytest.mark.parametrize(
("send_poster", "poster_url", "messages"),
[
(True, POSTER, [TEXT, IMAGE]),
(False, POSTER, [TEXT]),
(True, None, [TEXT]),
],
)
def test_send_poster_goes_as_separate_message_after_text(
send_poster, poster_url, messages
):
seen: list[httpx2.Request] = []
ch = channel(200, {"status": "ok"}, {**BASE, "send_poster": send_poster}, seen)
assert asyncio.run(ch.send(episode(poster_url))) is True
assert [json.loads(r.content)["message"] for r in seen] == messages
@pytest.mark.parametrize(("token", "header"), [("t0k", "Bearer t0k"), ("", None)])
def test_send_sets_bearer_header_only_with_token(token, header):
seen: list[httpx2.Request] = []
ch = channel(200, {"status": "ok"}, {**BASE, "access_token": token}, seen)
assert asyncio.run(ch.send(episode())) is True
assert seen[0].headers.get("Authorization") == header
def test_test_reports_failure_on_http_error():
ok, detail = asyncio.run(channel(401, {"msg": "token 错误"}).test())
assert ok is False
assert "token 错误" in detail
def test_send_without_target_returns_false_without_request():
seen: list[httpx2.Request] = []
ch = channel(200, {"status": "ok"}, {"url": "http://bot:3000"}, seen)
assert asyncio.run(ch.send(episode())) is False
assert seen == []
# OneBot 实现取不到海报:图片消息返回失败,或请求本身出错
def image_rejected(body: dict) -> httpx2.Response:
if body["message"] == IMAGE:
return httpx2.Response(200, json={"status": "failed", "retcode": 1200})
return httpx2.Response(200, json={"status": "ok", "retcode": 0})
def image_unreachable(body: dict) -> httpx2.Response:
if body["message"] == IMAGE:
raise httpx2.ReadTimeout("timeout")
return httpx2.Response(200, json={"status": "ok", "retcode": 0})
@pytest.mark.parametrize("respond", [image_rejected, image_unreachable])
def test_send_image_failure_keeps_text_result_without_retry(respond):
seen: list[httpx2.Request] = []
ch = channel(200, respond, seen=seen)
assert asyncio.run(ch.send(episode(POSTER))) is True
assert [json.loads(r.content)["message"] for r in seen] == [TEXT, IMAGE]
def test_send_text_failure_skips_image_and_is_not_retried():
seen: list[httpx2.Request] = []
ch = channel(200, {"status": "failed", "retcode": 100}, seen=seen)
assert asyncio.run(ch.send(episode(POSTER))) is False
assert len(seen) == 1
@pytest.mark.parametrize(
("status", "body", "expected"),
[(502, "<html>Bad Gateway</html>", "HTTP 502"), (200, "<html>", "不是预期的 JSON")],
)
def test_test_non_json_response_reports_status(status, body, expected):
ok, detail = asyncio.run(channel(status, body).test())
assert ok is False
assert expected in detail