mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-11-15 12:50:20 +08:00
102 lines
4.4 KiB
Python
102 lines
4.4 KiB
Python
"""
|
|
验证插件静态资源的浏览器缓存、条件请求和访问安全边界。
|
|
"""
|
|
|
|
from types import SimpleNamespace
|
|
from unittest.mock import Mock
|
|
|
|
import pytest
|
|
from fastapi import FastAPI, HTTPException
|
|
from starlette.testclient import TestClient
|
|
|
|
from app.api.endpoints import plugin
|
|
|
|
|
|
@pytest.fixture
|
|
def static_files(tmp_path, monkeypatch):
|
|
"""
|
|
在临时插件目录与独立路由中提供资源,保留真实访问校验依赖。
|
|
"""
|
|
root = tmp_path / "app/plugins/demo/dist"
|
|
root.mkdir(parents=True)
|
|
(root / "remoteEntry.js").write_text("export const version = 1", encoding="utf-8")
|
|
(root / "chunk.js").write_text("export const value = 1", encoding="utf-8")
|
|
manager = Mock()
|
|
manager.get_plugin_source_id.return_value = "Demo"
|
|
manager.get_plugin_ids.return_value = ["Demo"]
|
|
manager.get_plugin_attr.return_value = "1.0.0"
|
|
manager.get_plugin_auth_providers.return_value = []
|
|
verify = Mock()
|
|
monkeypatch.setattr(plugin, "get_plugin_manager", lambda: manager)
|
|
monkeypatch.setattr(plugin, "verify_resource_token", verify)
|
|
monkeypatch.setattr(plugin, "get_api_runtime_config_snapshot", lambda: SimpleNamespace(root_path=tmp_path))
|
|
app = FastAPI()
|
|
app.include_router(plugin.router, prefix="/plugin")
|
|
return TestClient(app), root, manager, verify
|
|
|
|
|
|
def test_current_version_entry_is_private_and_cacheable(static_files):
|
|
"""
|
|
只有当前版本参数可授予长期私有缓存,且返回文件校验头。
|
|
"""
|
|
client, _, manager, _ = static_files
|
|
response = client.get("/plugin/file/demo/dist/remoteEntry.js?v=1.0.0")
|
|
assert response.status_code == 200
|
|
assert response.content == b"export const version = 1"
|
|
assert response.headers["content-type"].startswith("application/javascript")
|
|
assert response.headers["cache-control"] == "private, max-age=31536000, immutable"
|
|
assert response.headers["etag"]
|
|
assert response.headers["last-modified"]
|
|
manager.get_plugin_attr.assert_called_once_with("Demo", "plugin_version")
|
|
manager.get_plugin_attr.return_value = "2.0.0"
|
|
assert client.get("/plugin/file/Demo/dist/remoteEntry.js?v=1.0.0").headers["cache-control"] == "private, no-cache"
|
|
assert "immutable" in client.get("/plugin/file/Demo/dist/remoteEntry.js?v=2.0.0").headers["cache-control"]
|
|
|
|
|
|
@pytest.mark.parametrize("validator", ["etag", "last-modified"])
|
|
def test_unversioned_chunks_revalidate_without_resending_body(static_files, validator):
|
|
"""
|
|
无版本 chunk 保留缓存内容,条件命中时仍校验权限并返回无响应体的 304。
|
|
"""
|
|
client, _, _, verify = static_files
|
|
url = "/plugin/file/Demo/dist/chunk.js"
|
|
first = client.get(url)
|
|
header = "If-None-Match" if validator == "etag" else "If-Modified-Since"
|
|
response = client.get(url, headers={header: first.headers[validator]})
|
|
assert response.status_code == 304
|
|
assert response.content == b""
|
|
assert response.headers["cache-control"] == "private, no-cache"
|
|
assert response.headers[validator] == first.headers[validator]
|
|
assert verify.call_count == 2
|
|
verify.side_effect = HTTPException(status_code=403, detail="Forbidden")
|
|
assert client.get(url, headers={header: first.headers[validator]}).status_code == 403
|
|
|
|
|
|
def test_changed_file_and_nonmatching_etag_return_new_body(static_files):
|
|
"""
|
|
文件改变或 ETag 不匹配时,不被旧修改时间错误判为未修改。
|
|
"""
|
|
client, root, _, _ = static_files
|
|
url = "/plugin/file/Demo/dist/chunk.js"
|
|
first = client.get(url)
|
|
(root / "chunk.js").write_text("export const value = 'updated'", encoding="utf-8")
|
|
response = client.get(url, headers={
|
|
"If-None-Match": first.headers["etag"],
|
|
"If-Modified-Since": "Wed, 01 Jan 2099 00:00:00 GMT",
|
|
})
|
|
assert response.status_code == 200
|
|
assert response.headers["etag"] != first.headers["etag"]
|
|
assert response.content == b"export const value = 'updated'"
|
|
|
|
|
|
def test_missing_and_escaped_files_are_not_cacheable(static_files, tmp_path):
|
|
"""
|
|
缺失资源与指向插件外的符号链接保持原有拒绝语义。
|
|
"""
|
|
client, root, _, _ = static_files
|
|
outside = tmp_path / "outside.js"
|
|
outside.write_text("private", encoding="utf-8")
|
|
(root / "escaped.js").symlink_to(outside)
|
|
assert client.get("/plugin/file/Demo/dist/escaped.js").status_code == 403
|
|
assert client.get("/plugin/file/Demo/dist/missing.js?v=1.0.0").status_code == 404
|