Files
MoviePilot/tests/test_plugin_declared_metadata.py
T

208 lines
7.0 KiB
Python

"""插件 package 声明快照和值对象展示回退测试。"""
from datetime import datetime, timezone
import pytest
from app.application.plugin.catalog import apply_declared_metadata_fallback
from app.application.plugin.declaration import PluginDeclaredMetadata
from app.application.plugin.identity import (
PluginBindingBasis,
PluginIdentity,
PluginPayloadSourceType,
TrustedPluginSourceType,
)
from app.schemas.plugin import Plugin, PluginRuntimeStatus
NOW = datetime(2026, 8, 25, 12, 0, tzinfo=timezone.utc)
OFFICIAL_SOURCE = "github:jxxghp/moviepilot-plugins"
def _metadata(
package: dict[str, object],
*,
version: str = "1.0.0",
matches_payload: bool = True,
) -> PluginDeclaredMetadata:
"""构造一份可用于身份测试的 package 声明快照。"""
return PluginDeclaredMetadata.from_package(
package,
declaration_version=version,
manifest_matches_payload=matches_payload,
)
def _identity(
metadata: PluginDeclaredMetadata,
*,
version: str = "1.0.0",
plugin_id: str = "DemoPlugin",
) -> PluginIdentity:
"""构造带已提交声明快照的官方在线身份。"""
return PluginIdentity(
plugin_id=plugin_id,
normalized_plugin_id=plugin_id.lower(),
trusted_source_type=TrustedPluginSourceType.OFFICIAL,
trusted_source_key=OFFICIAL_SOURCE,
binding_basis=PluginBindingBasis.OFFICIAL_DEFAULT,
payload_source_type=PluginPayloadSourceType.OFFICIAL,
payload_source_key=OFFICIAL_SOURCE,
declared_version=version,
package_generation="v3",
declared_metadata=metadata,
payload_receipt="sha256:" + "0" * 64,
revision=1,
created_at=NOW,
updated_at=NOW,
bound_at=NOW,
payload_applied_at=NOW,
)
def test_declared_metadata_round_trips_package_and_storage_without_extra_fields() -> None:
"""package 声明保存后应可稳定往返,并丢弃未纳入合同的字段。"""
package = {
"name": "Demo",
"description": "A demo plugin",
"icon": "demo.svg",
"author": "MoviePilot",
"system_version": ">=3.0.0",
"labels": "alpha, beta",
"level": 2,
"release": True,
"v4": True,
"v4t": False,
"unknown_field": "ignored",
"repo_url": "https://github.com/attacker/repo",
"source": "github:attacker/repo",
"token": "secret",
"plugin_public_key": "public-key",
}
snapshot = _metadata(package, version="2.0.0")
restored = PluginDeclaredMetadata.from_storage(snapshot.to_json())
stored = restored.to_json()
assert restored == snapshot
assert restored.declaration_version == "2.0.0"
assert restored.manifest_matches_payload is True
assert restored.runtime_support("v4") is True
assert restored.runtime_support("v4t") is False
assert stored["manifest"] == {
"author": "MoviePilot",
"description": "A demo plugin",
"icon": "demo.svg",
"labels": ["alpha", "beta"],
"level": 2,
"name": "Demo",
"release": True,
"system_version": ">=3.0.0",
}
assert stored["runtime"] == {"v4": True, "v4t": False}
assert "unknown_field" not in str(stored)
assert "attacker" not in str(stored)
assert "secret" not in str(stored)
assert "public-key" not in str(stored)
def test_declared_metadata_preserves_system_version_storage_limit() -> None:
"""系统版本声明继续遵守旧数据库列的长度合同,保证迁移可降级。"""
with pytest.raises(ValueError, match="长度不能超过 128"):
_metadata({"system_version": "v" * 129})
def test_declared_metadata_ignores_unknown_runtime_shapes_and_is_deep_copied() -> None:
"""未知代际和外部可变对象不能污染已提交快照。"""
labels = ["alpha"]
package = {
"name": "Demo",
"labels": labels,
"v4": True,
"v4t": False,
"v5": "not-a-bool",
"vX": True,
}
snapshot = _metadata(package)
labels.append("mutated-after-build")
exported = snapshot.to_json()
exported["manifest"]["labels"].append("mutated-export")
assert snapshot.to_json()["manifest"]["labels"] == ["alpha"]
assert snapshot.runtime_support("v4") is True
assert snapshot.runtime_support("v4t") is False
assert snapshot.runtime_support("v5") is None
def test_declared_metadata_records_current_and_historical_release_truth() -> None:
"""当前 package 与历史 Release 的声明对应关系必须可区分。"""
current = _metadata({"name": "Demo", "v3": True}, version="2.0.0")
historical = _metadata(
{"name": "Demo", "v3": True},
version="1.0.0",
matches_payload=False,
)
assert current.manifest_matches_payload is True
assert historical.manifest_matches_payload is False
assert historical.declaration_version == "1.0.0"
def test_declared_metadata_fallback_is_batch_safe_and_preserves_runtime_fields() -> None:
"""批量展示回退只补空展示字段,不覆盖运行态或另一插件身份。"""
first = _identity(
_metadata(
{
"name": "Demo from snapshot",
"description": "Saved description",
"icon": "saved.svg",
"author": "Saved author",
"labels": ["saved", "plugin"],
},
version="2.0.0",
),
version="2.0.0",
plugin_id="DemoPlugin",
)
second = _identity(
_metadata({"name": "Other from snapshot"}, version="3.0.0"),
version="3.0.0",
plugin_id="OtherPlugin",
)
plugins = [
Plugin(
id="DemoPlugin",
plugin_name="DemoPlugin",
plugin_version=None,
runtime_status=PluginRuntimeStatus.LOAD_FAILED,
state=True,
plugin_desc=None,
),
Plugin(
id="OtherPlugin",
plugin_name="Runtime name",
plugin_desc="Runtime description",
plugin_version="loaded-version",
runtime_status=PluginRuntimeStatus.READY,
),
Plugin(id="MissingPlugin", plugin_name="MissingPlugin"),
]
result = apply_declared_metadata_fallback(
plugins,
{"demoplugin": first, "otherplugin": second},
)
assert result[0].plugin_name == "Demo from snapshot"
assert result[0].plugin_desc == "Saved description"
assert result[0].plugin_icon == "saved.svg"
assert result[0].plugin_author == "Saved author"
assert result[0].plugin_version == "2.0.0"
assert result[0].plugin_label == "saved,plugin"
assert result[0].runtime_status is PluginRuntimeStatus.LOAD_FAILED
assert result[0].state is True
assert result[1].plugin_name == "Runtime name"
assert result[1].plugin_desc == "Runtime description"
assert result[1].plugin_version == "loaded-version"
assert result[1].runtime_status is PluginRuntimeStatus.READY
assert result[2].plugin_name == "MissingPlugin"