mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-10-28 19:06:36 +08:00
162 lines
5.1 KiB
Python
162 lines
5.1 KiB
Python
"""
|
|
系统内部用户消息分发与异步立即发送的上下文传播测试。
|
|
"""
|
|
|
|
# 可选三方模块必须在导入消息链前完成隔离。
|
|
# ruff: noqa: E402
|
|
|
|
import asyncio
|
|
from unittest.mock import patch
|
|
|
|
import pytest
|
|
|
|
from app.testing.bootstrap import ensure_optional_stub
|
|
|
|
# 仅在可选依赖缺失时补占位;已安装时保留真实模块,避免污染同一进程里导入真实类型的用例。
|
|
ensure_optional_stub("qbittorrentapi", TorrentFilesList=list)
|
|
ensure_optional_stub("transmission_rpc", File=object)
|
|
ensure_optional_stub("psutil")
|
|
|
|
from app.application.messaging.message import MessageQueueManager
|
|
from app.chain.message import MessageChain
|
|
from app.foundation.identity import (
|
|
SYSTEM_INTERNAL_USER_ID,
|
|
is_internal_user_id,
|
|
normalize_internal_user_id,
|
|
)
|
|
from app.runtime.correlation import correlation_scope, get_correlation_id
|
|
from app.schemas.message import Message
|
|
|
|
|
|
def _internal_message() -> Message:
|
|
"""
|
|
构造以系统内部用户为收件人的消息。
|
|
"""
|
|
return Message(
|
|
userid=SYSTEM_INTERNAL_USER_ID,
|
|
username="admin",
|
|
title="后台报告",
|
|
text="任务完成",
|
|
)
|
|
|
|
|
|
def test_internal_userid_identity_helpers() -> None:
|
|
"""
|
|
系统内部用户标识的判定与归一化。
|
|
"""
|
|
assert is_internal_user_id(SYSTEM_INTERNAL_USER_ID)
|
|
assert is_internal_user_id(" System ")
|
|
assert normalize_internal_user_id(SYSTEM_INTERNAL_USER_ID) is None
|
|
assert normalize_internal_user_id("10001") == "10001"
|
|
|
|
|
|
def test_post_message_normalizes_internal_userid_before_queueing() -> None:
|
|
"""
|
|
入队前应把系统内部用户归一化为空,不进入事件与队列载荷。
|
|
"""
|
|
chain = MessageChain()
|
|
message = _internal_message()
|
|
|
|
with patch("app.chain._messaging.MessageTemplateHelper.render", return_value=message), patch.object(
|
|
chain.messagehelper, "put"
|
|
), patch.object(chain.messageoper, "add"), patch.object(
|
|
chain.eventmanager, "send_event"
|
|
) as send_event, patch.object(
|
|
chain.user_repository,
|
|
"get_notification_settings",
|
|
return_value={"telegram_userid": "admin-1"},
|
|
), patch.object(
|
|
chain.messagequeue, "send_message"
|
|
) as send_message:
|
|
chain.post_message(message)
|
|
|
|
event_payload = send_event.call_args.kwargs["data"]
|
|
queued_message = send_message.call_args.kwargs["message"]
|
|
|
|
assert event_payload["userid"] is None
|
|
assert queued_message.userid is None
|
|
assert send_message.call_args.kwargs["immediately"] is False
|
|
|
|
|
|
def test_send_direct_message_normalizes_internal_userid() -> None:
|
|
"""
|
|
直接发送时同样归一化系统内部用户。
|
|
"""
|
|
chain = MessageChain()
|
|
|
|
with patch.object(chain, "run_module") as run_module:
|
|
chain.send_direct_message(_internal_message())
|
|
|
|
sent_message = run_module.call_args.kwargs["message"]
|
|
assert sent_message.userid is None
|
|
|
|
|
|
class _FakeLoop:
|
|
"""
|
|
同步模拟 asyncio 的 executor 提交,并记录提交时的上下文。
|
|
"""
|
|
|
|
def __init__(self, loop):
|
|
self.called = False
|
|
self.loop = loop
|
|
self.submission_correlation_id = None
|
|
|
|
def run_in_executor(self, _executor, func, *args):
|
|
"""
|
|
立即执行 worker 调用并返回已完成 Future。
|
|
"""
|
|
self.called = True
|
|
self.submission_correlation_id = get_correlation_id()
|
|
future = self.loop.create_future()
|
|
try:
|
|
func(*args)
|
|
# 捕获工作线程中的全部失败并回传主测试进行断言。
|
|
except BaseException as error: # pylint: disable=broad-exception-caught
|
|
future.set_exception(error)
|
|
else:
|
|
future.set_result(None)
|
|
return future
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_async_send_message_uses_executor_for_immediate_send() -> None:
|
|
"""
|
|
异步立即发送不能在事件循环里直接执行同步渠道回调。
|
|
"""
|
|
manager = MessageQueueManager()
|
|
fake_loop = _FakeLoop(asyncio.get_running_loop())
|
|
observed = []
|
|
with patch(
|
|
"asyncio.get_running_loop",
|
|
return_value=fake_loop,
|
|
), patch.object(
|
|
manager,
|
|
"_send",
|
|
side_effect=lambda *_args, **_kwargs: observed.append(get_correlation_id()),
|
|
) as send:
|
|
with correlation_scope("message-request"):
|
|
await manager.async_send_message("payload", immediately=True)
|
|
|
|
assert fake_loop.called
|
|
assert fake_loop.submission_correlation_id is None
|
|
assert observed == ["message-request"]
|
|
send.assert_called_once_with("payload")
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_async_send_message_preserves_call_context() -> None:
|
|
"""
|
|
异步立即发送的同步渠道回调应保留当前请求关联 ID。
|
|
"""
|
|
observed = []
|
|
manager = MessageQueueManager()
|
|
with patch.object(
|
|
manager,
|
|
"_send",
|
|
side_effect=lambda *_args, **_kwargs: observed.append(get_correlation_id()),
|
|
):
|
|
with correlation_scope("message-request"):
|
|
await manager.async_send_message("payload", immediately=True)
|
|
|
|
assert observed == ["message-request"]
|