"""Unit tests for content.publishing.youtube.""" from __future__ import annotations from unittest.mock import MagicMock, patch import pytest from content.publishing.youtube import ( YouTubeUploadResult, _daily_upload_count, _increment_daily_upload_count, _youtube_available, upload_episode, ) # ── _youtube_available ──────────────────────────────────────────────────────── class TestYoutubeAvailable: def test_returns_bool(self): assert isinstance(_youtube_available(), bool) def test_false_when_library_missing(self): with patch("importlib.util.find_spec", return_value=None): assert _youtube_available() is False # ── daily upload counter ────────────────────────────────────────────────────── class TestDailyUploadCounter: def test_zero_when_no_file(self, tmp_path): counter_path = tmp_path / "counter.json" with patch( "content.publishing.youtube.settings", MagicMock(content_youtube_counter_file=str(counter_path)), ): assert _daily_upload_count() == 0 def test_increments_correctly(self, tmp_path): counter_path = tmp_path / "counter.json" mock_settings = MagicMock(content_youtube_counter_file=str(counter_path)) with patch("content.publishing.youtube.settings", mock_settings): assert _daily_upload_count() == 0 _increment_daily_upload_count() assert _daily_upload_count() == 1 _increment_daily_upload_count() assert _daily_upload_count() == 2 def test_persists_across_calls(self, tmp_path): counter_path = tmp_path / "counter.json" mock_settings = MagicMock(content_youtube_counter_file=str(counter_path)) with patch("content.publishing.youtube.settings", mock_settings): _increment_daily_upload_count() _increment_daily_upload_count() with patch("content.publishing.youtube.settings", mock_settings): assert _daily_upload_count() == 2 # ── upload_episode ──────────────────────────────────────────────────────────── class TestUploadEpisode: @pytest.mark.asyncio async def test_returns_failure_when_library_missing(self, tmp_path): video = tmp_path / "ep.mp4" video.write_bytes(b"fake") with patch("content.publishing.youtube._youtube_available", return_value=False): result = await upload_episode(str(video), "Title") assert result.success is False assert "google" in result.error.lower() @pytest.mark.asyncio async def test_returns_failure_when_video_missing(self, tmp_path): with patch("content.publishing.youtube._youtube_available", return_value=True): result = await upload_episode(str(tmp_path / "nonexistent.mp4"), "Title") assert result.success is False assert "not found" in result.error @pytest.mark.asyncio async def test_returns_failure_when_quota_reached(self, tmp_path): video = tmp_path / "ep.mp4" video.write_bytes(b"fake") with ( patch("content.publishing.youtube._youtube_available", return_value=True), patch("content.publishing.youtube._daily_upload_count", return_value=6), ): result = await upload_episode(str(video), "Title") assert result.success is False assert "quota" in result.error.lower() @pytest.mark.asyncio async def test_successful_upload(self, tmp_path): video = tmp_path / "ep.mp4" video.write_bytes(b"fake video data") fake_upload_result = YouTubeUploadResult( success=True, video_id="abc123", video_url="https://www.youtube.com/watch?v=abc123", ) with ( patch("content.publishing.youtube._youtube_available", return_value=True), patch("content.publishing.youtube._daily_upload_count", return_value=0), patch( "asyncio.to_thread", return_value=fake_upload_result, ), ): result = await upload_episode(str(video), "My Episode Title") assert result.success is True assert result.video_id == "abc123" assert "abc123" in result.video_url @pytest.mark.asyncio async def test_title_truncated_to_100_chars(self, tmp_path): video = tmp_path / "ep.mp4" video.write_bytes(b"fake") long_title = "A" * 150 captured_args = {} async def _capture_to_thread(fn, *args, **kwargs): captured_args["title"] = args[1] # title is second positional arg return YouTubeUploadResult(success=True, video_id="x") with ( patch("content.publishing.youtube._youtube_available", return_value=True), patch("content.publishing.youtube._daily_upload_count", return_value=0), patch("asyncio.to_thread", side_effect=_capture_to_thread), ): await upload_episode(str(video), long_title) assert len(captured_args["title"]) <= 100 @pytest.mark.asyncio async def test_default_tags_is_empty_list(self, tmp_path): video = tmp_path / "ep.mp4" video.write_bytes(b"fake") captured_args = {} async def _capture(fn, *args, **kwargs): captured_args["tags"] = args[3] return YouTubeUploadResult(success=True, video_id="x") with ( patch("content.publishing.youtube._youtube_available", return_value=True), patch("content.publishing.youtube._daily_upload_count", return_value=0), patch("asyncio.to_thread", side_effect=_capture), ): await upload_episode(str(video), "Title") assert captured_args["tags"] == []