fix: 防止任务删除误删源视频目录并跟踪审查问题
This commit is contained in:
@@ -0,0 +1,104 @@
|
||||
"""任务删除安全回归:真实媒体副本验证用户目录与任务私有目录的归属边界。"""
|
||||
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from wov_app.config import STORAGE_DIR
|
||||
from wov_app.main import app
|
||||
from wov_app.db import Database
|
||||
from wov_app.maintenance import OrphanCleaner
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def media_library(tmp_path):
|
||||
"""复制已有真实视频与字幕,任何删除只作用于测试临时目录。"""
|
||||
assets = Path(__file__).resolve().parent.parent / "testdata"
|
||||
sources = [assets / "subtitle_10s.mp4", assets / "ocr_srt_run_ac7f480a3ccb.srt"]
|
||||
if not all(source.is_file() for source in sources):
|
||||
pytest.skip("缺少真实视频或字幕测试素材")
|
||||
library = tmp_path / "media"
|
||||
library.mkdir()
|
||||
files = [library / "movie.mp4", library / "movie.CN.srt"]
|
||||
for source, target in zip(sources, files):
|
||||
shutil.copy2(source, target)
|
||||
return files, [path.read_bytes() for path in files]
|
||||
|
||||
|
||||
def _create_run(db, run_id, video, source, status):
|
||||
"""在真实数据库登记任务,不启动模型或调度器。"""
|
||||
workflow_id = "delete-safety"
|
||||
db.upsert_workflow({"id": workflow_id, "name": "删除安全回归"})
|
||||
db.create_run({
|
||||
"id": run_id, "workflow_id": workflow_id, "workflow_version": 1,
|
||||
"source": source, "status": status, "input_uri": str(video),
|
||||
"created_at": "2020-01-01T00:00:00+00:00",
|
||||
"updated_at": "2020-01-01T00:00:00+00:00",
|
||||
})
|
||||
# 登记可读的真实字幕产物,拒绝删除时应连同记录保留。
|
||||
db.create_artifact({
|
||||
"run_id": run_id, "node_id": "ass", "name": "subtitle",
|
||||
"uri": str(video.with_suffix(".CN.srt")), "mime_type": "application/x-subrip",
|
||||
})
|
||||
|
||||
|
||||
@pytest.mark.parametrize("status", ["QUEUED", "RUNNING", "PAUSED", "FAILED", "COMPLETED"])
|
||||
def test_normal_delete_rejects_batch_run(media_library, status):
|
||||
"""普通 DELETE 对所有状态的批量 run 返回 422,保留媒体与关联数据。"""
|
||||
files, contents = media_library
|
||||
run_id = f"run_batch_delete_safety_{status.lower()}"
|
||||
with TestClient(app) as client:
|
||||
db = app.state.db
|
||||
_create_run(db, run_id, files[0], "batch", status)
|
||||
before = db.get_run(run_id)
|
||||
artifacts = db.list_artifacts(run_id)
|
||||
response = client.delete(f"/api/runs/{run_id}")
|
||||
assert response.status_code == 422
|
||||
assert "批量任务" in response.json()["detail"]
|
||||
assert db.get_run(run_id) == before
|
||||
assert db.list_artifacts(run_id) == artifacts
|
||||
assert [path.read_bytes() for path in files] == contents
|
||||
|
||||
|
||||
def test_upload_delete_ignores_external_input_parent(media_library):
|
||||
"""历史上传记录即使指向外部视频,也仅清理按 run_id 定位的私有目录。"""
|
||||
files, contents = media_library
|
||||
run_id = "run_external_delete_safety"
|
||||
with TestClient(app) as client:
|
||||
db = app.state.db
|
||||
_create_run(db, run_id, files[0], "upload", "FAILED")
|
||||
private_dirs = [STORAGE_DIR / kind / run_id for kind in ("uploads", "runs")]
|
||||
for directory in private_dirs:
|
||||
directory.mkdir(parents=True)
|
||||
shutil.copy2(files[1], directory / "subtitle.srt")
|
||||
# 同级其他任务目录也不能被扩大范围删除。
|
||||
sibling = STORAGE_DIR / "uploads" / "run_neighbor_delete_safety"
|
||||
sibling.mkdir(parents=True)
|
||||
shutil.copy2(files[1], sibling / "subtitle.srt")
|
||||
response = client.delete(f"/api/runs/{run_id}")
|
||||
assert response.status_code == 200
|
||||
assert db.get_run(run_id) is None
|
||||
assert db.list_artifacts(run_id) == []
|
||||
assert [path.read_bytes() for path in files] == contents
|
||||
assert all(not directory.exists() for directory in private_dirs)
|
||||
assert (sibling / "subtitle.srt").read_bytes() == contents[1]
|
||||
|
||||
|
||||
def test_orphan_cleanup_ignores_external_input_parent(media_library, tmp_path):
|
||||
"""自动清理过期上传任务时也不能从 input_uri 推导递归删除范围。"""
|
||||
files, contents = media_library
|
||||
db = Database(tmp_path / "orphan.db")
|
||||
run_id = "run_orphan_delete_safety"
|
||||
_create_run(db, run_id, files[0], "upload", "COMPLETED")
|
||||
# 产物确实丢失,符合孤儿清理条件;源视频与旁挂字幕仍是用户数据。
|
||||
db.delete_run_artifacts(run_id)
|
||||
storage = tmp_path / "private"
|
||||
upload_dir = storage / "uploads" / run_id
|
||||
upload_dir.mkdir(parents=True)
|
||||
shutil.copy2(files[0], upload_dir / "movie.mp4")
|
||||
OrphanCleaner(db, storage).clean_once()
|
||||
assert db.get_run(run_id) is None
|
||||
assert [path.read_bytes() for path in files] == contents
|
||||
assert not upload_dir.exists()
|
||||
Reference in New Issue
Block a user