160 lines
6.0 KiB
Python
160 lines
6.0 KiB
Python
"""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"] == []
|