mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-08-29 03:56:43 +08:00
208 lines
7.0 KiB
Python
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"
|