Files
xiaoxia-saas/tests/unit/test_extract_from_douyin_errors.py
T
xiaoxia 9e0959cb85
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
test(douyin): 更新单元测试适配多源resolver架构,删除yt-dlp/cookies相关mock
2026-09-17 19:30:08 +08:00

244 lines
10 KiB
Python
Raw 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.
"""验证 extract-from-douyin 在各种失败场景返回正确的 HTTP 状态码(绝不能 500)
新版架构:douyin_resolver 多源轮询 → MediaKit ASR → 本地下载+ASR → desc 兜底。
所有外部依赖(resolver、MediaKit、transcribe_to_text)均通过 mock 隔离。
"""
from __future__ import annotations
from unittest import mock
import pytest
from app.auth import AuthenticatedUser
from app.services.douyin_resolver import ResolveResult
from fastapi import HTTPException, status
class _FakeUser:
id = "u-test"
is_member = False
member_type = None
@pytest.fixture
def fake_user():
return AuthenticatedUser(user=_FakeUser())
def _import_target():
from app.api.routes import scripts_ai
return scripts_ai
def _fake_mk_available(text="识别成功的文案", duration=5.0):
"""Mock MediaKitClient 可用并返回指定 ASR 结果。"""
fake_mk = mock.MagicMock()
fake_mk.is_available = True
fake_mk.asr_submit.return_value = "tk1"
fake_mk.asr_poll.return_value = (text, duration)
return mock.patch("app.api.routes.scripts_ai.get_mediakit_client", return_value=fake_mk)
def _fake_mk_unavailable():
"""Mock MediaKitClient 不可用,强制走下载+本地 ASR 路径。"""
fake_mk = mock.MagicMock()
fake_mk.is_available = False
return mock.patch("app.api.routes.scripts_ai.get_mediakit_client", return_value=fake_mk)
def _fake_resolver_success(video_url="https://example.com/direct.mp4", desc="", source="app_feed"):
"""Mock resolver 返回成功。"""
result = ResolveResult(video_url=video_url, desc=desc, source=source)
return mock.patch("app.api.routes.scripts_ai.resolve_douyin_video", return_value=result)
def _fake_resolver_image(desc="图文文案内容", source="app_feed_image"):
"""Mock resolver 返回图文视频(video_url 为空)。"""
result = ResolveResult(video_url="", desc=desc, source=source)
return mock.patch("app.api.routes.scripts_ai.resolve_douyin_video", return_value=result)
def _fake_resolver_failure():
"""Mock resolver 所有源均失败,返回 None。"""
return mock.patch("app.api.routes.scripts_ai.resolve_douyin_video", return_value=None)
# ── 解析阶段失败 ──────────────────────────────────────────────────
def test_resolver_all_fail_returns_503_parse(fake_user):
"""所有解析源均失败 → 503 解析失败。"""
scripts_ai = _import_target()
body = scripts_ai.ExtractFromDouyinRequest(url="https://v.douyin.com/test123/")
with _fake_resolver_failure():
with pytest.raises(HTTPException) as exc:
scripts_ai.extract_from_douyin(request=body, current_user=fake_user, db=mock.MagicMock())
assert exc.value.status_code == status.HTTP_503_SERVICE_UNAVAILABLE
assert "解析失败" in exc.value.detail or "链接" in exc.value.detail
# ── 图文视频(无需ASR) ─────────────────────────────────────────
def test_image_post_returns_desc_directly(fake_user):
"""图文视频(resolver返回空video_url有desc)→ 直接返回 desc,不走 ASR。"""
scripts_ai = _import_target()
body = scripts_ai.ExtractFromDouyinRequest(url="https://v.douyin.com/abcdeFG/")
with _fake_resolver_image(desc="这是图文文案"):
resp = scripts_ai.extract_from_douyin(request=body, current_user=fake_user, db=mock.MagicMock())
assert resp.text == "这是图文文案"
assert resp.duration_seconds == 0.0
# ── MediaKit ASR 成功路径 ────────────────────────────────────────
def test_mediakit_asr_success(fake_user):
"""正常流程:resolver 成功 + MediaKit ASR 成功 → 返回文本。"""
scripts_ai = _import_target()
body = scripts_ai.ExtractFromDouyinRequest(url="https://v.douyin.com/xxxxx/ 快来看看!")
with _fake_resolver_success(desc="Feed标题"):
with _fake_mk_available(text="这是MediaKit识别的文案", duration=12.5):
result = scripts_ai.extract_from_douyin(
request=body, current_user=fake_user, db=mock.MagicMock()
)
assert result.text == "这是MediaKit识别的文案"
assert result.duration_seconds == 12.5
# ── 分享文本含前后文字 ──────────────────────────────────────────
def test_share_text_input_extracts_url_correctly(fake_user):
"""分享文本(含前后说明文字)应能正确提取 URL。"""
scripts_ai = _import_target()
share_text = "这个视频太搞笑了 https://v.douyin.com/abcdeFG/ 快来看看!#搞笑 #日常"
body = scripts_ai.ExtractFromDouyinRequest(url=share_text)
with _fake_resolver_success():
with _fake_mk_available(text="识别成功的文案", duration=5.0) as mk_mock:
resp = scripts_ai.extract_from_douyin(
request=body, current_user=fake_user, db=mock.MagicMock()
)
assert resp.source_url == "https://v.douyin.com/abcdeFG/"
assert resp.text == "识别成功的文案"
assert resp.duration_seconds == 5.0
# ── 非抖音链接 ─────────────────────────────────────────────────
def test_non_douyin_share_text_returns_400(fake_user):
"""粘贴非抖音分享链接 → 400。"""
scripts_ai = _import_target()
body = scripts_ai.ExtractFromDouyinRequest(url="看看这个 https://www.bilibili.com/video/BV1xx 哈哈哈")
with pytest.raises(HTTPException) as exc:
scripts_ai.extract_from_douyin(request=body, current_user=fake_user, db=mock.MagicMock())
assert exc.value.status_code == 400
# ── ASR 空结果 → desc 兜底 ────────────────────────────────────
def test_asr_empty_falls_back_to_desc(fake_user):
"""MediaKit 和本地 ASR 都返回空文本(无旁白视频)→ 使用 desc 兜底。"""
scripts_ai = _import_target()
body = scripts_ai.ExtractFromDouyinRequest(url="https://v.douyin.com/xxxxx/")
# MediaKit 返回空文本
fake_mk = mock.MagicMock()
fake_mk.is_available = True
fake_mk.asr_submit.return_value = "tk1"
fake_mk.asr_poll.return_value = ("", 4.5)
# 本地下载+ASR 也返回空(通过mock _direct_url_download_and_local_asr
with _fake_resolver_success(desc="Feed描述文案"):
with mock.patch("app.api.routes.scripts_ai.get_mediakit_client", return_value=fake_mk):
with mock.patch(
"app.api.routes.scripts_ai._direct_url_download_and_local_asr",
return_value=("", 0.0),
):
resp = scripts_ai.extract_from_douyin(
request=body, current_user=fake_user, db=mock.MagicMock()
)
assert resp.text == "Feed描述文案"
# ── ASR 未配置 → 503 ─────────────────────────────────────────
def test_asr_not_configured_returns_503(fake_user):
"""本地 ASR 未配置 → 503。"""
from app.services.script_asr_service import ASRNotConfiguredError
scripts_ai = _import_target()
body = scripts_ai.ExtractFromDouyinRequest(url="https://v.douyin.com/xxxxx/")
with _fake_resolver_success():
with _fake_mk_unavailable():
with mock.patch(
"app.api.routes.scripts_ai._direct_url_download_and_local_asr",
side_effect=HTTPException(status_code=503, detail="ASR未配置"),
):
with pytest.raises(HTTPException) as exc:
scripts_ai.extract_from_douyin(
request=body, current_user=fake_user, db=mock.MagicMock()
)
assert exc.value.status_code == status.HTTP_503_SERVICE_UNAVAILABLE
# ── ASR 转写失败 → 502 ────────────────────────────────────────
def test_asr_transcription_failure_returns_502(fake_user):
"""ASR 转写异常 → 502(被 _direct_url_download_and_local_asr 包装)。"""
from app.services.script_asr_service import ASRTranscriptionError
scripts_ai = _import_target()
body = scripts_ai.ExtractFromDouyinRequest(url="https://v.douyin.com/xxxxx/")
# MediaKit 失败
fake_mk = mock.MagicMock()
fake_mk.is_available = True
from app.services.mediakit_client import MediaKitError
fake_mk.asr_submit.side_effect = MediaKitError("ASR failed", code="TaskFailed")
with _fake_resolver_success(desc=""):
with mock.patch("app.api.routes.scripts_ai.get_mediakit_client", return_value=fake_mk):
with mock.patch(
"app.api.routes.scripts_ai._direct_url_download_and_local_asr",
side_effect=HTTPException(status_code=502, detail="语音识别失败"),
):
with pytest.raises(HTTPException) as exc:
scripts_ai.extract_from_douyin(
request=body, current_user=fake_user, db=mock.MagicMock()
)
assert exc.value.status_code == status.HTTP_502_BAD_GATEWAY
# ── 下载超时 → 504 ────────────────────────────────────────────
def test_download_timeout_returns_504(fake_user):
"""视频下载超时 → 504。"""
scripts_ai = _import_target()
body = scripts_ai.ExtractFromDouyinRequest(url="https://v.douyin.com/xxxxx/")
with _fake_resolver_success():
with _fake_mk_unavailable():
with mock.patch(
"app.api.routes.scripts_ai._direct_url_download_and_local_asr",
side_effect=HTTPException(status_code=504, detail="视频下载超时"),
):
with pytest.raises(HTTPException) as exc:
scripts_ai.extract_from_douyin(
request=body, current_user=fake_user, db=mock.MagicMock()
)
assert exc.value.status_code == status.HTTP_504_GATEWAY_TIMEOUT