"""Phase 5 tests: Integration — full video → transcribe with provider switching. Covers: - Full transcript with dashscope provider (mocked OpenAI) - Full transcript with openrouter provider (mocked httpx) - API key validation per provider """ from unittest.mock import AsyncMock, MagicMock, patch import httpx import pytest from fastapi import FastAPI from fastapi.testclient import TestClient from app.routers.video import router @pytest.fixture def video_client(tmp_path, monkeypatch): upload_dir = tmp_path / "test_uploads" upload_dir.mkdir() monkeypatch.setenv("VIDEO_UPLOAD_DIR", str(upload_dir)) monkeypatch.setenv("MAX_VIDEO_SIZE_MB", "50") from app.core.config import get_settings get_settings.cache_clear() app = FastAPI() app.include_router(router, prefix="/api/v1") return TestClient(app), upload_dir def _upload_video(client, filename="test.mp4", content=b"\x00" * 1024): resp = client.post( "/api/v1/video/upload", files={"file": (filename, content, "video/mp4")}, ) assert resp.status_code == 200 return resp.json()["video_id"] class TestDashScopeIntegration: @patch("app.routers.video.VideoService.extract_audio") @patch("app.services.asr_providers.OpenAI") def test_transcribe_with_dashscope(self, mock_openai_cls, mock_extract, video_client, monkeypatch): monkeypatch.setenv("ASR_PROVIDER", "dashscope") monkeypatch.setenv("DASHSCOPE_API_KEY", "sk-dashscope-test") from app.core.config import get_settings get_settings.cache_clear() client, upload_dir = video_client video_id = _upload_video(client) fake_wav = upload_dir / "extracted.wav" fake_wav.write_bytes(b"RIFF" + b"\x00" * 100) mock_extract.return_value = fake_wav mock_resp = MagicMock() mock_resp.choices = [MagicMock()] mock_resp.choices[0].message.content = "测试转录結果" mock_openai_instance = MagicMock() mock_openai_instance.chat.completions.create.return_value = mock_resp mock_openai_cls.return_value = mock_openai_instance resp = client.post(f"/api/v1/video/{video_id}/transcribe") assert resp.status_code == 200 assert "text" in resp.json() assert "測" in resp.json()["text"] or "轉" in resp.json()["text"] class TestOpenRouterIntegration: @patch("app.routers.video.VideoService.extract_audio") @patch("app.services.asr_providers.httpx.AsyncClient") def test_transcribe_with_openrouter(self, mock_httpx_cls, mock_extract, video_client, monkeypatch): monkeypatch.setenv("ASR_PROVIDER", "openrouter") monkeypatch.setenv("OPENROUTER_API_KEY", "sk-or-test") from app.core.config import get_settings get_settings.cache_clear() client, upload_dir = video_client video_id = _upload_video(client) fake_wav = upload_dir / "extracted.wav" fake_wav.write_bytes(b"RIFF" + b"\x00" * 100) mock_extract.return_value = fake_wav mock_response = MagicMock(spec=httpx.Response) mock_response.json.return_value = {"text": "測試轉錄結果", "usage": {}} mock_response.raise_for_status = MagicMock() mock_http_client = AsyncMock() mock_http_client.post.return_value = mock_response mock_httpx_cls.return_value = mock_http_client resp = client.post(f"/api/v1/video/{video_id}/transcribe") assert resp.status_code == 200 assert "text" in resp.json() assert "轉" in resp.json()["text"] or "錄" in resp.json()["text"] class TestApiKeyValidation: def test_missing_dashscope_key_returns_500(self, video_client, monkeypatch): monkeypatch.setenv("ASR_PROVIDER", "dashscope") monkeypatch.setenv("DASHSCOPE_API_KEY", "") from app.core.config import get_settings get_settings.cache_clear() client, upload_dir = video_client video_id = _upload_video(client) resp = client.post(f"/api/v1/video/{video_id}/transcribe") assert resp.status_code == 500 assert "DASHSCOPE_API_KEY" in resp.json()["detail"] def test_missing_openrouter_key_returns_500(self, video_client, monkeypatch): monkeypatch.setenv("ASR_PROVIDER", "openrouter") monkeypatch.setenv("OPENROUTER_API_KEY", "") from app.core.config import get_settings get_settings.cache_clear() client, upload_dir = video_client video_id = _upload_video(client) resp = client.post(f"/api/v1/video/{video_id}/transcribe") assert resp.status_code == 500 assert "OPENROUTER_API_KEY" in resp.json()["detail"]