152 lines
5.7 KiB
Python
152 lines
5.7 KiB
Python
"""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
|