""" 视频 API 集成测试 TDD 测试用例 - 测试视频上传、审核相关 API 接口 接口规范参考:DevelopmentPlan.md 第 7 章 验收标准参考:FeatureSummary.md F-10~F-18 """ import pytest from typing import Any from httpx import AsyncClient, ASGITransport from app.main import app @pytest.fixture async def auth_headers(): """获取认证头""" transport = ASGITransport(app=app) async with AsyncClient(transport=transport, base_url="http://test") as client: login_response = await client.post("/api/v1/auth/login", json={ "email": "creator@test.com", "password": "password" }) token = login_response.json()["access_token"] return {"Authorization": f"Bearer {token}"} class TestVideoUploadAPI: """视频上传 API 测试""" @pytest.mark.integration @pytest.mark.asyncio async def test_upload_video_success(self, auth_headers) -> None: """测试视频上传成功 - 返回 202 和 video_id""" transport = ASGITransport(app=app) async with AsyncClient(transport=transport, base_url="http://test") as client: response = await client.post( "/api/v1/videos/upload", files={"file": ("test.mp4", b"video content", "video/mp4")}, data={ "task_id": "task_001", "title": "测试视频" }, headers=auth_headers ) assert response.status_code == 202 data = response.json() assert "video_id" in data assert data["status"] == "processing" @pytest.mark.integration @pytest.mark.asyncio async def test_upload_oversized_video_returns_413(self, auth_headers) -> None: """测试超大视频返回 413 - 最大 100MB""" transport = ASGITransport(app=app) async with AsyncClient(transport=transport, base_url="http://test") as client: # 创建超过 100MB 的测试数据 oversized_content = b"x" * (101 * 1024 * 1024) response = await client.post( "/api/v1/videos/upload", files={"file": ("large.mp4", oversized_content, "video/mp4")}, data={"task_id": "task_001"}, headers=auth_headers ) assert response.status_code == 413 assert "100MB" in response.json()["detail"] @pytest.mark.integration @pytest.mark.asyncio @pytest.mark.parametrize("filename,expected_status", [ ("test.mp4", 202), ("test.mov", 202), ("test.avi", 400), # AVI - 不支持 ("test.mkv", 400), # MKV - 不支持 ("test.pdf", 400), ]) async def test_upload_video_format_validation( self, auth_headers, filename: str, expected_status: int, ) -> None: """测试视频格式验证 - 仅支持 MP4/MOV""" transport = ASGITransport(app=app) async with AsyncClient(transport=transport, base_url="http://test") as client: response = await client.post( "/api/v1/videos/upload", files={"file": (filename, b"content", "video/mp4")}, data={"task_id": "task_001"}, headers=auth_headers ) assert response.status_code == expected_status @pytest.mark.integration @pytest.mark.asyncio async def test_resumable_upload(self, auth_headers) -> None: """测试断点续传功能""" transport = ASGITransport(app=app) async with AsyncClient(transport=transport, base_url="http://test") as client: # 初始化上传 init_response = await client.post( "/api/v1/videos/upload/init", json={ "filename": "large_video.mp4", "file_size": 50 * 1024 * 1024, "task_id": "task_001" }, headers=auth_headers ) assert init_response.status_code == 200 upload_id = init_response.json()["upload_id"] # 上传分片 chunk_response = await client.post( f"/api/v1/videos/upload/{upload_id}/chunk", files={"chunk": ("chunk_0", b"x" * 1024 * 1024)}, data={"chunk_index": 0}, headers=auth_headers ) assert chunk_response.status_code == 200 assert chunk_response.json()["received_chunks"] == 1 class TestVideoAuditAPI: """视频审核 API 测试""" @pytest.mark.integration @pytest.mark.asyncio async def test_get_audit_result_success(self, auth_headers) -> None: """测试获取审核结果成功""" transport = ASGITransport(app=app) async with AsyncClient(transport=transport, base_url="http://test") as client: response = await client.get( "/api/v1/videos/video_001/audit", headers=auth_headers ) assert response.status_code == 200 data = response.json() # 验证审核报告结构 assert "report_id" in data assert "video_id" in data assert "status" in data assert "violations" in data assert "brief_compliance" in data assert "processing_time_ms" in data @pytest.mark.integration @pytest.mark.asyncio async def test_get_audit_result_processing(self, auth_headers) -> None: """测试获取处理中的审核结果""" transport = ASGITransport(app=app) async with AsyncClient(transport=transport, base_url="http://test") as client: response = await client.get( "/api/v1/videos/video_processing/audit", headers=auth_headers ) assert response.status_code == 200 data = response.json() assert data["status"] == "processing" assert "progress" in data @pytest.mark.integration @pytest.mark.asyncio async def test_get_nonexistent_video_returns_404(self, auth_headers) -> None: """测试获取不存在的视频返回 404""" transport = ASGITransport(app=app) async with AsyncClient(transport=transport, base_url="http://test") as client: response = await client.get( "/api/v1/videos/nonexistent_id/audit", headers=auth_headers ) assert response.status_code == 404 class TestViolationEvidenceAPI: """违规证据 API 测试""" @pytest.mark.integration @pytest.mark.asyncio async def test_get_violation_evidence(self, auth_headers) -> None: """测试获取违规证据 - 包含截图和时间戳""" transport = ASGITransport(app=app) async with AsyncClient(transport=transport, base_url="http://test") as client: response = await client.get( "/api/v1/videos/video_001/violations/vio_001/evidence", headers=auth_headers ) assert response.status_code == 200 data = response.json() assert "violation_id" in data assert "evidence_type" in data assert "screenshot_url" in data assert "timestamp_start" in data assert "timestamp_end" in data assert "content" in data @pytest.mark.integration @pytest.mark.asyncio async def test_evidence_screenshot_accessible(self, auth_headers) -> None: """测试证据截图可访问""" # 截图访问需要静态文件服务,这里只验证 URL 格式 transport = ASGITransport(app=app) async with AsyncClient(transport=transport, base_url="http://test") as client: evidence_response = await client.get( "/api/v1/videos/video_001/violations/vio_001/evidence", headers=auth_headers ) screenshot_url = evidence_response.json()["screenshot_url"] assert screenshot_url.startswith("/static/screenshots/") class TestVideoPreviewAPI: """视频预览 API 测试""" @pytest.mark.integration @pytest.mark.asyncio async def test_get_video_preview_with_timestamp(self, auth_headers) -> None: """测试带时间戳的视频预览""" transport = ASGITransport(app=app) async with AsyncClient(transport=transport, base_url="http://test") as client: response = await client.get( "/api/v1/videos/video_001/preview", params={"start_ms": 5000, "end_ms": 10000}, headers=auth_headers ) assert response.status_code == 200 data = response.json() assert "preview_url" in data assert "start_ms" in data assert "end_ms" in data @pytest.mark.integration @pytest.mark.asyncio async def test_video_seek_to_violation(self, auth_headers) -> None: """测试视频跳转到违规时间点""" transport = ASGITransport(app=app) async with AsyncClient(transport=transport, base_url="http://test") as client: # 获取违规列表 violations_response = await client.get( "/api/v1/videos/video_001/violations", headers=auth_headers ) violations = violations_response.json()["violations"] # 每个违规项应包含可跳转的时间戳 for violation in violations: assert "timestamp_start" in violation assert violation["timestamp_start"] >= 0 class TestVideoResubmitAPI: """视频重新提交 API 测试""" @pytest.mark.integration @pytest.mark.asyncio async def test_resubmit_video_success(self, auth_headers) -> None: """测试重新提交视频""" transport = ASGITransport(app=app) async with AsyncClient(transport=transport, base_url="http://test") as client: response = await client.post( "/api/v1/videos/video_001/resubmit", json={ "modification_note": "已修改违规内容", "modified_sections": ["00:05-00:10"] }, headers=auth_headers ) assert response.status_code == 202 data = response.json() assert data["status"] == "processing" assert "new_video_id" in data @pytest.mark.integration @pytest.mark.asyncio async def test_resubmit_without_modification_note(self, auth_headers) -> None: """测试无修改说明的重新提交""" transport = ASGITransport(app=app) async with AsyncClient(transport=transport, base_url="http://test") as client: response = await client.post( "/api/v1/videos/video_001/resubmit", json={}, headers=auth_headers ) # 应该允许不提供修改说明 assert response.status_code == 202 class TestVideoListAPI: """视频列表 API 测试""" @pytest.mark.integration @pytest.mark.asyncio async def test_list_videos_with_pagination(self, auth_headers) -> None: """测试视频列表分页""" transport = ASGITransport(app=app) async with AsyncClient(transport=transport, base_url="http://test") as client: response = await client.get( "/api/v1/videos", params={"page": 1, "page_size": 10}, headers=auth_headers ) assert response.status_code == 200 data = response.json() assert "items" in data assert "total" in data assert "page" in data assert "page_size" in data assert len(data["items"]) <= 10 @pytest.mark.integration @pytest.mark.asyncio async def test_list_videos_filter_by_status(self, auth_headers) -> None: """测试按状态筛选视频""" transport = ASGITransport(app=app) async with AsyncClient(transport=transport, base_url="http://test") as client: response = await client.get( "/api/v1/videos", params={"status": "completed"}, headers=auth_headers ) assert response.status_code == 200 data = response.json() for item in data["items"]: assert item["status"] == "completed" @pytest.mark.integration @pytest.mark.asyncio async def test_list_videos_filter_by_task(self, auth_headers) -> None: """测试按任务筛选视频""" transport = ASGITransport(app=app) async with AsyncClient(transport=transport, base_url="http://test") as client: response = await client.get( "/api/v1/videos", params={"task_id": "task_001"}, headers=auth_headers ) assert response.status_code == 200 data = response.json() for item in data["items"]: assert item["task_id"] == "task_001"