mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-10-30 20:05:42 +08:00
严格检查全部宿主源码,清零类型错误预算并移除按模块类型豁免。 按原因、所属方法、错误码及语法指纹锁定必要兼容例外和站点类型桩, CI 阻断新增忽略、检查范围退化、本地桩替代真实接口及新的类型错误。 修复空值、异步和供应方类型边界,保持插件与 API 兼容合同, 并补充存储、订阅、通知、Agent 和工作流边界的回归覆盖。
136 lines
6.5 KiB
Python
136 lines
6.5 KiB
Python
"""
|
|
验证通知渠道对缺失资源、附件类型及可选实例的兼容处理。
|
|
"""
|
|
|
|
from types import SimpleNamespace
|
|
from unittest.mock import Mock
|
|
|
|
import pytest
|
|
|
|
from app.domain.context import Context, MediaInfo, TorrentInfo
|
|
from app.modules.dingtalk import DingTalkModule
|
|
from app.modules.discord import DiscordModule
|
|
from app.modules.feishu.module import FeishuModule
|
|
from app.modules.qqbot.module import QQBotModule
|
|
from app.modules.qqbot.qqbot import QQBot
|
|
from app.modules.slack.slack import Slack
|
|
from app.modules.synologychat.synologychat import SynologyChat
|
|
from app.modules.vocechat import VoceChatModule
|
|
from app.modules.vocechat.vocechat import VoceChat
|
|
from app.modules.wechat import WechatModule
|
|
from app.modules.wechat.wechatbot import WeChatBot
|
|
from app.modules.wechatclawbot import WechatClawBotModule
|
|
from app.schemas.message import Message, MessageResponse
|
|
from app.schemas.types import NotificationChannel
|
|
|
|
|
|
@pytest.mark.parametrize("client_class", [QQBot, SynologyChat, VoceChat, WeChatBot, Slack])
|
|
def test_torrent_notifications_skip_missing_torrent_and_preserve_valid_items(client_class, monkeypatch):
|
|
"""
|
|
列表中的缺失资源不阻断有效候选发送,也不会把空上下文渲染为一条种子。
|
|
"""
|
|
client = object.__new__(client_class)
|
|
sent = Mock(return_value=True)
|
|
if client_class is QQBot:
|
|
monkeypatch.setattr(client, "send_msg", sent)
|
|
elif client_class is SynologyChat:
|
|
client._webhook_url = "https://chat.test"
|
|
client._token = "test"
|
|
monkeypatch.setattr(client, "_SynologyChat__send_request", sent)
|
|
elif client_class is VoceChat:
|
|
client._client = Mock()
|
|
monkeypatch.setattr(client, "_VoceChat__send_request", sent)
|
|
elif client_class is WeChatBot:
|
|
monkeypatch.setattr(client, "_send_markdown", sent)
|
|
else:
|
|
client._client = SimpleNamespace(chat_postMessage=sent)
|
|
valid = Context(torrent_info=TorrentInfo(title="Demo.2026.1080p", site_name="ExampleSite", size=1024, seeders=3))
|
|
assert client.send_torrents_msg([Context(), valid], userid="1", title="候选") is True
|
|
assert sent.call_count == 1
|
|
assert "ExampleSite" in str(sent.call_args)
|
|
assert str(sent.call_args).count("ExampleSite") == 1
|
|
|
|
|
|
def test_dingtalk_missing_torrent_does_not_hide_later_candidate(monkeypatch):
|
|
"""
|
|
钉钉 Markdown 列表跳过空资源后继续展示可点击的有效候选。
|
|
"""
|
|
module = object.__new__(DingTalkModule)
|
|
sent = Mock()
|
|
monkeypatch.setattr(module, "_post_list_message", sent)
|
|
message = Message(title="候选")
|
|
module.post_torrents_message(message, [Context(), Context(torrent_info=TorrentInfo(
|
|
title="Demo", page_url="https://site.test/details/1",
|
|
))])
|
|
sent.assert_called_once_with(message, "2. [Demo](https://site.test/details/1)")
|
|
|
|
|
|
@pytest.mark.parametrize(("module_class", "scheme", "method", "module_path"), [
|
|
(DiscordModule, "discord", "download_discord_file_bytes", "app.modules.discord"),
|
|
(QQBotModule, "qq", "download_qq_file_bytes", "app.modules.qqbot.module"),
|
|
])
|
|
@pytest.mark.parametrize("payload", [b"attachment", "invalid text", None])
|
|
def test_attachment_downloads_require_bytes(module_class, scheme, method, module_path, payload, monkeypatch):
|
|
"""
|
|
附件传输仅返回原始字节,错误文本或空响应不会进入语音与文件处理流程。
|
|
"""
|
|
module = object.__new__(module_class)
|
|
monkeypatch.setattr(module, "get_config", Mock(return_value=SimpleNamespace(name="source")))
|
|
request = SimpleNamespace(get_res=Mock(return_value=SimpleNamespace(content=payload)))
|
|
monkeypatch.setattr(f"{module_path}.RequestUtils", Mock(return_value=request))
|
|
result = getattr(module, method)(f"{scheme}://file/https://cdn.test/file", "source")
|
|
assert result == (payload if isinstance(payload, bytes) else None)
|
|
request.get_res.assert_called_once_with("https://cdn.test/file")
|
|
|
|
|
|
@pytest.mark.parametrize("reference", ["wxwork://media_id/image", "wxbot://image/image"])
|
|
@pytest.mark.parametrize("payload", ["data:image/png;base64,c2FmZQ==", b"invalid"])
|
|
def test_wechat_images_only_expose_data_url_text(reference, payload, monkeypatch):
|
|
"""
|
|
应用与机器人图片下载结果必须是文本地址,字节结果不能作为 data URL 外泄。
|
|
"""
|
|
module = object.__new__(WechatModule)
|
|
client = SimpleNamespace(download_media_to_data_url=Mock(return_value=payload), download_image_to_data_url=Mock(return_value=payload))
|
|
monkeypatch.setattr(module, "get_config", Mock(return_value=SimpleNamespace(name="source")))
|
|
monkeypatch.setattr(module, "get_instance", Mock(return_value=client))
|
|
assert module.download_wechat_image_to_data_url(reference, "source") == (payload if isinstance(payload, str) else None)
|
|
assert module._commands_enabled(None) is False
|
|
|
|
|
|
@pytest.mark.parametrize("module_class", [VoceChatModule, WechatClawBotModule])
|
|
def test_notification_without_active_client_does_not_send(module_class, monkeypatch):
|
|
"""
|
|
配置存在而客户端尚未激活时,列表通知停止发送,不访问可选实例属性。
|
|
"""
|
|
module = object.__new__(module_class)
|
|
monkeypatch.setattr(module, "get_configs", Mock(return_value={"source": SimpleNamespace(name="source")}))
|
|
monkeypatch.setattr(module, "check_message", Mock(return_value=True))
|
|
instance = Mock(return_value=None)
|
|
monkeypatch.setattr(module, "get_instance", instance)
|
|
module.post_medias_message(Message(title="媒体"), [MediaInfo(title="Demo")])
|
|
assert instance.call_count == 1
|
|
|
|
|
|
def test_feishu_invalid_stream_state_does_not_finalize_card(monkeypatch):
|
|
"""
|
|
非字典流式卡片状态不能被当作可关闭的消息,避免发送错误的卡片 ID。
|
|
"""
|
|
module = object.__new__(FeishuModule)
|
|
module._channel = NotificationChannel.Feishu
|
|
client = Mock()
|
|
monkeypatch.setattr(module, "get_instance", client)
|
|
response = MessageResponse(channel=NotificationChannel.Feishu, metadata={"feishu_streaming": ["invalid"]})
|
|
assert module.finalize_message(response) is False
|
|
client.assert_not_called()
|
|
|
|
|
|
@pytest.mark.parametrize(("manifest", "expected"), [("{\"features\": {}}", {"features": {}}), ([], None)])
|
|
def test_slack_manifest_export_requires_object(manifest, expected):
|
|
"""
|
|
Slack 清单支持 JSON 文本与对象,错误集合类型不会进入命令更新流程。
|
|
"""
|
|
client = object.__new__(Slack)
|
|
client._app_id = "app"
|
|
client._manifest_client = SimpleNamespace(apps_manifest_export=Mock(return_value={"ok": True, "manifest": manifest}))
|
|
assert client._export_manifest() == expected
|