fix: #317 CI修复 - asset_analyzer缩进 + TTS单测SSRF兼容
CI/CD Pipeline / Validate Code Quality And Tests (pull_request) Failing after 29s
CI/CD Pipeline / Production Browser E2E (pull_request) Failing after 1534h16m34s
CI/CD Pipeline / Staging E2E Tests (pull_request) Failing after 1534h16m35s
CI/CD Pipeline / Deploy Production (pull_request) Failing after 1534h16m35s
CI/CD Pipeline / Build Production Runtime Images (pull_request) Failing after 1534h16m36s
CI/CD Pipeline / Build & Push Staging (Watchtower auto-deploy) (pull_request) Failing after 1534h16m36s
CI/CD Pipeline / Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / Frontend Lint (pull_request) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (pull_request) Failing after 1534h48m14s

P0-1: asset_analyzer.py 缩进错误
- 第147行起23行缩进多了一层(改subprocess时弄乱)
- 修正 streams/format_info/for循环 等缩进

P0-2: TTS单测因SSRF接入挂了8个
- 根因:测试mock httpx,但safe_download_*在发请求前做DNS解析
- 修复:将httpx mock替换为safe_download_bytes/safe_download_file mock
- test_tts_oss_transfer.py: 4个失败修复,mock safe_download_bytes
- test_tts_segment_synthesis.py: 3个失败修复,mock safe_download_file
- test_tts_streaming.py: 1个失败修复,mock safe_download_bytes
- 安全层单测保持独立,33个test_url_security.py全绿

76个相关测试全绿(9+24+10+33)
This commit is contained in:
CI Bot
2026-07-14 17:11:42 +08:00
parent 2f03e13928
commit 745ea06659
4 changed files with 62 additions and 90 deletions
+19 -19
View File
@@ -144,29 +144,29 @@ class AssetAnalyzer:
]
stdout, _ = run_ffprobe(cmd, timeout=30)
data = json.loads(stdout)
streams = data.get("streams", [])
format_info = data.get("format", {})
streams = data.get("streams", [])
format_info = data.get("format", {})
for stream in streams:
if stream.get("codec_type") == "video":
info.width = int(stream.get("width", 0))
info.height = int(stream.get("height", 0))
info.codec = stream.get("codec_name", "")
for stream in streams:
if stream.get("codec_type") == "video":
info.width = int(stream.get("width", 0))
info.height = int(stream.get("height", 0))
info.codec = stream.get("codec_name", "")
# 解析帧率
fps_str = stream.get("r_frame_rate", "0/1")
if "/" in fps_str:
num, denom = fps_str.split("/")
info.fps = float(num) / float(denom) if float(denom) != 0 else 0.0
else:
info.fps = float(fps_str)
# 解析帧率
fps_str = stream.get("r_frame_rate", "0/1")
if "/" in fps_str:
num, denom = fps_str.split("/")
info.fps = float(num) / float(denom) if float(denom) != 0 else 0.0
else:
info.fps = float(fps_str)
elif stream.get("codec_type") == "audio":
info.has_audio = True
elif stream.get("codec_type") == "audio":
info.has_audio = True
info.duration = float(format_info.get("duration", 0))
info.bitrate = int(format_info.get("bit_rate", 0))
info.file_size = int(format_info.get("size", 0))
info.duration = float(format_info.get("duration", 0))
info.bitrate = int(format_info.get("bit_rate", 0))
info.file_size = int(format_info.get("size", 0))
except Exception as e:
logger.warning(f"Failed to get video info: {e}")
+28 -41
View File
@@ -7,6 +7,7 @@
from __future__ import annotations
from datetime import datetime, timezone
import unittest
from unittest.mock import MagicMock, patch
from packages.application.cosyvoice_service import CosyVoiceService
@@ -67,13 +68,10 @@ def _make_workflow(
class TestTransferAudioToOSS:
"""测试 _transfer_audio_to_oss 方法。"""
@patch("packages.application.tts_job.workflow.httpx")
def test_success_download_and_upload(self, mock_httpx: MagicMock) -> None:
@patch("packages.application.tts_job.workflow.safe_download_bytes")
def test_success_download_and_upload(self, mock_download: MagicMock) -> None:
"""成功下载音频并上传到 OSS,返回永久 URL 和 storage_key。"""
mock_resp = MagicMock()
mock_resp.content = b"fake audio data"
mock_resp.raise_for_status.return_value = None
mock_httpx.get.return_value = mock_resp
mock_download.return_value = b"fake audio data"
storage = MagicMock()
storage.upload_file.return_value = "https://oss.example.com/tts-outputs/user_001/job_123.mp3"
@@ -89,20 +87,21 @@ class TestTransferAudioToOSS:
assert url == "https://oss.example.com/tts-outputs/user_001/job_123.mp3"
assert key == "tts-outputs/user_001/job_123.mp3"
mock_httpx.get.assert_called_once_with(
mock_download.assert_called_once_with(
"https://cosyvoice-temp.com/audio.mp3",
purpose="tts_audio_download",
allowed_mime_types=unittest.mock.ANY,
timeout=60.0,
follow_redirects=True,
)
storage.upload_file.assert_called_once()
call_args = storage.upload_file.call_args
assert call_args[0][1] == "tts-outputs/user_001/job_123.mp3"
assert call_args[1]["content_type"] == "audio/mpeg"
@patch("packages.application.tts_job.workflow.httpx")
def test_download_failure_fallback(self, mock_httpx: MagicMock) -> None:
@patch("packages.application.tts_job.workflow.safe_download_bytes")
def test_download_failure_fallback(self, mock_download: MagicMock) -> None:
"""下载失败时回退到原始临时 URL,storage_key 为空。"""
mock_httpx.get.side_effect = Exception("Network error")
mock_download.side_effect = Exception("Network error")
workflow = _make_workflow()
url, key = workflow._transfer_audio_to_oss(
@@ -114,13 +113,10 @@ class TestTransferAudioToOSS:
assert url == "https://cosyvoice-temp.com/audio.mp3"
assert key == ""
@patch("packages.application.tts_job.workflow.httpx")
def test_upload_failure_fallback(self, mock_httpx: MagicMock) -> None:
@patch("packages.application.tts_job.workflow.safe_download_bytes")
def test_upload_failure_fallback(self, mock_download: MagicMock) -> None:
"""上传 OSS 失败时回退到原始临时 URL。"""
mock_resp = MagicMock()
mock_resp.content = b"fake audio data"
mock_resp.raise_for_status.return_value = None
mock_httpx.get.return_value = mock_resp
mock_download.return_value = b"fake audio data"
storage = MagicMock()
storage.upload_file.side_effect = Exception("OSS bucket error")
@@ -135,13 +131,10 @@ class TestTransferAudioToOSS:
assert url == "https://cosyvoice-temp.com/audio.mp3"
assert key == ""
@patch("packages.application.tts_job.workflow.httpx")
def test_wav_content_type(self, mock_httpx: MagicMock) -> None:
@patch("packages.application.tts_job.workflow.safe_download_bytes")
def test_wav_content_type(self, mock_download: MagicMock) -> None:
"""wav 格式使用正确的 content_type。"""
mock_resp = MagicMock()
mock_resp.content = b"fake wav data"
mock_resp.raise_for_status.return_value = None
mock_httpx.get.return_value = mock_resp
mock_download.return_value = b"fake wav data"
storage = MagicMock()
storage.upload_file.return_value = "https://oss.example.com/audio.wav"
@@ -162,13 +155,10 @@ class TestTransferAudioToOSS:
class TestProcessSynthesisResultWithOSS:
"""测试 process_synthesis_result 集成 OSS 转存。"""
@patch("packages.application.tts_job.workflow.httpx")
def test_stores_permanent_url_and_key(self, mock_httpx: MagicMock) -> None:
@patch("packages.application.tts_job.workflow.safe_download_bytes")
def test_stores_permanent_url_and_key(self, mock_download: MagicMock) -> None:
"""合成结果存 OSS 永久 URL 和 storage_key。"""
mock_resp = MagicMock()
mock_resp.content = b"audio bytes"
mock_resp.raise_for_status.return_value = None
mock_httpx.get.return_value = mock_resp
mock_download.return_value = b"audio bytes"
storage = MagicMock()
storage.upload_file.return_value = "https://oss.example.com/tts-outputs/user_001/test_job_001.mp3"
@@ -192,10 +182,10 @@ class TestProcessSynthesisResultWithOSS:
assert result.duration == 5.0
assert result.file_size == 50000
@patch("packages.application.tts_job.workflow.httpx")
def test_fallback_to_temp_url_on_oss_failure(self, mock_httpx: MagicMock) -> None:
@patch("packages.application.tts_job.workflow.safe_download_bytes")
def test_fallback_to_temp_url_on_oss_failure(self, mock_download: MagicMock) -> None:
"""OSS 转存失败时,使用 CosyVoice 临时 URL(不阻塞合成流程)。"""
mock_httpx.get.side_effect = Exception("Download failed")
mock_download.side_effect = Exception("Download failed")
repo = MagicMock()
job = _make_job(status=TTSJobStatus.PROCESSING)
@@ -216,13 +206,10 @@ class TestProcessSynthesisResultWithOSS:
class TestStartSynthesisSyncWithOSS:
"""测试 start_synthesis 同步路径的 OSS 转存。"""
@patch("packages.application.tts_job.workflow.httpx")
def test_sync_path_transfers_to_oss(self, mock_httpx: MagicMock) -> None:
@patch("packages.application.tts_job.workflow.safe_download_bytes")
def test_sync_path_transfers_to_oss(self, mock_download: MagicMock) -> None:
"""CosyVoice 同步返回 audio_url 时,也走 OSS 转存。"""
mock_resp = MagicMock()
mock_resp.content = b"sync audio bytes"
mock_resp.raise_for_status.return_value = None
mock_httpx.get.return_value = mock_resp
mock_download.return_value = b"sync audio bytes"
storage = MagicMock()
storage.upload_file.return_value = "https://oss.example.com/tts-outputs/user_001/test_job_001.mp3"
@@ -248,10 +235,10 @@ class TestStartSynthesisSyncWithOSS:
assert job.output_audio_key == "tts-outputs/user_001/test_job_001.mp3"
assert job.duration == 2.0
@patch("packages.application.tts_job.workflow.httpx")
def test_sync_path_oss_failure_stores_temp_url(self, mock_httpx: MagicMock) -> None:
@patch("packages.application.tts_job.workflow.safe_download_bytes")
def test_sync_path_oss_failure_stores_temp_url(self, mock_download: MagicMock) -> None:
"""同步路径 OSS 失败时,降级存储临时 URL。"""
mock_httpx.get.side_effect = Exception("Network error")
mock_download.side_effect = Exception("Network error")
service = MagicMock(spec=CosyVoiceService)
service.submit_synthesize_task.return_value = {
+11 -23
View File
@@ -233,14 +233,11 @@ class TestStartSegmentSynthesis:
# 短文本走普通路径,不调用分段
assert job.status == TTSJobStatus.PROCESSING
@patch("packages.application.tts_job.workflow.httpx")
def test_long_text_sync_segments(self, mock_httpx: MagicMock) -> None:
@patch("packages.application.tts_job.workflow.safe_download_file")
def test_long_text_sync_segments(self, mock_download: MagicMock) -> None:
"""长文本同步分段:所有段立即返回 audio_url,直接合并。"""
# Mock 分段音频下载
mock_resp = MagicMock()
mock_resp.content = b"segment audio"
mock_resp.raise_for_status.return_value = None
mock_httpx.get.return_value = mock_resp
mock_download.return_value = 1024 # 模拟文件大小
service = MagicMock(spec=CosyVoiceService)
# 每个分段都同步返回 audio_url
@@ -355,14 +352,11 @@ class TestHandleSegmentFailure:
class TestPollSegmentTasks:
"""测试 _poll_segment_tasks 分段缺失重新合成(适配同步接口)。"""
@patch("packages.application.tts_job.workflow.httpx")
def test_all_segments_done(self, mock_httpx: MagicMock) -> None:
@patch("packages.application.tts_job.workflow.safe_download_file")
def test_all_segments_done(self, mock_download: MagicMock) -> None:
"""所有分段缺少 audio_url 时重新同步合成,合并后标记完成。"""
# Mock 下载分段音频
mock_resp = MagicMock()
mock_resp.content = b"seg audio"
mock_resp.raise_for_status.return_value = None
mock_httpx.get.return_value = mock_resp
mock_download.return_value = 1024 # 模拟文件大小
service = MagicMock(spec=CosyVoiceService)
service.submit_synthesize_task.side_effect = [
@@ -422,13 +416,10 @@ class TestPollSegmentTasks:
assert result.status == TTSJobStatus.FAILED
@patch("packages.application.tts_job.workflow.httpx")
def test_partial_audio_urls_reuse_existing(self, mock_httpx: MagicMock) -> None:
@patch("packages.application.tts_job.workflow.safe_download_file")
def test_partial_audio_urls_reuse_existing(self, mock_download: MagicMock) -> None:
"""部分分段已有 audio_url 时直接复用,缺失的重新合成。"""
mock_resp = MagicMock()
mock_resp.content = b"seg audio"
mock_resp.raise_for_status.return_value = None
mock_httpx.get.return_value = mock_resp
mock_download.return_value = 1024 # 模拟文件大小
service = MagicMock(spec=CosyVoiceService)
# 只有 1 个分段需要重新合成
@@ -515,11 +506,8 @@ class TestPollAndProcessSynthesisSegmentDetection:
workflow = _make_workflow(cosyvoice_service=service, repo=repo, storage=storage)
with patch("packages.application.tts_job.workflow.httpx") as mock_httpx:
mock_resp = MagicMock()
mock_resp.content = b"audio"
mock_resp.raise_for_status.return_value = None
mock_httpx.get.return_value = mock_resp
with patch("packages.application.tts_job.workflow.safe_download_file") as mock_download:
mock_download.return_value = 1024 # 模拟文件大小
result = workflow.poll_and_process_synthesis("test_job_seg")
Regular → Executable
+4 -7
View File
@@ -221,17 +221,14 @@ class TestTTSStreamingService:
@pytest.mark.asyncio
async def test_download_audio(self):
"""下载音频数据。"""
"""下载音频数据(SSRF防护走safe_download_bytes,mock掉安全层)。"""
cosyvoice = MagicMock(spec=CosyVoiceService)
service = TTSStreamingService(cosyvoice)
with patch("packages.application.tts_job.streaming_service.httpx") as mock_httpx:
mock_resp = MagicMock()
mock_resp.content = b"audio data"
mock_resp.raise_for_status.return_value = None
mock_httpx.get.return_value = mock_resp
with patch("packages.application.tts_job.streaming_service.safe_download_bytes") as mock_download:
mock_download.return_value = b"audio data"
result = service._download_audio("https://example.com/audio.mp3")
assert result == b"audio data"
mock_httpx.get.assert_called_once()
mock_download.assert_called_once()