fe2ab121e7
CI/CD Pipeline / Validate Code Quality And Tests (push) Failing after 40s
CI/CD Pipeline / Unit Tests (push) Failing after 1m42s
CI/CD Pipeline / Integration Tests (push) Successful in 1m20s
CI/CD Pipeline / Frontend Lint (push) Has been cancelled
CI/CD Pipeline / Build Staging API Image (push) Has been cancelled
CI/CD Pipeline / Build Staging Worker Image (push) Has been cancelled
CI/CD Pipeline / Build Staging Web Image (push) Has been cancelled
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Has been cancelled
CI/CD Pipeline / Staging E2E Tests (push) Has been cancelled
CI/CD Pipeline / Staging API Integration Tests (push) Has been cancelled
CI/CD Pipeline / Build Production API Image (push) Has been cancelled
CI/CD Pipeline / Build Production Worker Image (push) Has been cancelled
CI/CD Pipeline / Build Production Web Image (push) Has been cancelled
CI/CD Pipeline / Deploy Production (push) Has been cancelled
CI/CD Pipeline / Production Browser E2E (push) Has been cancelled
fix(security): 后端安全技术债务第二轮(P1+P2+P2) - audio_merger裸subprocess下沉 + ffmpeg_utils架构下沉到packages/shared - _verify_url_accessible重定向每跳SSRF校验 - url_security下载文件魔数校验 - 新增33个单测
141 lines
5.2 KiB
Python
Executable File
141 lines
5.2 KiB
Python
Executable File
"""AudioMerger 单元测试 — P1 裸subprocess下沉验证.
|
|
|
|
验证 AudioMerger 使用 shared.ffmpeg_utils.run_ffmpeg 统一入口,
|
|
不再直接调用 subprocess.run。
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
import subprocess
|
|
import tempfile
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
import pytest
|
|
from application.tts_job.audio_merger import AudioMergeError, AudioMerger
|
|
|
|
|
|
class TestAudioMergerUsesRunFfmpeg:
|
|
"""验证 AudioMerger 使用 run_ffmpeg 统一入口,而非裸 subprocess."""
|
|
|
|
def test_single_file_does_not_call_ffmpeg(self):
|
|
"""单文件时直接读取,不调用 FFmpeg."""
|
|
merger = AudioMerger()
|
|
with tempfile.NamedTemporaryFile(suffix=".mp3", delete=False) as f:
|
|
f.write(b"fake audio data")
|
|
path = f.name
|
|
|
|
try:
|
|
with patch("application.tts_job.audio_merger.run_ffmpeg") as mock_run:
|
|
result = merger.merge([path])
|
|
mock_run.assert_not_called()
|
|
assert result == b"fake audio data"
|
|
finally:
|
|
os.unlink(path)
|
|
|
|
def test_multiple_files_calls_run_ffmpeg(self):
|
|
"""多文件时调用 run_ffmpeg 合并。"""
|
|
merger = AudioMerger()
|
|
paths = []
|
|
for i in range(2):
|
|
f = tempfile.NamedTemporaryFile(suffix=".mp3", delete=False)
|
|
f.write(f"audio{i}".encode())
|
|
f.close()
|
|
paths.append(f.name)
|
|
|
|
try:
|
|
with patch("application.tts_job.audio_merger.run_ffmpeg") as mock_run:
|
|
# run_ffmpeg 成功返回,模拟合并完成
|
|
# 需要让 output_path 文件存在,否则 read 会报错
|
|
def fake_run_ffmpeg(cmd, **kwargs):
|
|
# 找到 output_path(命令最后一个参数)
|
|
output_path = cmd[-1]
|
|
with open(output_path, "wb") as out:
|
|
out.write(b"merged audio")
|
|
return ("", "")
|
|
|
|
mock_run.side_effect = fake_run_ffmpeg
|
|
result = merger.merge(paths)
|
|
|
|
mock_run.assert_called_once()
|
|
call_args = mock_run.call_args[0][0]
|
|
# 验证使用了 FFMPEG_BIN 而非硬编码 "ffmpeg"
|
|
from shared.ffmpeg_utils import FFMPEG_BIN
|
|
|
|
assert call_args[0] == FFMPEG_BIN
|
|
# 验证使用 concat demuxer
|
|
assert "concat" in call_args
|
|
assert result == b"merged audio"
|
|
finally:
|
|
for p in paths:
|
|
os.unlink(p)
|
|
|
|
def test_ffmpeg_failure_raises_audio_merge_error(self):
|
|
"""FFmpeg 失败时抛出 AudioMergeError."""
|
|
merger = AudioMerger()
|
|
paths = []
|
|
for i in range(2):
|
|
f = tempfile.NamedTemporaryFile(suffix=".mp3", delete=False)
|
|
f.write(f"audio{i}".encode())
|
|
f.close()
|
|
paths.append(f.name)
|
|
|
|
try:
|
|
with patch("application.tts_job.audio_merger.run_ffmpeg") as mock_run:
|
|
mock_run.side_effect = subprocess.CalledProcessError(
|
|
returncode=1, cmd=["ffmpeg"], stderr="concat error"
|
|
)
|
|
|
|
with pytest.raises(AudioMergeError, match="FFmpeg 合并失败"):
|
|
merger.merge(paths)
|
|
|
|
mock_run.assert_called_once()
|
|
finally:
|
|
for p in paths:
|
|
os.unlink(p)
|
|
|
|
def test_ffmpeg_timeout_raises_audio_merge_error(self):
|
|
"""FFmpeg 超时时抛出 AudioMergeError."""
|
|
merger = AudioMerger()
|
|
paths = []
|
|
for i in range(2):
|
|
f = tempfile.NamedTemporaryFile(suffix=".mp3", delete=False)
|
|
f.write(f"audio{i}".encode())
|
|
f.close()
|
|
paths.append(f.name)
|
|
|
|
try:
|
|
with patch("application.tts_job.audio_merger.run_ffmpeg") as mock_run:
|
|
mock_run.side_effect = subprocess.TimeoutExpired(cmd=["ffmpeg"], timeout=120)
|
|
|
|
with pytest.raises(AudioMergeError, match="超时"):
|
|
merger.merge(paths)
|
|
|
|
mock_run.assert_called_once()
|
|
finally:
|
|
for p in paths:
|
|
os.unlink(p)
|
|
|
|
def test_empty_list_raises_error(self):
|
|
"""空列表时直接抛错,不调用 ffmpeg."""
|
|
merger = AudioMerger()
|
|
with patch("application.tts_job.audio_merger.run_ffmpeg") as mock_run:
|
|
with pytest.raises(AudioMergeError, match="没有可合并的音频文件"):
|
|
merger.merge([])
|
|
mock_run.assert_not_called()
|
|
|
|
def test_no_direct_subprocess_import(self):
|
|
"""验证 audio_merger 模块不直接 import subprocess(通过模块源码检查)。"""
|
|
import inspect
|
|
|
|
import application.tts_job.audio_merger as am_module
|
|
|
|
source = inspect.getsource(am_module)
|
|
# 不应该有 "import subprocess" 整行
|
|
src_lines = [line.strip() for line in source.split("\n") if line.strip()]
|
|
# 允许 from subprocess import CalledProcessError, TimeoutExpired(只导入异常类)
|
|
# 不允许直接 import subprocess
|
|
assert not any(
|
|
line == "import subprocess" for line in src_lines
|
|
), "audio_merger.py 不应直接 import subprocess,应通过 run_ffmpeg 统一入口"
|