import pytest from app.services import youtube_client class FakeResponse: def __init__(self, status_code, payload): self.status_code = status_code self._payload = payload self.text = str(payload) def json(self): return self._payload class FakeClient: def __init__(self, responses): self._responses = list(responses) def __enter__(self): return self def __exit__(self, *args): return False def get(self, url, params=None, headers=None): return self._responses.pop(0) def _credentials(): class Cred: token = "fake-token" return Cred() def test_fetch_subscriptions_paginates(monkeypatch): page1 = FakeResponse( 200, { "items": [ { "id": "subA", "snippet": { "title": "Channel A", "description": "desc a", "resourceId": {"channelId": "chanA"}, "thumbnails": {"high": {"url": "http://thumb/a.jpg"}}, }, } ], "nextPageToken": "page2", }, ) page2 = FakeResponse( 200, { "items": [ { "snippet": { "title": "Channel B", "description": "desc b", "resourceId": {"channelId": "chanB"}, "thumbnails": {"default": {"url": "http://thumb/b.jpg"}}, } } ] }, ) monkeypatch.setattr(youtube_client.httpx, "Client", lambda timeout: FakeClient([page1, page2])) result = youtube_client.fetch_subscriptions(_credentials()) assert [r["youtube_channel_id"] for r in result] == ["chanA", "chanB"] assert result[0]["youtube_subscription_id"] == "subA" assert result[0]["thumbnail_url"] == "http://thumb/a.jpg" assert result[1]["thumbnail_url"] == "http://thumb/b.jpg" def test_fetch_subscriptions_raises_on_quota_exceeded(monkeypatch): response = FakeResponse( 403, {"error": {"message": "Quota exceeded", "errors": [{"reason": "quotaExceeded"}]}}, ) monkeypatch.setattr(youtube_client.httpx, "Client", lambda timeout: FakeClient([response])) with pytest.raises(youtube_client.YouTubeQuotaExceeded): youtube_client.fetch_subscriptions(_credentials()) def test_fetch_playlist_video_ids(monkeypatch): response = FakeResponse( 200, { "items": [ {"contentDetails": {"videoId": "vid1"}}, {"contentDetails": {"videoId": "vid2"}}, ] }, ) monkeypatch.setattr(youtube_client.httpx, "Client", lambda timeout: FakeClient([response])) result = youtube_client.fetch_playlist_video_ids(_credentials(), "UUplaylist", 10) assert result == ["vid1", "vid2"] def test_fetch_playlist_video_ids_returns_empty_on_404(monkeypatch): response = FakeResponse(404, {"error": {"message": "playlist not found"}}) monkeypatch.setattr(youtube_client.httpx, "Client", lambda timeout: FakeClient([response])) result = youtube_client.fetch_playlist_video_ids(_credentials(), "UUplaylist", 10) assert result == [] def test_fetch_videos_details(monkeypatch): response = FakeResponse( 200, { "items": [ { "id": "vid1", "snippet": { "channelId": "chanA", "title": "Title 1", "description": "desc", "publishedAt": "2026-09-10T12:00:00Z", "thumbnails": {"high": {"url": "http://thumb/1.jpg"}}, }, "contentDetails": {"duration": "PT10M5S"}, } ] }, ) monkeypatch.setattr(youtube_client.httpx, "Client", lambda timeout: FakeClient([response])) result = youtube_client.fetch_videos_details(_credentials(), ["vid1"]) assert len(result) == 1 assert result[0]["youtube_video_id"] == "vid1" assert result[0]["youtube_channel_id"] == "chanA" assert result[0]["duration_iso8601"] == "PT10M5S" assert result[0]["thumbnail_url"] == "http://thumb/1.jpg" def test_unsubscribe_success(monkeypatch): response = FakeResponse(204, {}) seen = {} class FakeDeleteClient(FakeClient): def delete(self, url, params=None, headers=None): seen["params"] = params return self._responses.pop(0) monkeypatch.setattr(youtube_client.httpx, "Client", lambda timeout: FakeDeleteClient([response])) youtube_client.unsubscribe(_credentials(), "sub123") assert seen["params"] == {"id": "sub123"} def test_unsubscribe_already_gone_is_not_an_error(monkeypatch): response = FakeResponse(404, {"error": {"message": "not found"}}) class FakeDeleteClient(FakeClient): def delete(self, url, params=None, headers=None): return self._responses.pop(0) monkeypatch.setattr(youtube_client.httpx, "Client", lambda timeout: FakeDeleteClient([response])) youtube_client.unsubscribe(_credentials(), "sub123") # must not raise def test_unsubscribe_insufficient_scope(monkeypatch): response = FakeResponse( 403, {"error": {"message": "Request had insufficient authentication scopes.", "errors": [{"reason": "forbidden"}]}}, ) class FakeDeleteClient(FakeClient): def delete(self, url, params=None, headers=None): return self._responses.pop(0) monkeypatch.setattr(youtube_client.httpx, "Client", lambda timeout: FakeDeleteClient([response])) with pytest.raises(youtube_client.YouTubeInsufficientScope): youtube_client.unsubscribe(_credentials(), "sub123") def test_fetch_uploads_playlists_batches(monkeypatch): response = FakeResponse( 200, { "items": [ {"id": "chanA", "contentDetails": {"relatedPlaylists": {"uploads": "UUchanA"}}}, {"id": "chanB", "contentDetails": {"relatedPlaylists": {"uploads": "UUchanB"}}}, ] }, ) monkeypatch.setattr(youtube_client.httpx, "Client", lambda timeout: FakeClient([response])) result = youtube_client.fetch_uploads_playlists(_credentials(), ["chanA", "chanB"]) assert result == {"chanA": "UUchanA", "chanB": "UUchanB"}