Files
MoviePilot/tests/test_publish_moviepilot_plugin_skill.py

228 lines
7.9 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.
import importlib.util
import json
import sys
from argparse import Namespace
from pathlib import Path
from typing import Any, Optional
def load_publish_plugin_module() -> Any:
"""加载插件发布脚本模块。"""
script_path = (
Path(__file__).resolve().parents[1]
/ "skills"
/ "publish-moviepilot-plugin"
/ "scripts"
/ "publish_plugin.py"
)
spec = importlib.util.spec_from_file_location("publish_plugin_skill", script_path)
module = importlib.util.module_from_spec(spec)
sys.modules[spec.name] = module
spec.loader.exec_module(module)
return module
def test_collect_local_files_excludes_secrets_and_keeps_dist(tmp_path: Path) -> None:
"""收集本地插件文件时应排除敏感文件并保留前端构建产物。"""
module = load_publish_plugin_module()
plugin_dir = tmp_path / "plugins.v2" / "myplugin"
plugin_dir.mkdir(parents=True)
(plugin_dir / "__init__.py").write_text("class MyPlugin:\n pass\n", encoding="utf-8")
(plugin_dir / ".env").write_text("SECRET=1\n", encoding="utf-8")
(plugin_dir / "dist" / "assets").mkdir(parents=True)
(plugin_dir / "dist" / "assets" / "remoteEntry.js").write_text(
"export default {};\n",
encoding="utf-8",
)
layout = module.Layout(package_file="package.v2.json", plugin_root="plugins.v2")
files, rejected = module.collect_local_files(
tmp_path,
layout,
"MyPlugin",
list(module.DEFAULT_EXCLUDES),
[],
)
assert "plugins.v2/myplugin/__init__.py" in files
assert "plugins.v2/myplugin/dist/assets/remoteEntry.js" in files
assert rejected == {"plugins.v2/myplugin/.env": ".env"}
def test_merge_package_content_preserves_other_plugins() -> None:
"""合并 package 文件时只更新目标插件条目。"""
module = load_publish_plugin_module()
remote_content = json.dumps(
{
"OtherPlugin": {"name": "其他插件", "version": "1.0.0"},
"MyPlugin": {"name": "旧插件", "version": "0.9.0"},
},
ensure_ascii=False,
).encode("utf-8")
merged = module.merge_package_content(
remote_content,
"MyPlugin",
{"name": "新插件", "version": "1.0.0"},
)
package_data = json.loads(merged.decode("utf-8"))
assert package_data["OtherPlugin"] == {"name": "其他插件", "version": "1.0.0"}
assert package_data["MyPlugin"] == {"name": "新插件", "version": "1.0.0"}
def test_push_creates_public_repo_when_explicitly_requested(tmp_path: Path) -> None:
"""显式允许自动建仓时,推送会默认创建公开仓库。"""
module = load_publish_plugin_module()
plugin_dir = tmp_path / "plugins.v2" / "myplugin"
plugin_dir.mkdir(parents=True)
(plugin_dir / "__init__.py").write_text("class MyPlugin:\n pass\n", encoding="utf-8")
(tmp_path / "package.v2.json").write_text(
json.dumps({"MyPlugin": {"name": "测试插件", "version": "1.0.0"}}, ensure_ascii=False),
encoding="utf-8",
)
class FakeClient:
"""模拟缺失仓库的 GitHub 客户端。"""
def __init__(self) -> None:
"""初始化调用记录。"""
self.created_private = None
self.put_paths: list[str] = []
def repo_exists(self) -> bool:
"""返回仓库不存在。"""
return False
def create_repo(self, private: bool = False) -> dict[str, Any]:
"""记录建仓可见性。"""
self.created_private = private
return {"private": private, "html_url": "https://github.com/example/repo"}
def put_file(
self,
path: str,
content: bytes,
message: str,
sha: Optional[str] = None,
) -> dict[str, Any]:
"""记录上传路径。"""
self.put_paths.append(path)
return {"content": {"path": path}}
def delete_file(self, path: str, message: str, sha: str) -> dict[str, Any]:
"""记录删除路径。"""
return {"content": {"path": path}}
fake_client = FakeClient()
def fake_build_context(args: Namespace, require_token: bool = False) -> dict[str, Any]:
"""构造无需访问网络的推送上下文。"""
layout = module.Layout(package_file="package.v2.json", plugin_root="plugins.v2")
return {
"repo": args.repo,
"branch": args.branch,
"local_repo": tmp_path,
"layout": layout,
"plugin_id": args.plugin_id,
"remote_prefix": module.remote_plugin_prefix(layout, args.plugin_id),
"client": fake_client,
"excludes": list(module.DEFAULT_EXCLUDES),
"includes": [],
}
original_build_context = module.build_context
module.build_context = fake_build_context
try:
payload = module.push(
Namespace(
command="push",
repo="example/repo",
plugin_id="MyPlugin",
local_repo=str(tmp_path),
package_version="v2",
branch="main",
token="token",
message="Publish MyPlugin",
api_base=module.GITHUB_API_BASE,
proxy="",
timeout=module.DEFAULT_TIMEOUT,
include=[],
exclude=[],
delete_remote=False,
create_repo_if_missing=True,
private=False,
force=False,
dry_run=False,
)
)
finally:
module.build_context = original_build_context
assert payload["repo_existed"] is False
assert fake_client.created_private is False
assert "package.v2.json" in fake_client.put_paths
assert "plugins.v2/myplugin/__init__.py" in fake_client.put_paths
def test_create_repo_dry_run_does_not_touch_github() -> None:
"""建仓 dry-run 不访问 GitHub只返回计划。"""
module = load_publish_plugin_module()
class FakeClient:
"""模拟不应被调用的 GitHub 客户端。"""
def repo_exists(self) -> bool:
"""如果被调用则说明 dry-run 行为错误。"""
raise AssertionError("dry-run should not query GitHub")
def create_repo(self, private: bool = False) -> dict[str, Any]:
"""如果被调用则说明 dry-run 行为错误。"""
raise AssertionError("dry-run should not create GitHub repo")
def fake_build_context(args: Namespace, require_token: bool = False) -> dict[str, Any]:
"""构造无需访问网络的建仓上下文。"""
return {
"repo": args.repo,
"branch": args.branch,
"local_repo": Path.cwd(),
"layout": module.Layout(package_file="package.v2.json", plugin_root="plugins.v2"),
"plugin_id": "",
"remote_prefix": "",
"client": FakeClient(),
"excludes": list(module.DEFAULT_EXCLUDES),
"includes": [],
}
original_build_context = module.build_context
module.build_context = fake_build_context
try:
payload = module.create_repo(
Namespace(
command="create-repo",
repo="example/repo",
plugin_id="",
local_repo="",
package_version="auto",
branch="main",
token="",
message="",
api_base=module.GITHUB_API_BASE,
proxy="",
timeout=module.DEFAULT_TIMEOUT,
include=[],
exclude=[],
delete_remote=False,
create_repo_if_missing=False,
private=False,
force=False,
dry_run=True,
)
)
finally:
module.build_context = original_build_context
assert payload["success"] is True
assert payload["dry_run"] is True
assert payload["private"] is False