mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-10-29 03:18:14 +08:00
verify_access_code 把设备访问码拼进校验地址的路径段(/c/<code>),一旦连不上 就把整条 URL 写进 error 日志,明文凭据随之落到实例日志文件里;404 与状态码两 个分支本来就不带访问码,这里补齐同一条边界:只记录设备根地址。 这是 #7157 之后对其余裸 {url} 日志点做完整评估时唯一确认会写入凭据的一处:其余 调用点记录的是站点根地址,或把 api_key 经 params 传递、URL 本身不含凭据。
174 lines
5.8 KiB
Python
174 lines
5.8 KiB
Python
"""
|
|
飞牛媒体服务器类型迁移后的边界分支测试。
|
|
|
|
测试只使用本地替身,不连接真实飞牛服务。
|
|
"""
|
|
|
|
from types import SimpleNamespace
|
|
from unittest.mock import Mock
|
|
|
|
import pytest
|
|
|
|
from app.modules.trimemedia import api as trimemedia_api
|
|
from app.modules.trimemedia.module import TrimeMediaModule
|
|
from app.modules.trimemedia.trimemedia import TrimeMedia
|
|
from app.schemas.types import MediaSource
|
|
|
|
|
|
def _authenticated_client() -> TrimeMedia:
|
|
"""
|
|
构造一个带有最小认证状态的飞牛客户端替身。
|
|
"""
|
|
client = TrimeMedia.__new__(TrimeMedia)
|
|
client._host = "https://trimemedia.example"
|
|
client._username = "admin"
|
|
client._password = "password"
|
|
client._access_code = "access"
|
|
client._playhost = None
|
|
client._sync_libraries = []
|
|
client._userinfo = trimemedia_api.User("user", "admin", 1)
|
|
client._api = SimpleNamespace(
|
|
host="https://trimemedia.example",
|
|
token="token",
|
|
cookies={"Trim-MC-access": "access"},
|
|
)
|
|
client.is_authenticated = Mock(return_value=True)
|
|
return client
|
|
|
|
|
|
def test_authenticate_returns_none_when_probe_fails(monkeypatch):
|
|
"""
|
|
临时认证 API 探测失败时返回 None。
|
|
"""
|
|
client = TrimeMedia.__new__(TrimeMedia)
|
|
client._host = "https://trimemedia.example"
|
|
client._username = "admin"
|
|
client._password = "password"
|
|
monkeypatch.setattr(
|
|
TrimeMedia,
|
|
"_TrimeMedia__create_api",
|
|
staticmethod(lambda *_args: None),
|
|
)
|
|
|
|
assert client.authenticate("admin", "password") is None
|
|
|
|
|
|
def test_get_items_yields_nested_directory_items():
|
|
"""
|
|
媒体库目录应递归展开,并只产出电影条目。
|
|
"""
|
|
client = _authenticated_client()
|
|
root = trimemedia_api.Item(guid="root", type=trimemedia_api.Type.DIRECTORY)
|
|
movie = trimemedia_api.Item(guid="movie", type=trimemedia_api.Type.MOVIE)
|
|
client._api.item_list = Mock(side_effect=[[root], [movie]])
|
|
client._TrimeMedia__build_media_server_item = Mock(return_value="movie-result")
|
|
|
|
assert list(client.get_items("library")) == ["movie-result"]
|
|
assert client._api.item_list.call_args_list[1].kwargs["guid"] == "root"
|
|
|
|
|
|
def test_get_tv_episodes_skips_incomplete_episode_numbers():
|
|
"""
|
|
飞牛返回缺少季号或集号的条目时,只保留可用于统一契约的完整编号。
|
|
"""
|
|
client = _authenticated_client()
|
|
client.get_iteminfo = Mock(
|
|
return_value=SimpleNamespace(media_source=MediaSource.TMDB, media_id="1")
|
|
)
|
|
client._api.season_list = Mock(
|
|
return_value=[SimpleNamespace(guid="season", season_number=1)]
|
|
)
|
|
client._api.episode_list = Mock(
|
|
return_value=[
|
|
SimpleNamespace(season_number=None, episode_number=1),
|
|
SimpleNamespace(season_number=1, episode_number=None),
|
|
SimpleNamespace(season_number=1, episode_number=2),
|
|
]
|
|
)
|
|
|
|
assert client.get_tv_episodes(item_id="series", media_source=MediaSource.TMDB, media_id="1") == (
|
|
"series",
|
|
{1: [2]},
|
|
)
|
|
|
|
|
|
@pytest.mark.parametrize("method", ["get_latest", "get_latest_backdrops"])
|
|
def test_latest_rejects_none_count_like_previous_arithmetic(method):
|
|
"""
|
|
最新列表的 None 数量仍在原有乘法位置报告 TypeError。
|
|
"""
|
|
client = _authenticated_client()
|
|
|
|
with pytest.raises(TypeError, match="NoneType"):
|
|
getattr(client, method)(num=None)
|
|
|
|
|
|
def test_get_items_rejects_none_start_index_like_previous_arithmetic():
|
|
"""
|
|
媒体列表的 None 起始位置仍保持原有加法失败语义。
|
|
"""
|
|
client = _authenticated_client()
|
|
|
|
with pytest.raises(TypeError, match="NoneType"):
|
|
list(client.get_items("library", start_index=None))
|
|
|
|
|
|
def test_get_resume_builds_play_item_and_get_image_cookies(monkeypatch):
|
|
"""
|
|
继续观看条目应生成播放卡片,图片请求应返回带访问码的 Cookie。
|
|
"""
|
|
client = _authenticated_client()
|
|
client._api.play_list = Mock(
|
|
return_value=[
|
|
trimemedia_api.Item(
|
|
guid="movie",
|
|
type=trimemedia_api.Type.MOVIE,
|
|
title="电影",
|
|
poster="/poster.jpg",
|
|
)
|
|
]
|
|
)
|
|
|
|
resume = client.get_resume(num=1)
|
|
assert resume and resume[0].id == "movie"
|
|
monkeypatch.setattr("app.modules.trimemedia.trimemedia.SecurityUtils.is_safe_url", lambda *_args, **_kwargs: True)
|
|
assert client.get_image_cookies("https://trimemedia.example/poster.jpg") == {
|
|
"Trim-MC-token": "token",
|
|
"Trim-MC-access": "access",
|
|
}
|
|
|
|
|
|
def test_module_image_cookies_returns_none_when_no_instance_owns_image(monkeypatch):
|
|
"""
|
|
所有飞牛实例均拒绝图片地址时,模块不提供其它实例的授权凭据。
|
|
|
|
:param monkeypatch: 隔离模块实例目录
|
|
"""
|
|
module = TrimeMediaModule()
|
|
server = SimpleNamespace(get_image_cookies=Mock(return_value=None))
|
|
monkeypatch.setattr(module, "get_instances", lambda: {"first": server})
|
|
|
|
assert module.mediaserver_image_cookies(image_url="https://other.example/poster.jpg") is None
|
|
server.get_image_cookies.assert_called_once_with("https://other.example/poster.jpg")
|
|
|
|
|
|
def test_verify_access_code_failure_log_omits_access_code(monkeypatch):
|
|
"""
|
|
访问码是校验地址的路径段,连接失败时日志只记录设备根地址。
|
|
|
|
:param monkeypatch: 隔离 HTTP 会话与日志输出
|
|
"""
|
|
api = trimemedia_api.Api(
|
|
host="https://trimemedia.example/v",
|
|
apikey="apikey",
|
|
access_code="device-access-code",
|
|
)
|
|
logs = Mock()
|
|
monkeypatch.setattr(trimemedia_api, "logger", logs)
|
|
monkeypatch.setattr(api._request_utils, "get_res", lambda *_args, **_kwargs: None)
|
|
|
|
assert api.verify_access_code() is False
|
|
message = logs.error.call_args[0][0]
|
|
assert "device-access-code" not in message
|
|
assert "https://trimemedia.example" in message
|