Files
MoviePilot/tests/test_server_helper.py
2026-08-23 02:29:40 +08:00

260 lines
11 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

from __future__ import annotations
import unittest
from unittest.mock import AsyncMock, Mock, patch
from app.adapters.external.server import (
MoviePilotServerHelper,
configure_server_application_services,
)
from app.application.server.report import ServerReportService
from app.application.server.share import ServerSharingService
from app.runtime.config import settings
from app.schemas.types import MediaSource
class MoviePilotServerHelperTests(unittest.TestCase):
"""
MoviePilot 服务端请求辅助工具测试。
"""
def setUp(self) -> None:
"""
清理安装用户 ID 缓存,避免不同用例之间互相影响。
"""
MoviePilotServerHelper._user_uid = None
configure_server_application_services(
report_service=ServerReportService(
config_reader=Mock(return_value=None),
config_writer=Mock(),
installed_plugins_provider=Mock(return_value=[]),
subscribes_provider=Mock(return_value=[]),
async_subscribes_provider=AsyncMock(return_value=[]),
plugin_report_sender=Mock(),
async_plugin_report_sender=AsyncMock(),
subscribe_report_sender=Mock(),
async_subscribe_report_sender=AsyncMock(),
async_config_writer=AsyncMock(),
repo_url_sanitizer=MoviePilotServerHelper.sanitize_plugin_repo_url,
),
sharing_service=ServerSharingService(
subscribe_provider=Mock(return_value=None),
async_subscribe_provider=AsyncMock(return_value=None),
workflow_provider=Mock(return_value=None),
async_workflow_provider=AsyncMock(return_value=None),
user_uuid_provider=Mock(return_value="user-1"),
subscribe_sender=Mock(),
async_subscribe_sender=AsyncMock(),
workflow_sender=Mock(),
async_workflow_sender=AsyncMock(),
response_handler=Mock(return_value=(True, "")),
subscribe_cache_clearer=Mock(),
workflow_cache_clearer=Mock(),
),
)
def test_server_request_adds_user_uid_header(self):
"""
发往 MoviePilot 服务端的请求会自动携带安装用户 ID。
"""
with patch.object(MoviePilotServerHelper, "get_user_uid", return_value="uid-1"), \
patch("app.adapters.external.server.settings.MP_SERVER_HOST", "https://movie-pilot.org"):
headers = MoviePilotServerHelper.build_headers(
"https://movie-pilot.org/plugin/install",
headers={"Content-Type": "application/json"},
)
self.assertEqual(headers["X-MoviePilot-User-Uid"], "uid-1")
self.assertEqual(headers["Content-Type"], "application/json")
def test_non_server_request_does_not_add_user_uid_header(self):
"""
发往其他域名的请求不会携带安装用户 ID。
"""
with patch.object(MoviePilotServerHelper, "get_user_uid", return_value="uid-1"), \
patch("app.adapters.external.server.settings.MP_SERVER_HOST", "https://movie-pilot.org"):
headers = MoviePilotServerHelper.build_headers(
"https://example.com/plugin/install",
headers={"Content-Type": "application/json"},
)
self.assertNotIn("X-MoviePilot-User-Uid", headers)
def test_existing_user_uid_header_is_preserved(self):
"""
调用方显式传入的安装用户 ID 请求头不被覆盖。
"""
with patch.object(MoviePilotServerHelper, "get_user_uid", return_value="uid-1"), \
patch("app.adapters.external.server.settings.MP_SERVER_HOST", "https://movie-pilot.org"):
headers = MoviePilotServerHelper.build_headers(
"https://movie-pilot.org/plugin/install",
headers={
"Content-Type": "application/json",
"X-MoviePilot-User-Uid": "custom-uid",
},
)
self.assertEqual(headers["X-MoviePilot-User-Uid"], "custom-uid")
def test_existing_user_uid_header_is_detected_case_insensitively(self):
"""
调用方使用不同大小写的安装用户 ID 请求头时不会重复注入。
"""
with patch.object(MoviePilotServerHelper, "get_user_uid", return_value="uid-1"), \
patch("app.adapters.external.server.settings.MP_SERVER_HOST", "https://movie-pilot.org"):
headers = MoviePilotServerHelper.build_headers(
"https://movie-pilot.org/plugin/install",
headers={
"Content-Type": "application/json",
"x-moviepilot-user-uid": "custom-uid",
},
)
self.assertNotIn("X-MoviePilot-User-Uid", headers)
self.assertEqual(headers["x-moviepilot-user-uid"], "custom-uid")
def test_content_type_can_be_added(self):
"""
构建 JSON 请求头时会补充 Content-Type。
"""
with patch.object(MoviePilotServerHelper, "get_user_uid", return_value="uid-1"), \
patch("app.adapters.external.server.settings.MP_SERVER_HOST", "https://movie-pilot.org"):
headers = MoviePilotServerHelper.build_headers(
"https://movie-pilot.org/plugin/install",
content_type="application/json",
)
self.assertEqual(headers["Content-Type"], "application/json")
def test_subscribe_fork_uses_fork_endpoint(self):
"""
订阅复用请求使用服务端 fork 接口。
"""
with patch("app.adapters.external.server.settings.MP_SERVER_HOST", "https://movie-pilot.org"), \
patch.object(MoviePilotServerHelper, "_get", return_value=None) as request:
MoviePilotServerHelper.subscribe_fork(9)
request.assert_called_once_with(
"https://movie-pilot.org/subscribe/fork/9",
timeout=5,
)
def test_workflow_fork_uses_fork_endpoint(self):
"""
工作流复用请求使用服务端 fork 接口。
"""
with patch("app.adapters.external.server.settings.MP_SERVER_HOST", "https://movie-pilot.org"), \
patch.object(MoviePilotServerHelper, "_get", return_value=None) as request:
MoviePilotServerHelper.workflow_fork(9)
request.assert_called_once_with(
"https://movie-pilot.org/workflow/fork/9",
timeout=5,
)
def test_user_permissions_uses_server_endpoint(self):
"""
用户权限请求使用服务端权限接口。
"""
with patch("app.adapters.external.server.settings.MP_SERVER_HOST", "https://movie-pilot.org"), \
patch.object(MoviePilotServerHelper, "_get", return_value=None) as request:
MoviePilotServerHelper.user_permissions("jxxghp")
request.assert_called_once_with(
"https://movie-pilot.org/user/permissions",
params={"github_user": "jxxghp"},
include_user_uid=False,
timeout=5,
)
def test_is_admin_user_uses_server_permissions(self):
"""
共享管理权限由服务端权限结果决定。
"""
response = Mock(status_code=200)
response.json.return_value = {"is_admin": True}
with patch.object(MoviePilotServerHelper, "get_github_user", return_value="jxxghp"), \
patch.object(MoviePilotServerHelper, "user_permissions", return_value=response):
self.assertTrue(MoviePilotServerHelper.is_admin_user())
def test_is_admin_user_returns_false_without_server_permission(self):
"""
服务端未返回管理权限时不授予共享管理权限。
"""
response = Mock(status_code=200)
response.json.return_value = {"is_admin": False}
with patch.object(MoviePilotServerHelper, "get_github_user", return_value="user"), \
patch.object(MoviePilotServerHelper, "user_permissions", return_value=response):
self.assertFalse(MoviePilotServerHelper.is_admin_user())
def test_subscribe_statistic_payload_only_keeps_server_contract(self):
"""订阅统计载荷应删除本地运行列和所有旧专用媒体 ID。"""
payload = MoviePilotServerHelper._build_subscribe_statistic_payload({
"id": 1,
"name": "Test",
"type": "电影",
"media_source": MediaSource.Douban,
"media_id": 42,
"tmdbid": 99,
"state": "N",
"username": "tester",
})
self.assertEqual(payload, {
"name": "Test",
"type": "电影",
"media_source": MediaSource.Douban.value,
"media_id": "42",
})
def test_subscribe_share_payload_only_keeps_server_contract(self):
"""订阅分享载荷应保留分享配置并剔除本地下载状态。"""
payload = MoviePilotServerHelper._build_subscribe_share_payload({
"share_title": "Share",
"share_user": "tester",
"name": "Test",
"type": "电视剧",
"media_source": "bangumi",
"media_id": "7",
"include": "WEB-DL",
"audio_quality": "lossless",
"downloader": "default",
"bangumiid": 7,
})
self.assertEqual(payload, {
"share_title": "Share",
"share_user": "tester",
"name": "Test",
"type": "电视剧",
"media_source": MediaSource.Bangumi.value,
"media_id": "7",
"include": "WEB-DL",
})
def test_subscribe_payload_rejects_incomplete_unified_identity(self):
"""中心服务载荷不得再从旧专用 ID 推导主身份。"""
self.assertIsNone(
MoviePilotServerHelper._build_subscribe_statistic_payload({
"name": "Legacy",
"type": "电影",
"tmdbid": 99,
})
)
def test_durable_subscribe_report_treats_disabled_sharing_as_success(self):
"""用户关闭统计分享时durable intent 应视为无需远端投递。"""
with patch.object(settings, "SUBSCRIBE_STATISTIC_SHARE", False), patch.object(
MoviePilotServerHelper,
"sub_reg",
) as reporter:
self.assertTrue(MoviePilotServerHelper.sub_reg_durable({"media_id": "1"}))
reporter.assert_not_called()
with patch.object(settings, "SUBSCRIBE_STATISTIC_SHARE", False), patch.object(
MoviePilotServerHelper,
"sub_done",
) as reporter:
self.assertTrue(MoviePilotServerHelper.sub_done_durable({"media_id": "1"}))
reporter.assert_not_called()