test(douyin): 更新单元测试适配多源resolver架构,删除yt-dlp/cookies相关mock
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (push) Successful in 1s
CI/CD Pipeline / Check push changed paths (push) Successful in 2s
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / PR Build Web Image (push) Has been skipped
CI/CD Pipeline / PR Build API Image (push) Has been skipped
CI/CD Pipeline / PR Build Worker Image (push) Has been skipped
CI/CD Pipeline / Build Staging Web Image (push) Has been skipped
CI/CD Pipeline / Build Staging API Image (push) Successful in 34s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 2m20s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 2m53s
CI/CD Pipeline / Retag skipped Staging API Image (push) Has been skipped
CI/CD Pipeline / Retag skipped Staging Worker Image (push) Has been skipped
CI/CD Pipeline / Retag skipped Staging Web Image (push) Failing after 15s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Failing after 3m51s
CI/CD Pipeline / Staging E2E Tests (push) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (push) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (push) Has been skipped
CI/CD Pipeline / Integration Tests (push) Successful in 10m22s
CI/CD Pipeline / Build Production API Image (push) Has been cancelled
CI/CD Pipeline / Build Production Web Image (push) Has been cancelled
CI/CD Pipeline / Build Production Worker Image (push) Has been cancelled
CI/CD Pipeline / Deploy Production (push) Has been cancelled
CI/CD Pipeline / Production Browser E2E (push) Has been cancelled
CI/CD Pipeline / Canary Release to Production (push) Has been cancelled
CI/CD Pipeline / CI Gate (push) Has been cancelled
CI/CD Pipeline / Validate - Security (push) Has been cancelled
CI/CD Pipeline / Unit Tests (push) Has been cancelled
CI/CD Pipeline / Validate - Style (push) Has been cancelled
CI/CD Pipeline / Validate - Python (mypy + alembic) (push) Has been cancelled

This commit is contained in:
xiaoxia
2026-09-17 19:30:08 +08:00
parent 8fb75162d4
commit 9e0959cb85
2 changed files with 249 additions and 504 deletions
+71 -133
View File
@@ -42,72 +42,43 @@ def mock_db():
return MagicMock()
def _mock_youtube_dl(
extract_info_return=None,
extract_info_side_effect=None,
prepare_filename_return="/tmp/douyin_extract_abc/abc123.mp4",
def _mock_resolver(
video_url="https://example.com/direct.mp4",
desc="",
source="app_feed",
):
"""构造 yt_dlp.YoutubeDL 的 mock.
路由中用法: ydl = yt_dlp.YoutubeDL(opts); info = ydl.extract_info(...)
所以 mock_ydl_cls.return_value 就是 ydl 实例.
"""
mock_ydl_instance = MagicMock()
if extract_info_side_effect is not None:
mock_ydl_instance.extract_info.side_effect = extract_info_side_effect
else:
mock_ydl_instance.extract_info.return_value = extract_info_return or {
"id": "abc123",
"duration": 120.5,
}
mock_ydl_instance.prepare_filename.return_value = prepare_filename_return
return mock_ydl_instance
"""Mock resolve_douyin_video 返回 ResolveResult。"""
from app.services.douyin_resolver import ResolveResult
return ResolveResult(video_url=video_url, desc=desc, source=source)
# ── extract_from_douyin ──────────────────────────────────────────────────────
class TestExtractFromDouyin:
"""POST /extract-from-douyin 测试."""
"""POST /extract-from-douyin 测试(新版:resolver 多源轮询架构)。"""
@patch("app.api.routes.scripts_ai.get_mediakit_client")
@patch("app.api.routes.scripts_ai.transcribe_to_text")
@patch("tempfile.TemporaryDirectory")
@patch("yt_dlp.YoutubeDL")
@patch("app.api.routes.scripts_ai.os.path.getsize", return_value=1024)
@patch("app.api.routes.scripts_ai.os.path.isfile", return_value=True)
@patch("app.api.routes.scripts_ai._ytdlp_extract_video_url", return_value=(None, 0.0))
def test_extract_from_douyin_success(
@patch("app.api.routes.scripts_ai._direct_url_download_and_local_asr")
@patch("app.api.routes.scripts_ai.resolve_douyin_video")
def test_extract_from_douyin_success_mediakit(
self,
mock_meta,
mock_isfile,
mock_getsize,
mock_ydl_cls,
mock_tempdir,
mock_transcribe,
mock_resolve,
mock_local_asr,
mock_get_mk,
):
"""正常流程(MediaKit不可用,走本地下载+ASR):下载视频 + ASR 转写成功."""
"""正常流程(MediaKit ASR 成功):resolver → MediaKit → 返回文本。"""
from app.api.routes.scripts_ai import extract_from_douyin
from app.schemas.scripts_ai import ExtractFromDouyinRequest
mock_resolve.return_value = _mock_resolver(desc="Feed标题")
fake_mk = MagicMock()
fake_mk.is_available = False
fake_mk.is_available = True
fake_mk.asr_submit.return_value = "tk1"
fake_mk.asr_poll.return_value = ("这是一段测试文案内容", 120.5)
mock_get_mk.return_value = fake_mk
mock_ydl_cls.return_value = _mock_youtube_dl(
extract_info_return={"id": "abc123", "duration": 120.5},
)
mock_ydl_cls.return_value.__enter__ = MagicMock(return_value=mock_ydl_cls.return_value)
mock_ydl_cls.return_value.__exit__ = MagicMock(return_value=False)
mock_td = MagicMock()
mock_td.__enter__ = MagicMock(return_value="/tmp/douyin_extract_abc")
mock_td.__exit__ = MagicMock(return_value=False)
mock_tempdir.return_value = mock_td
mock_transcribe.return_value = "这是一段测试文案内容"
req = ExtractFromDouyinRequest(url="https://v.douyin.com/xxxxx/")
auth = _make_auth_user()
result = extract_from_douyin(request=req, current_user=auth, db=MagicMock())
@@ -115,7 +86,36 @@ class TestExtractFromDouyin:
assert result.text == "这是一段测试文案内容"
assert result.duration_seconds == 120.5
assert result.source_url == "https://v.douyin.com/xxxxx/"
mock_transcribe.assert_called_once()
mock_local_asr.assert_not_called()
@patch("app.api.routes.scripts_ai.get_mediakit_client")
@patch("app.api.routes.scripts_ai._direct_url_download_and_local_asr")
@patch("app.api.routes.scripts_ai.resolve_douyin_video")
def test_extract_from_douyin_success_local_asr(
self,
mock_resolve,
mock_local_asr,
mock_get_mk,
):
"""MediaKit 不可用时走本地下载+ASR。"""
from app.api.routes.scripts_ai import extract_from_douyin
from app.schemas.scripts_ai import ExtractFromDouyinRequest
mock_resolve.return_value = _mock_resolver()
fake_mk = MagicMock()
fake_mk.is_available = False
mock_get_mk.return_value = fake_mk
mock_local_asr.return_value = ("本地ASR识别文案", 30.0)
req = ExtractFromDouyinRequest(url="https://v.douyin.com/xxxxx/")
auth = _make_auth_user()
result = extract_from_douyin(request=req, current_user=auth, db=MagicMock())
assert result.text == "本地ASR识别文案"
assert result.duration_seconds == 30.0
mock_local_asr.assert_called_once()
@pytest.mark.parametrize(
"bad_url",
@@ -128,7 +128,7 @@ class TestExtractFromDouyin:
],
)
def test_extract_from_douyin_invalid_url(self, bad_url):
"""非法 URL 返回 400."""
"""非法 URL 返回 400。"""
from app.api.routes.scripts_ai import extract_from_douyin
from app.schemas.scripts_ai import ExtractFromDouyinRequest
from fastapi import HTTPException
@@ -140,111 +140,49 @@ class TestExtractFromDouyin:
extract_from_douyin(request=req, current_user=auth)
assert exc_info.value.status_code == 400
@patch("app.api.routes.scripts_ai.get_mediakit_client")
@patch("app.api.routes.scripts_ai._ytdlp_extract_video_url", return_value=(None, 0.0))
@patch("tempfile.TemporaryDirectory")
@patch("yt_dlp.YoutubeDL")
def test_extract_from_douyin_download_failure(self, mock_ydl_cls, mock_tempdir, mock_meta, mock_get_mk):
"""下载失败返回 502."""
@patch("app.api.routes.scripts_ai.resolve_douyin_video", return_value=None)
def test_extract_from_douyin_resolver_all_fail(self, mock_resolve):
"""所有解析源失败 → 503 解析失败。"""
from app.api.routes.scripts_ai import extract_from_douyin
from app.schemas.scripts_ai import ExtractFromDouyinRequest
from fastapi import HTTPException
fake_mk = MagicMock()
fake_mk.is_available = False
mock_get_mk.return_value = fake_mk
mock_ydl_cls.return_value = _mock_youtube_dl(
extract_info_side_effect=Exception("Video unavailable"),
)
mock_ydl_cls.return_value.__enter__ = MagicMock(return_value=mock_ydl_cls.return_value)
mock_ydl_cls.return_value.__exit__ = MagicMock(return_value=False)
mock_td = MagicMock()
mock_td.__enter__ = MagicMock(return_value="/tmp/douyin_extract_abc")
mock_td.__exit__ = MagicMock(return_value=False)
mock_tempdir.return_value = mock_td
req = ExtractFromDouyinRequest(url="https://v.douyin.com/xxxxx/")
auth = _make_auth_user()
with pytest.raises(HTTPException) as exc_info:
extract_from_douyin(request=req, current_user=auth)
assert exc_info.value.status_code == 502
@patch("app.api.routes.scripts_ai.transcribe_to_text")
@patch("tempfile.TemporaryDirectory")
@patch("yt_dlp.YoutubeDL")
@patch("app.api.routes.scripts_ai.os.path.getsize", return_value=1024)
@patch("app.api.routes.scripts_ai.os.path.isfile", return_value=True)
def test_extract_from_douyin_asr_not_configured(
self,
mock_isfile,
mock_getsize,
mock_ydl_cls,
mock_tempdir,
mock_transcribe,
):
"""ASR 未配置返回 503."""
from app.api.routes.scripts_ai import extract_from_douyin
from app.schemas.scripts_ai import ExtractFromDouyinRequest
from app.services.script_asr_service import ASRNotConfiguredError
from fastapi import HTTPException
mock_ydl_cls.return_value = _mock_youtube_dl(
extract_info_return={"id": "abc123", "duration": 60},
)
mock_td = MagicMock()
mock_td.__enter__ = MagicMock(return_value="/tmp/douyin_extract_abc")
mock_td.__exit__ = MagicMock(return_value=False)
mock_tempdir.return_value = mock_td
mock_transcribe.side_effect = ASRNotConfiguredError("ASR 服务未配置")
req = ExtractFromDouyinRequest(url="https://v.douyin.com/xxxxx/")
auth = _make_auth_user()
with pytest.raises(HTTPException) as exc_info:
extract_from_douyin(request=req, current_user=auth)
assert exc_info.value.status_code == 503
assert "解析" in exc_info.value.detail
@patch("app.api.routes.scripts_ai.transcribe_to_text")
@patch("tempfile.TemporaryDirectory")
@patch("yt_dlp.YoutubeDL")
@patch("app.api.routes.scripts_ai.os.path.getsize", return_value=1024)
@patch("app.api.routes.scripts_ai.os.path.isfile", return_value=True)
def test_extract_from_douyin_asr_failure(
@patch("app.api.routes.scripts_ai.get_mediakit_client")
@patch("app.api.routes.scripts_ai._direct_url_download_and_local_asr")
@patch("app.api.routes.scripts_ai.resolve_douyin_video")
def test_extract_from_douyin_asr_empty_desc_fallback(
self,
mock_isfile,
mock_getsize,
mock_ydl_cls,
mock_tempdir,
mock_transcribe,
mock_resolve,
mock_local_asr,
mock_get_mk,
):
"""ASR 调用失败返回 502."""
"""ASR 返回空文本时使用 desc 兜底。"""
from app.api.routes.scripts_ai import extract_from_douyin
from app.schemas.scripts_ai import ExtractFromDouyinRequest
from app.services.script_asr_service import ASRTranscriptionError
from fastapi import HTTPException
mock_ydl_cls.return_value = _mock_youtube_dl(
extract_info_return={"id": "abc123", "duration": 60},
)
mock_resolve.return_value = _mock_resolver(desc="Feed描述文案兜底")
mock_td = MagicMock()
mock_td.__enter__ = MagicMock(return_value="/tmp/douyin_extract_abc")
mock_td.__exit__ = MagicMock(return_value=False)
mock_tempdir.return_value = mock_td
fake_mk = MagicMock()
fake_mk.is_available = True
fake_mk.asr_submit.return_value = "tk1"
fake_mk.asr_poll.return_value = ("", 4.5) # 空结果(无旁白视频)
mock_get_mk.return_value = fake_mk
mock_transcribe.side_effect = ASRTranscriptionError("语音识别失败: timeout")
mock_local_asr.return_value = ("", 0.0) # 本地也返回空
req = ExtractFromDouyinRequest(url="https://v.douyin.com/xxxxx/")
auth = _make_auth_user()
result = extract_from_douyin(request=req, current_user=auth, db=MagicMock())
with pytest.raises(HTTPException) as exc_info:
extract_from_douyin(request=req, current_user=auth)
assert exc_info.value.status_code == 502
assert result.text == "Feed描述文案兜底"
# ── ai_rewrite ───────────────────────────────────────────────────────────────