Files
xiaoxia-saas/tests/unit/test_mediakit_cover.py
CI Bot cb1ba46d30
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / PR Build API Image (push) Has been skipped
CI/CD Pipeline / PR Build Web Image (push) Has been skipped
CI/CD Pipeline / PR Build Worker Image (push) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 1m27s
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 2m24s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 2m29s
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 2m37s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 5m7s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 7m50s
CI/CD Pipeline / Unit Tests (push) Successful in 10m30s
CI/CD Pipeline / Build Production API Image (push) Has been skipped
CI/CD Pipeline / Build Production Worker Image (push) Has been skipped
CI/CD Pipeline / Build Production Web Image (push) Has been skipped
CI/CD Pipeline / Deploy Production (push) Has been skipped
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Integration Tests (push) Successful in 3m38s
CI/CD Pipeline / CI Gate (push) Has been skipped
CI/CD Pipeline / Build Staging API Image (push) Successful in 12m56s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 54s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 39s
CI/CD Pipeline / Staging E2E Tests (push) Successful in 44s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 2m47s
CI/CD Pipeline / Canary Release to Production (push) Has been skipped
style: auto-format with black + isort + prettier [skip ci-format-check]
2026-08-11 14:42:39 +00:00

313 lines
12 KiB
Python
Executable File
Raw Permalink 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.
"""MediaKit AI 封面生成单元测试.
测试 #1208: AI封面接入MediaKit视频截帧
"""
from unittest.mock import MagicMock, Mock, patch
import pytest
from packages.shared.mediakit_client import MediaKitClient, get_mediakit_client
class TestMediaKitClient:
"""MediaKit 客户端测试."""
def test_is_available_with_key(self):
"""配置了 API Key 时可用."""
with patch("packages.shared.mediakit_client.get_shared_settings") as mock_settings:
mock_settings.return_value.mediakit_api_key = "test-key"
mock_settings.return_value.mediakit_base_url = "https://test.com/api/v1"
mock_settings.return_value.mediakit_timeout = 60
client = MediaKitClient()
assert client.is_available is True
def test_is_available_without_key(self):
"""未配置 API Key 时不可用."""
with patch("packages.shared.mediakit_client.get_shared_settings") as mock_settings:
mock_settings.return_value.mediakit_api_key = ""
mock_settings.return_value.mediakit_base_url = "https://test.com/api/v1"
mock_settings.return_value.mediakit_timeout = 60
client = MediaKitClient()
assert client.is_available is False
@patch("packages.shared.mediakit_client.httpx.post")
@patch("packages.shared.mediakit_client.httpx.get")
def test_extract_frames_success(self, mock_get, mock_post):
"""抽帧成功."""
with patch("packages.shared.mediakit_client.get_shared_settings") as mock_settings:
mock_settings.return_value.mediakit_api_key = "test-key"
mock_settings.return_value.mediakit_base_url = "https://test.com/api/v1"
mock_settings.return_value.mediakit_timeout = 60
# Mock submit task
mock_post.return_value.status_code = 200
mock_post.return_value.json.return_value = {"task_id": "task-123"}
mock_post.return_value.raise_for_status = Mock()
# Mock poll task
mock_get.return_value.status_code = 200
mock_get.return_value.json.return_value = {
"status": "success",
"result": {
"snapshots": [
{"image_url": "https://example.com/frame1.jpg", "timestamp": 2.5},
{"image_url": "https://example.com/frame2.jpg", "timestamp": 5.0},
]
},
}
mock_get.return_value.raise_for_status = Mock()
client = MediaKitClient()
frames = client.extract_frames(
video_url="https://example.com/video.mp4",
strategy="SceneChange",
max_frames=5,
)
assert frames is not None
assert len(frames) == 2
assert frames[0]["image_url"] == "https://example.com/frame1.jpg"
assert frames[0]["timestamp"] == 2.5
@patch("packages.shared.mediakit_client.httpx.post")
def test_extract_frames_submit_failed(self, mock_post):
"""提交任务失败."""
with patch("packages.shared.mediakit_client.get_shared_settings") as mock_settings:
mock_settings.return_value.mediakit_api_key = "test-key"
mock_settings.return_value.mediakit_base_url = "https://test.com/api/v1"
mock_settings.return_value.mediakit_timeout = 60
mock_post.side_effect = Exception("Network error")
client = MediaKitClient()
frames = client.extract_frames(
video_url="https://example.com/video.mp4",
strategy="SceneChange",
max_frames=5,
)
assert frames is None
@patch("packages.shared.mediakit_client.httpx.post")
@patch("packages.shared.mediakit_client.httpx.get")
def test_extract_frames_task_failed(self, mock_get, mock_post):
"""任务执行失败."""
with patch("packages.shared.mediakit_client.get_shared_settings") as mock_settings:
mock_settings.return_value.mediakit_api_key = "test-key"
mock_settings.return_value.mediakit_base_url = "https://test.com/api/v1"
mock_settings.return_value.mediakit_timeout = 60
# Mock submit task
mock_post.return_value.status_code = 200
mock_post.return_value.json.return_value = {"task_id": "task-123"}
mock_post.return_value.raise_for_status = Mock()
# Mock poll task - failed
mock_get.return_value.status_code = 200
mock_get.return_value.json.return_value = {
"status": "failed",
"error": "Video processing failed",
}
mock_get.return_value.raise_for_status = Mock()
client = MediaKitClient()
frames = client.extract_frames(
video_url="https://example.com/video.mp4",
strategy="SceneChange",
max_frames=5,
)
assert frames is None
def test_extract_frames_not_available(self):
"""客户端不可用时返回 None."""
with patch("packages.shared.mediakit_client.get_shared_settings") as mock_settings:
mock_settings.return_value.mediakit_api_key = ""
mock_settings.return_value.mediakit_base_url = "https://test.com/api/v1"
mock_settings.return_value.mediakit_timeout = 60
client = MediaKitClient()
frames = client.extract_frames(
video_url="https://example.com/video.mp4",
strategy="SceneChange",
max_frames=5,
)
assert frames is None
class TestAICoverService:
"""AI 封面服务测试."""
@patch("packages.shared.mediakit_client.get_mediakit_client")
def test_call_ai_cover_with_mediakit_success(self, mock_get_client):
"""MediaKit 抽帧成功."""
mock_client = Mock()
mock_client.is_available = True
mock_client.extract_frames.return_value = [{"image_url": "https://example.com/frame.jpg", "timestamp": 3.5}]
mock_get_client.return_value = mock_client
from packages.shared.ai_service import _call_ai_cover_service
result = _call_ai_cover_service(
plan_id="plan-123",
asset_ids=["asset-1"],
cover_type="ai_frame",
primary_video_url="https://example.com/video.mp4",
)
assert result["type"] == "ai_frame"
assert result["image_url"] == "https://example.com/frame.jpg"
assert result["frame_time"] == 3.5
assert result["confidence"] == 0.85
mock_client.extract_frames.assert_called_once()
@patch("packages.shared.mediakit_client.get_mediakit_client")
def test_call_ai_cover_with_mediakit_failure_raises(self, mock_get_client):
"""MediaKit 失败时抛出 RuntimeError(不再降级到 stub."""
mock_client = Mock()
mock_client.is_available = True
mock_client.extract_frames.side_effect = Exception("API error")
mock_get_client.return_value = mock_client
from packages.shared.ai_service import _call_ai_cover_service
with pytest.raises(RuntimeError, match="MediaKit"):
_call_ai_cover_service(
plan_id="plan-123",
asset_ids=["asset-1"],
cover_type="ai_frame",
primary_video_url="https://example.com/video.mp4",
)
def test_call_ai_cover_without_video_url_raises(self):
"""没有视频 URL 时抛出 RuntimeError(不再降级到 stub."""
from packages.shared.ai_service import _call_ai_cover_service
with pytest.raises(RuntimeError, match="MediaKit"):
_call_ai_cover_service(
plan_id="plan-123",
asset_ids=["asset-1"],
cover_type="ai_frame",
primary_video_url=None,
)
def test_call_ai_cover_upload_type(self):
"""upload 类型直接返回."""
from packages.shared.ai_service import _call_ai_cover_service
result = _call_ai_cover_service(
plan_id="plan-123",
asset_ids=["asset-1"],
cover_type="upload",
primary_video_url="https://example.com/video.mp4",
)
assert result["type"] == "upload"
assert result["image_url"] == ""
def test_call_ai_cover_manual_type(self):
"""manual 类型返回指定时间点."""
from packages.shared.ai_service import _call_ai_cover_service
result = _call_ai_cover_service(
plan_id="plan-123",
asset_ids=["asset-1"],
cover_type="manual",
frame_time=5.0,
primary_video_url="https://example.com/video.mp4",
)
assert result["type"] == "manual"
assert result["frame_time"] == 5.0
@patch("packages.shared.mediakit_client.get_mediakit_client")
def test_call_ai_cover_mediakit_not_available_raises(self, mock_get_client):
"""MediaKit 未配置时抛出 RuntimeError(不再降级到 stub."""
mock_client = Mock()
mock_client.is_available = False
mock_get_client.return_value = mock_client
from packages.shared.ai_service import _call_ai_cover_service
with pytest.raises(RuntimeError, match="MediaKit"):
_call_ai_cover_service(
plan_id="plan-123",
asset_ids=["asset-1"],
cover_type="ai_frame",
primary_video_url="https://example.com/video.mp4",
)
@patch("packages.shared.mediakit_client.get_mediakit_client")
def test_call_ai_cover_empty_frames_raises(self, mock_get_client):
"""MediaKit 返回空帧列表时抛出 RuntimeError(不再降级)."""
mock_client = Mock()
mock_client.is_available = True
mock_client.extract_frames.return_value = []
mock_get_client.return_value = mock_client
from packages.shared.ai_service import _call_ai_cover_service
with pytest.raises(RuntimeError, match="MediaKit"):
_call_ai_cover_service(
plan_id="plan-123",
asset_ids=["asset-1"],
cover_type="ai_frame",
primary_video_url="https://example.com/video.mp4",
)
class TestGenerateCover:
"""run_generate_cover 测试."""
@patch("packages.shared.ai_service._call_ai_cover_service")
def test_run_generate_cover_passes_video_url(self, mock_cover_service):
"""正确传递 video_url 参数."""
mock_cover_service.return_value = {
"type": "ai_frame",
"image_url": "https://example.com/cover.jpg",
"frame_time": 2.0,
"confidence": 0.9,
}
from packages.shared.ai_service import run_generate_cover
result = run_generate_cover(
plan_id="plan-123",
asset_ids=["asset-1"],
cover_type="ai_frame",
primary_video_url="https://example.com/video.mp4",
)
assert result["type"] == "ai_frame"
mock_cover_service.assert_called_once()
call_kwargs = mock_cover_service.call_args[1]
assert call_kwargs["primary_video_url"] == "https://example.com/video.mp4"
@patch("packages.shared.ai_service._call_ai_cover_service")
def test_run_generate_cover_without_video_url(self, mock_cover_service):
"""不传 video_url 时默认为 None."""
mock_cover_service.return_value = {
"type": "ai_frame",
"image_url": "https://example.com/cover.jpg",
"frame_time": 2.0,
"confidence": 0.9,
}
from packages.shared.ai_service import run_generate_cover
result = run_generate_cover(
plan_id="plan-123",
asset_ids=["asset-1"],
cover_type="ai_frame",
)
assert result["type"] == "ai_frame"
call_kwargs = mock_cover_service.call_args[1]
assert call_kwargs["primary_video_url"] is None