mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-08-31 21:17:06 +08:00
428 lines
14 KiB
Python
428 lines
14 KiB
Python
"""插件来源准入与目标身份规划测试。"""
|
|
|
|
from dataclasses import replace
|
|
from datetime import datetime, timedelta, timezone
|
|
|
|
import pytest
|
|
|
|
from app.application.plugin.admission import (
|
|
PluginInstallAdmissionRequest,
|
|
PluginSourceAdmissionError,
|
|
admit_plugin_install,
|
|
)
|
|
from app.application.plugin.declaration import PluginDeclaredMetadata
|
|
from app.application.plugin.identity import (
|
|
PluginBindingBasis,
|
|
PluginIdentity,
|
|
PluginPayloadSourceType,
|
|
TrustedPluginSourceType,
|
|
)
|
|
from app.application.plugin.source import (
|
|
CandidateInventory,
|
|
MarketRead,
|
|
PluginLocalCandidate,
|
|
PluginMarketCandidate,
|
|
)
|
|
|
|
NOW = datetime(2026, 8, 25, 12, 0, tzinfo=timezone.utc)
|
|
OFFICIAL = "github:jxxghp/moviepilot-plugins"
|
|
THIRD_PARTY = "github:example/moviepilot-plugins"
|
|
|
|
|
|
def _online_candidate(
|
|
*,
|
|
source_key: str = OFFICIAL,
|
|
source_type: TrustedPluginSourceType = TrustedPluginSourceType.OFFICIAL,
|
|
) -> PluginMarketCandidate:
|
|
"""构造一个可安装在线候选。"""
|
|
owner_repo = source_key.removeprefix("github:")
|
|
return PluginMarketCandidate(
|
|
plugin_id="DemoPlugin",
|
|
source_key=source_key,
|
|
source_type=source_type,
|
|
repo_url=f"https://github.com/{owner_repo}",
|
|
package_generation="v3",
|
|
plugin_version="2.0.0",
|
|
dto={"system_version": ">=3.0.0", "v3": True, "v3t": False},
|
|
)
|
|
|
|
|
|
def _identity() -> PluginIdentity:
|
|
"""构造已经绑定官方来源的旧载荷身份。"""
|
|
return PluginIdentity(
|
|
plugin_id="DemoPlugin",
|
|
normalized_plugin_id="demoplugin",
|
|
trusted_source_type=TrustedPluginSourceType.OFFICIAL,
|
|
trusted_source_key=OFFICIAL,
|
|
binding_basis=PluginBindingBasis.OFFICIAL_DEFAULT,
|
|
payload_source_type=PluginPayloadSourceType.OFFICIAL,
|
|
payload_source_key=OFFICIAL,
|
|
declared_version="1.0.0",
|
|
package_generation="v3",
|
|
declared_metadata=PluginDeclaredMetadata.from_package(
|
|
{"name": "Demo", "v3": True, "v3t": False},
|
|
declaration_version="1.0.0",
|
|
manifest_matches_payload=True,
|
|
),
|
|
payload_receipt="sha256:" + "0" * 64,
|
|
revision=3,
|
|
created_at=NOW,
|
|
updated_at=NOW,
|
|
bound_at=NOW,
|
|
payload_applied_at=NOW,
|
|
)
|
|
|
|
|
|
def _inventory(*candidates, local_candidates=()) -> CandidateInventory:
|
|
"""构造完整市场库存。"""
|
|
return CandidateInventory(
|
|
(MarketRead.present("https://github.com/example/plugins", candidates),),
|
|
tuple(local_candidates),
|
|
)
|
|
|
|
|
|
def test_same_source_update_preserves_binding_and_advances_payload() -> None:
|
|
"""同源更新只推进载荷事实,不改变既有可信绑定依据。"""
|
|
current = _identity()
|
|
admission = admit_plugin_install(
|
|
_inventory(_online_candidate()),
|
|
request=PluginInstallAdmissionRequest(
|
|
plugin_id="DemoPlugin",
|
|
generations=("v3", "v2", "v1"),
|
|
requested_repo_url="https://github.com/jxxghp/MoviePilot-Plugins",
|
|
explicit_source=False,
|
|
),
|
|
identity=current,
|
|
now=NOW,
|
|
)
|
|
|
|
target = admission.build_identity(
|
|
payload_receipt="sha256:" + "1" * 64,
|
|
applied_at=NOW,
|
|
)
|
|
|
|
assert target.binding_basis is PluginBindingBasis.OFFICIAL_DEFAULT
|
|
assert target.trusted_source_key == OFFICIAL
|
|
assert target.revision == 4
|
|
assert target.declared_version == "2.0.0"
|
|
|
|
|
|
def test_current_release_marks_declaration_as_matching_payload() -> None:
|
|
"""按当前 package 版本安装时,声明快照应标记为对应当前载荷。"""
|
|
admission = admit_plugin_install(
|
|
_inventory(_online_candidate()),
|
|
request=PluginInstallAdmissionRequest(
|
|
plugin_id="DemoPlugin",
|
|
generations=("v3", "v2", "v1"),
|
|
requested_repo_url="https://github.com/jxxghp/MoviePilot-Plugins",
|
|
),
|
|
identity=None,
|
|
now=NOW,
|
|
)
|
|
|
|
target = admission.build_identity(
|
|
payload_receipt="sha256:" + "7" * 64,
|
|
applied_at=NOW,
|
|
)
|
|
|
|
assert target.declared_metadata is not None
|
|
assert target.declared_metadata.declaration_version == "2.0.0"
|
|
assert target.declared_metadata.manifest_matches_payload is True
|
|
|
|
|
|
def test_historical_release_marks_declaration_as_not_matching_payload() -> None:
|
|
"""安装历史 Release 时必须保留其声明版本,并标记与当前载荷不对应。"""
|
|
admission = admit_plugin_install(
|
|
_inventory(_online_candidate()),
|
|
request=PluginInstallAdmissionRequest(
|
|
plugin_id="DemoPlugin",
|
|
generations=("v3", "v2", "v1"),
|
|
requested_repo_url="https://github.com/jxxghp/MoviePilot-Plugins",
|
|
),
|
|
identity=None,
|
|
now=NOW,
|
|
)
|
|
|
|
target = admission.build_identity(
|
|
payload_receipt="sha256:" + "8" * 64,
|
|
applied_at=NOW,
|
|
declared_version="1.0.0",
|
|
manifest_matches_payload=False,
|
|
)
|
|
|
|
assert target.declared_version == "1.0.0"
|
|
assert target.declared_metadata is not None
|
|
assert target.declared_metadata.declaration_version == "2.0.0"
|
|
assert target.declared_metadata.manifest_matches_payload is False
|
|
|
|
|
|
def test_first_online_binding_uses_payload_commit_time() -> None:
|
|
"""首次在线绑定在载荷提交时生效,不能早于身份创建时间。"""
|
|
applied_at = NOW + timedelta(seconds=1)
|
|
admission = admit_plugin_install(
|
|
_inventory(_online_candidate()),
|
|
request=PluginInstallAdmissionRequest(
|
|
plugin_id="DemoPlugin",
|
|
generations=("v3", "v2", "v1"),
|
|
requested_repo_url="https://github.com/jxxghp/MoviePilot-Plugins",
|
|
explicit_source=True,
|
|
),
|
|
identity=None,
|
|
now=NOW,
|
|
)
|
|
|
|
target = admission.build_identity(
|
|
payload_receipt="sha256:" + "6" * 64,
|
|
applied_at=applied_at,
|
|
)
|
|
|
|
assert target.created_at == applied_at
|
|
assert target.updated_at == applied_at
|
|
assert target.bound_at == applied_at
|
|
assert target.payload_applied_at == applied_at
|
|
|
|
|
|
def test_force_semantics_cannot_authorize_source_change() -> None:
|
|
"""普通安装即使替换载荷,也不能选择不同于已绑定来源的仓库。"""
|
|
with pytest.raises(PluginSourceAdmissionError, match="已绑定其他仓库"):
|
|
admit_plugin_install(
|
|
_inventory(
|
|
_online_candidate(),
|
|
_online_candidate(
|
|
source_key=THIRD_PARTY,
|
|
source_type=TrustedPluginSourceType.THIRD_PARTY,
|
|
),
|
|
),
|
|
request=PluginInstallAdmissionRequest(
|
|
plugin_id="DemoPlugin",
|
|
generations=("v3", "v2", "v1"),
|
|
requested_repo_url="https://github.com/example/moviepilot-plugins",
|
|
explicit_source=True,
|
|
),
|
|
identity=_identity(),
|
|
now=NOW,
|
|
)
|
|
|
|
|
|
@pytest.mark.parametrize("revision", [None, 2, 4])
|
|
def test_source_change_requires_exact_identity_revision(revision: int | None) -> None:
|
|
"""显式换源必须携带当前身份的精确 revision。"""
|
|
with pytest.raises(PluginSourceAdmissionError, match="状态已变化"):
|
|
admit_plugin_install(
|
|
_inventory(
|
|
_online_candidate(
|
|
source_key=THIRD_PARTY,
|
|
source_type=TrustedPluginSourceType.THIRD_PARTY,
|
|
)
|
|
),
|
|
request=PluginInstallAdmissionRequest(
|
|
plugin_id="DemoPlugin",
|
|
generations=("v3", "v2", "v1"),
|
|
requested_repo_url="https://github.com/example/moviepilot-plugins",
|
|
explicit_source=True,
|
|
source_change=True,
|
|
expected_revision=revision,
|
|
),
|
|
identity=_identity(),
|
|
now=NOW,
|
|
)
|
|
|
|
|
|
def test_source_change_builds_explicit_transition() -> None:
|
|
"""合法换源把 trusted 与 payload 一起指向明确选择的新仓库。"""
|
|
candidate = _online_candidate(
|
|
source_key=THIRD_PARTY,
|
|
source_type=TrustedPluginSourceType.THIRD_PARTY,
|
|
)
|
|
admission = admit_plugin_install(
|
|
_inventory(candidate),
|
|
request=PluginInstallAdmissionRequest(
|
|
plugin_id="DemoPlugin",
|
|
generations=("v3", "v2", "v1"),
|
|
requested_repo_url=candidate.repo_url,
|
|
explicit_source=True,
|
|
source_change=True,
|
|
expected_revision=3,
|
|
),
|
|
identity=_identity(),
|
|
now=NOW,
|
|
)
|
|
|
|
applied_at = NOW + timedelta(seconds=1)
|
|
target = admission.build_identity(
|
|
payload_receipt="sha256:" + "2" * 64,
|
|
applied_at=applied_at,
|
|
)
|
|
|
|
assert target.binding_basis is PluginBindingBasis.EXPLICIT_SOURCE_CHANGE
|
|
assert target.trusted_source_key == THIRD_PARTY
|
|
assert target.payload_source_key == THIRD_PARTY
|
|
assert target.bound_at == applied_at
|
|
|
|
|
|
def test_local_payload_preserves_existing_online_trust() -> None:
|
|
"""本地开发载荷覆盖时保留此前可信在线来源,便于之后同源恢复。"""
|
|
local = PluginLocalCandidate(
|
|
plugin_id="DemoPlugin",
|
|
repo_url="local://DemoPlugin?package_version=v3",
|
|
package_generation="v3",
|
|
plugin_version="2.0.0-dev",
|
|
dto={"v3": True},
|
|
)
|
|
admission = admit_plugin_install(
|
|
_inventory(local_candidates=(local,)),
|
|
request=PluginInstallAdmissionRequest(
|
|
plugin_id="DemoPlugin",
|
|
generations=("v3", "v2", "v1"),
|
|
requested_repo_url=local.repo_url,
|
|
explicit_source=True,
|
|
),
|
|
identity=_identity(),
|
|
now=NOW,
|
|
)
|
|
|
|
target = admission.build_identity(
|
|
payload_receipt="sha256:" + "3" * 64,
|
|
applied_at=NOW,
|
|
)
|
|
|
|
assert target.trusted_source_key == OFFICIAL
|
|
assert target.binding_basis is PluginBindingBasis.OFFICIAL_DEFAULT
|
|
assert target.payload_source_type is PluginPayloadSourceType.LOCAL
|
|
assert target.payload_source_key is None
|
|
|
|
|
|
def test_first_local_payload_creates_local_only_identity() -> None:
|
|
"""首次本地安装不会伪造在线可信来源。"""
|
|
local = PluginLocalCandidate(
|
|
plugin_id="DemoPlugin",
|
|
repo_url="local://DemoPlugin?package_version=v3",
|
|
package_generation="v3",
|
|
plugin_version="2.0.0-dev",
|
|
dto={"v3": True},
|
|
)
|
|
admission = admit_plugin_install(
|
|
_inventory(local_candidates=(local,)),
|
|
request=PluginInstallAdmissionRequest(
|
|
plugin_id="DemoPlugin",
|
|
generations=("v3", "v2", "v1"),
|
|
requested_repo_url=local.repo_url,
|
|
explicit_source=True,
|
|
),
|
|
identity=None,
|
|
now=NOW,
|
|
)
|
|
|
|
target = admission.build_identity(
|
|
payload_receipt="sha256:" + "4" * 64,
|
|
applied_at=NOW,
|
|
)
|
|
|
|
assert target.binding_basis is PluginBindingBasis.LOCAL_ONLY
|
|
assert target.trusted_source_type is TrustedPluginSourceType.UNKNOWN
|
|
|
|
|
|
def test_sanitized_local_reference_selects_configured_candidate_without_path() -> None:
|
|
"""脱敏本地来源标识仍能选择配置内候选,但公共投影不暴露路径。"""
|
|
local = PluginLocalCandidate(
|
|
plugin_id="DemoPlugin",
|
|
repo_url=(
|
|
"local://DemoPlugin?path=/private/secret/plugins&version=v3"
|
|
),
|
|
package_generation="v3",
|
|
plugin_version="2.0.0-dev",
|
|
dto={"v3": True, "path": "/private/secret/plugins"},
|
|
)
|
|
|
|
admission = admit_plugin_install(
|
|
_inventory(local_candidates=(local,)),
|
|
request=PluginInstallAdmissionRequest(
|
|
plugin_id="DemoPlugin",
|
|
generations=("v3", "v2", "v1"),
|
|
requested_repo_url="local://DemoPlugin?version=v3",
|
|
),
|
|
identity=None,
|
|
now=NOW,
|
|
)
|
|
|
|
assert admission.candidate is local
|
|
public = admission.candidate.public_dict()
|
|
assert public == {
|
|
"plugin_id": "DemoPlugin",
|
|
"source_type": "local",
|
|
"package_generation": "v3",
|
|
"plugin_version": "2.0.0-dev",
|
|
}
|
|
assert "/private/secret/plugins" not in str(public)
|
|
|
|
|
|
def test_legacy_identity_can_bind_explicit_online_source() -> None:
|
|
"""存量未绑定身份可在管理员明确选源后建立在线可信来源。"""
|
|
legacy = replace(
|
|
_identity(),
|
|
trusted_source_type=TrustedPluginSourceType.UNKNOWN,
|
|
trusted_source_key=None,
|
|
binding_basis=PluginBindingBasis.LEGACY_UNBOUND,
|
|
payload_source_type=PluginPayloadSourceType.UNKNOWN,
|
|
payload_source_key=None,
|
|
declared_version=None,
|
|
package_generation=None,
|
|
declared_metadata=None,
|
|
payload_receipt=None,
|
|
bound_at=None,
|
|
payload_applied_at=None,
|
|
)
|
|
candidate = _online_candidate(
|
|
source_key=THIRD_PARTY,
|
|
source_type=TrustedPluginSourceType.THIRD_PARTY,
|
|
)
|
|
admission = admit_plugin_install(
|
|
_inventory(candidate),
|
|
request=PluginInstallAdmissionRequest(
|
|
plugin_id="DemoPlugin",
|
|
generations=("v3", "v2", "v1"),
|
|
requested_repo_url=candidate.repo_url,
|
|
explicit_source=True,
|
|
),
|
|
identity=legacy,
|
|
now=NOW,
|
|
)
|
|
|
|
target = admission.build_identity(
|
|
payload_receipt="sha256:" + "5" * 64,
|
|
applied_at=NOW,
|
|
)
|
|
|
|
assert target.binding_basis is PluginBindingBasis.EXPLICIT_INSTALL
|
|
assert target.trusted_source_key == THIRD_PARTY
|
|
assert target.revision == 4
|
|
|
|
|
|
def test_source_change_rejects_local_payload_reference() -> None:
|
|
"""带 revision 的显式换源只能切换在线可信来源。"""
|
|
local = PluginLocalCandidate(
|
|
plugin_id="DemoPlugin",
|
|
repo_url="local://DemoPlugin?path=/private/plugins&version=v3",
|
|
package_generation="v3",
|
|
plugin_version="2.0.0-dev",
|
|
)
|
|
current = _identity()
|
|
|
|
with pytest.raises(
|
|
PluginSourceAdmissionError,
|
|
match="只能更换为在线插件仓库",
|
|
):
|
|
admit_plugin_install(
|
|
_inventory(local_candidates=(local,)),
|
|
request=PluginInstallAdmissionRequest(
|
|
plugin_id="DemoPlugin",
|
|
generations=("v3", "v2", "v1"),
|
|
requested_repo_url="local://DemoPlugin?version=v3",
|
|
explicit_source=True,
|
|
source_change=True,
|
|
expected_revision=current.revision,
|
|
),
|
|
identity=current,
|
|
now=NOW,
|
|
)
|