Files
xiaoxia-saas/tests/unit/test_audio_merger_security.py
T
xiaoxia 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) (#332)
fix(security): 后端安全技术债务第二轮(P1+P2+P2)

- audio_merger裸subprocess下沉 + ffmpeg_utils架构下沉到packages/shared
- _verify_url_accessible重定向每跳SSRF校验
- url_security下载文件魔数校验
- 新增33个单测
2026-07-15 00:12:04 +08:00

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 统一入口"