diff --git a/twitch/feeds.py b/twitch/feeds.py index 14ee22c..43ab43d 100644 --- a/twitch/feeds.py +++ b/twitch/feeds.py @@ -831,10 +831,19 @@ class GameFeed(TTVDropsBaseFeed): except TypeError, ValueError: length = 0 + if not length: + return [] + mime: str = getattr(item, "box_art_mime_type", "") mime_type: str = mime or "image/jpeg" - return [feedgenerator.Enclosure(image_url, str(length), mime_type)] + return [ + feedgenerator.Enclosure( + self._absolute_url(image_url), + str(length), + mime_type, + ), + ] return [] def feed_url(self) -> str: @@ -965,10 +974,19 @@ class DropCampaignFeed(TTVDropsBaseFeed): except TypeError, ValueError: length = 0 + if not length: + return [] + mime: str = getattr(item, "image_mime_type", "") mime_type: str = mime or "image/jpeg" - return [feedgenerator.Enclosure(image_url, str(length), mime_type)] + return [ + feedgenerator.Enclosure( + self._absolute_url(image_url), + str(length), + mime_type, + ), + ] return [] def feed_url(self) -> str: @@ -1132,10 +1150,19 @@ class GameCampaignFeed(TTVDropsBaseFeed): except TypeError, ValueError: length = 0 + if not length: + return [] + mime: str = getattr(item, "image_mime_type", "") mime_type: str = mime or "image/jpeg" - return [feedgenerator.Enclosure(image_url, str(length), mime_type)] + return [ + feedgenerator.Enclosure( + self._absolute_url(image_url), + str(length), + mime_type, + ), + ] return [] def feed_url(self, obj: Game) -> str: @@ -1307,8 +1334,7 @@ class RewardCampaignFeed(TTVDropsBaseFeed): Returns: list[feedgenerator.Enclosure]: A list of Enclosure objects if an image URL is available, otherwise an empty list. """ - # Use image_url as enclosure if available - image_url: str = getattr(item, "image_url", "") + image_url: str = getattr(item, "image_best_url", "") if image_url: try: size: int | None = getattr(item, "image_size_bytes", None) @@ -1316,10 +1342,19 @@ class RewardCampaignFeed(TTVDropsBaseFeed): except TypeError, ValueError: length = 0 + if not length: + return [] + mime: str = getattr(item, "image_mime_type", "") mime_type: str = mime or "image/jpeg" - return [feedgenerator.Enclosure(image_url, str(length), mime_type)] + return [ + feedgenerator.Enclosure( + self._absolute_url(image_url), + str(length), + mime_type, + ), + ] return [] def feed_url(self) -> str: diff --git a/twitch/tests/test_feeds.py b/twitch/tests/test_feeds.py index 03016ff..30c1fca 100644 --- a/twitch/tests/test_feeds.py +++ b/twitch/tests/test_feeds.py @@ -272,6 +272,85 @@ class RSSFeedTestCase(TestCase): assert 'type="text/xsl"' in content assert 'media="screen"' in content + def test_campaign_and_game_feeds_use_absolute_media_enclosure_urls(self) -> None: + """Campaign/game RSS+Atom enclosures should use absolute URLs for local media files.""" + self.game.box_art = "" + self.game.box_art_file.save( + "box.png", + ContentFile(b"game-image-bytes"), + save=False, + ) + self.game.box_art_size_bytes = len(b"game-image-bytes") + self.game.box_art_mime_type = "image/png" + self.game.save() + + self.campaign.image_url = "" + self.campaign.image_file.save( + "campaign.png", + ContentFile(b"campaign-image-bytes"), + save=False, + ) + self.campaign.image_size_bytes = len(b"campaign-image-bytes") + self.campaign.image_mime_type = "image/png" + self.campaign.save() + + feed_urls: list[str] = [ + reverse("twitch:game_feed"), + reverse("twitch:campaign_feed"), + reverse("twitch:game_campaign_feed", args=[self.game.twitch_id]), + reverse("twitch:game_feed_atom"), + reverse("twitch:campaign_feed_atom"), + reverse("twitch:game_campaign_feed_atom", args=[self.game.twitch_id]), + ] + + for url in feed_urls: + response: _MonkeyPatchedWSGIResponse = self.client.get(url) + assert response.status_code == 200 + content: str = response.content.decode("utf-8") + + msg: str = ( + f"Expected absolute media enclosure URLs for {url}, got: {content}" + ) + assert "http://testserver/media/" in content, msg + assert 'url="/media/' not in content, msg + assert 'href="/media/' not in content, msg + + def test_all_campaign_game_reward_feeds_skip_enclosures_when_length_unknown( + self, + ) -> None: + """RSS/Atom feeds should not emit enclosure elements with length=0.""" + self.game.box_art = "https://example.com/box.png" + self.game.box_art_size_bytes = None + self.game.save() + + self.campaign.image_url = "https://example.com/campaign.png" + self.campaign.image_size_bytes = None + self.campaign.save() + + self.reward_campaign.image_url = "https://example.com/reward.png" + self.reward_campaign.save() + + feed_urls: list[str] = [ + reverse("twitch:game_feed"), + reverse("twitch:campaign_feed"), + reverse("twitch:game_campaign_feed", args=[self.game.twitch_id]), + reverse("twitch:reward_campaign_feed"), + reverse("twitch:game_feed_atom"), + reverse("twitch:campaign_feed_atom"), + reverse("twitch:game_campaign_feed_atom", args=[self.game.twitch_id]), + reverse("twitch:reward_campaign_feed_atom"), + ] + + for url in feed_urls: + response: _MonkeyPatchedWSGIResponse = self.client.get(url) + assert response.status_code == 200 + content: str = response.content.decode("utf-8") + + msg: str = ( + f"Feed {url} unexpectedly emitted a zero-length enclosure: {content}" + ) + assert 'length="0"' not in content, msg + def test_game_feed_enclosure_helpers(self) -> None: """Helper methods should return values from model fields.""" feed = GameFeed()