"""Tests for summarizer.py — mocks transcript API + DeepSeek API.""" import pytest import os import httpx import aiosqlite import summarizer from unittest.mock import patch, MagicMock, AsyncMock class MockTranscript: def fetch(self): return [ {"text": "Welcome to this video tutorial.", "start": 0.0, "duration": 3.0}, {"text": "Today we are learning about Svelte 5 and runes.", "start": 3.0, "duration": 4.0} ] class MockTranscriptList: def __init__(self, transcript): self.transcript = transcript def find_manually_created_transcript(self, languages): return self.transcript def find_generated_transcript(self, languages): return self.transcript def __iter__(self): return iter([self.transcript]) def _mock_deepseek_response(content: str) -> MagicMock: """Build a DeepSeek/OpenAI-compatible API response.""" r = MagicMock(spec=httpx.Response) r.status_code = 200 r.json.return_value = { "choices": [{"message": {"content": content}}] } return r @pytest.mark.asyncio async def test_download_transcript_success(): mock_trans = MockTranscript() mock_list = MockTranscriptList(mock_trans) with patch("youtube_transcript_api.YouTubeTranscriptApi.list", return_value=mock_list): text = summarizer.download_transcript("test_vid") assert text == "Welcome to this video tutorial. Today we are learning about Svelte 5 and runes." @pytest.mark.asyncio async def test_summarize_video_success(test_db): await test_db.execute(""" INSERT INTO videos (video_id, title, url, status) VALUES ('summarize_vid', 'Video to Summarize', 'https://www.youtube.com/watch?v=summarize_vid', 'pending') """) await test_db.commit() mock_trans = MockTranscript() mock_list = MockTranscriptList(mock_trans) mock_response = _mock_deepseek_response("**Mock Core Hook**\n- Takeaway 1\n- Takeaway 2\nVerdict line.") with patch("youtube_transcript_api.YouTubeTranscriptApi.list", return_value=mock_list), \ patch("httpx.AsyncClient.post", new_callable=AsyncMock, return_value=mock_response) as mock_post: summary = await summarizer.summarize_video("summarize_vid") # Verify API request details mock_post.assert_called_once() url = mock_post.call_args[0][0] assert summarizer.BASE_URL in url headers = mock_post.call_args[1]["headers"] assert "Authorization" in headers assert headers["User-Agent"] is not None payload = mock_post.call_args[1]["json"] assert payload["model"] == "deepseek-chat" assert len(payload["messages"]) == 2 # system + user assert summary == "**Mock Core Hook**\n- Takeaway 1\n- Takeaway 2\nVerdict line." # Query db to verify transcript & summary were saved cursor = await test_db.execute("SELECT * FROM videos WHERE video_id = 'summarize_vid'") video = await cursor.fetchone() assert video is not None assert video["transcript"] == "Welcome to this video tutorial. Today we are learning about Svelte 5 and runes." assert video["summary"] == "**Mock Core Hook**\n- Takeaway 1\n- Takeaway 2\nVerdict line." @pytest.mark.asyncio async def test_summarize_video_deepseek_error_leaves_null(test_db): """When DeepSeek fails, summary should stay NULL for retry.""" await test_db.execute(""" INSERT INTO videos (video_id, title, url, status) VALUES ('err_vid', 'Error Video', 'https://youtube.com/watch?v=err_vid', 'pending') """) await test_db.commit() mock_trans = MockTranscript() mock_list = MockTranscriptList(mock_trans) mock_response = MagicMock(spec=httpx.Response) mock_response.status_code = 429 # raise_for_status on 429 should raise HTTPStatusError mock_response.raise_for_status.side_effect = httpx.HTTPStatusError( "429 Rate Limited", request=MagicMock(), response=mock_response ) with patch("youtube_transcript_api.YouTubeTranscriptApi.list", return_value=mock_list), \ patch("httpx.AsyncClient.post", new_callable=AsyncMock, return_value=mock_response): result = await summarizer.summarize_video("err_vid") assert result is None # Returns None instead of an error string # Verify DB — summary should remain NULL cursor = await test_db.execute("SELECT summary FROM videos WHERE video_id = 'err_vid'") row = await cursor.fetchone() assert row["summary"] is None @pytest.mark.asyncio async def test_summarize_all_pending(test_db): await test_db.execute(""" INSERT INTO videos (video_id, title, url, summary, status) VALUES ('pending_vid', 'Pending Video', 'https://youtube.com/watch?v=pending_vid', '', 'pending'), ('done_vid', 'Done Video', 'https://youtube.com/watch?v=done_vid', 'Existing summary', 'pending') """) await test_db.commit() mock_trans = MockTranscript() mock_list = MockTranscriptList(mock_trans) mock_response = _mock_deepseek_response("Fresh summary") with patch("youtube_transcript_api.YouTubeTranscriptApi.list", return_value=mock_list), \ patch("httpx.AsyncClient.post", new_callable=AsyncMock, return_value=mock_response) as mock_post: await summarizer.summarize_all_pending() # Should only call API once for 'pending_vid' (done_vid already has summary) mock_post.assert_called_once() # Verify db states cursor = await test_db.execute("SELECT video_id, summary FROM videos ORDER BY video_id") videos = await cursor.fetchall() assert videos[0]["video_id"] == "done_vid" assert videos[0]["summary"] == "Existing summary" # Untouched assert videos[1]["video_id"] == "pending_vid" assert videos[1]["summary"] == "Fresh summary" # Updated