Files
vrsub/tests/test_run_deletion_safety.py
T

105 lines
4.6 KiB
Python

"""任务删除安全回归:真实媒体副本验证用户目录与任务私有目录的归属边界。"""
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()