from django.test import TestCase, override_settings from rest_framework.test import APIClient from rest_framework import status from xml.etree import ElementTree as ET from app.feeds.models import Profile, Post class RSSFeedTest(TestCase): """Test cases for the RSS Feed using Given/When/Then structure.""" def setUp(self): self.client = APIClient() self.rss_url = "/rss.xml" # Create test profiles self.profile1 = Profile.objects.create( feed="https://example.com/social.org", title="Example Profile", nick="example_user", description="Test profile 1", ) self.profile2 = Profile.objects.create( feed="https://test.com/social.org", title="Test Profile", nick="test_user", description="Test profile 2", ) self.profile3 = Profile.objects.create( feed="https://third.com/social.org", title="Third Profile", nick="third_user", description="Test profile 3", ) # Create test posts with different content and tags self.post1 = Post.objects.create( profile=self.profile1, post_id="2025-01-01T12:00:00+00:00", content="This post is about Emacs and org-mode", tags="emacs org-mode", ) self.post2 = Post.objects.create( profile=self.profile2, post_id="2025-01-01T13:00:00+00:00", content="Learning Python programming language", tags="python programming", ) self.post3 = Post.objects.create( profile=self.profile3, post_id="2025-01-01T14:00:00+00:00", content="Django web framework with Python", tags="django python web", ) self.post4 = Post.objects.create( profile=self.profile1, post_id="2025-01-01T15:00:00+00:00", content="Emacs configuration and setup", tags="emacs configuration", ) self.post5 = Post.objects.create( profile=self.profile2, post_id="2025-01-01T16:00:00+00:00", content="JavaScript and React development", tags="javascript react frontend", ) def test_rss_feed_all_posts_success(self): """Test GET /rss.xml returns RSS feed with all posts.""" # Given: Posts exist in the database # When: We request the RSS feed response = self.client.get(self.rss_url) # Then: Should return 200 and valid RSS self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response["Content-Type"], "application/rss+xml; charset=utf-8") # Parse XML and verify structure root = ET.fromstring(response.content) self.assertEqual(root.tag, "rss") self.assertEqual(root.get("version"), "2.0") # Verify channel channel = root.find("channel") self.assertIsNotNone(channel) # Verify title title = channel.find("title") self.assertIsNotNone(title) self.assertEqual(title.text, "Org Social Relay - Latest Posts") # Verify items items = channel.findall("item") self.assertEqual(len(items), 5) # All 5 posts # Verify first item (most recent) first_item = items[0] guid = first_item.find("guid") self.assertIsNotNone(guid) self.assertIn(self.post5.post_id, guid.text) def test_rss_feed_filtered_by_tag(self): """Test GET /rss.xml?tag= returns RSS feed filtered by tag.""" # Given: Posts with various tags # When: We request the RSS feed filtered by tag "emacs" response = self.client.get(self.rss_url, {"tag": "emacs"}) # Then: Should return 200 and filtered RSS self.assertEqual(response.status_code, status.HTTP_200_OK) # Parse XML root = ET.fromstring(response.content) channel = root.find("channel") # Verify title includes tag title = channel.find("title") self.assertIn("emacs", title.text.lower()) # Verify only posts with "emacs" tag are included items = channel.findall("item") self.assertEqual(len(items), 2) # post1 and post4 # Verify all items have the emacs tag for item in items: categories = [cat.text for cat in item.findall("category")] self.assertIn("emacs", categories) def test_rss_feed_filtered_by_feed(self): """Test GET /rss.xml?feed= returns RSS feed filtered by author feed.""" # Given: Posts from different profiles # When: We request the RSS feed filtered by profile1's feed response = self.client.get(self.rss_url, {"feed": self.profile1.feed}) # Then: Should return 200 and filtered RSS self.assertEqual(response.status_code, status.HTTP_200_OK) # Parse XML root = ET.fromstring(response.content) channel = root.find("channel") # Verify title includes feed title = channel.find("title") self.assertIn(self.profile1.feed, title.text) # Verify only posts from profile1 are included items = channel.findall("item") self.assertEqual(len(items), 2) # post1 and post4 # Verify all items are from profile1 for item in items: link = item.find("link") self.assertIn(self.profile1.feed, link.text) def test_rss_feed_item_structure(self): """Test that RSS feed items have correct structure.""" # Given: Posts exist # When: We request the RSS feed response = self.client.get(self.rss_url) # Parse XML root = ET.fromstring(response.content) channel = root.find("channel") items = channel.findall("item") # Then: Each item should have required fields for item in items: # Required fields self.assertIsNotNone(item.find("title")) self.assertIsNotNone(item.find("link")) self.assertIsNotNone(item.find("description")) self.assertIsNotNone(item.find("pubDate")) self.assertIsNotNone(item.find("guid")) self.assertIsNotNone(item.find("author")) def test_rss_feed_limit_200_posts(self): """Test that RSS feed is limited to 200 posts as per specification.""" # Given: More than 200 posts for i in range(201): Post.objects.create( profile=self.profile1, post_id=f"2025-01-02T{i // 60:02d}:{i % 60:02d}:00+00:00", content=f"Post number {i}", tags="test", ) # When: We request the RSS feed response = self.client.get(self.rss_url) # Parse XML root = ET.fromstring(response.content) channel = root.find("channel") items = channel.findall("item") # Then: Should be limited to 200 posts self.assertEqual(len(items), 200) def test_rss_feed_ordered_by_most_recent(self): """Test that RSS feed items are ordered from most recent to oldest.""" # Given: Posts with different timestamps # When: We request the RSS feed response = self.client.get(self.rss_url) # Parse XML root = ET.fromstring(response.content) channel = root.find("channel") items = channel.findall("item") # Then: First item should be the most recent post first_item = items[0] first_guid = first_item.find("guid").text self.assertIn(self.post5.post_id, first_guid) # Most recent # Last item should be the oldest post last_item = items[-1] last_guid = last_item.find("guid").text self.assertIn(self.post1.post_id, last_guid) # Oldest def test_rss_feed_categories_from_tags(self): """Test that post tags are converted to RSS categories.""" # Given: Post with multiple tags # When: We request the RSS feed response = self.client.get(self.rss_url) # Parse XML root = ET.fromstring(response.content) channel = root.find("channel") items = channel.findall("item") # Find item for post3 (has multiple tags) post3_item = None for item in items: guid = item.find("guid").text if self.post3.post_id in guid: post3_item = item break self.assertIsNotNone(post3_item) # Then: Categories should match tags categories = [cat.text for cat in post3_item.findall("category")] expected_tags = self.post3.tags.split() self.assertEqual(set(categories), set(expected_tags)) def test_rss_feed_guid_is_post_url(self): """Test that GUID is the post URL.""" # Given: Posts exist # When: We request the RSS feed response = self.client.get(self.rss_url) # Parse XML root = ET.fromstring(response.content) channel = root.find("channel") items = channel.findall("item") # Then: Each GUID should be feed#post_id format for item in items: guid = item.find("guid") self.assertIsNotNone(guid) self.assertIn("#", guid.text) self.assertTrue(guid.get("isPermaLink") in ["true", None]) def test_rss_feed_content_type(self): """Test that RSS feed returns correct content type.""" # Given: RSS endpoint # When: We request the RSS feed response = self.client.get(self.rss_url) # Then: Content-Type should be application/rss+xml self.assertEqual(response["Content-Type"], "application/rss+xml; charset=utf-8") def test_rss_feed_valid_xml(self): """Test that RSS feed returns valid XML.""" # Given: RSS endpoint # When: We request the RSS feed response = self.client.get(self.rss_url) # Then: Should be parseable as XML without errors try: ET.fromstring(response.content) except ET.ParseError as e: self.fail(f"RSS feed is not valid XML: {e}") def test_rss_feed_description_is_post_content(self): """Test that description contains the post content (converted to HTML).""" # Given: Posts with content # When: We request the RSS feed response = self.client.get(self.rss_url) # Parse XML root = ET.fromstring(response.content) channel = root.find("channel") items = channel.findall("item") # Find item for post1 post1_item = None for item in items: guid = item.find("guid").text if self.post1.post_id in guid: post1_item = item break self.assertIsNotNone(post1_item) # Then: Description should contain HTML-converted content description = post1_item.find("description") self.assertIsNotNone(description.text) # Content is now converted to HTML, so we check for HTML elements self.assertIn("

", description.text) # The original content should be present in the HTML self.assertIn("This post is about Emacs and org-mode", description.text) def test_rss_feed_author_is_nick(self): """Test that author is the profile nick.""" # Given: Posts from different profiles # When: We request the RSS feed response = self.client.get(self.rss_url) # Parse XML root = ET.fromstring(response.content) channel = root.find("channel") items = channel.findall("item") # Find item for post1 post1_item = None for item in items: guid = item.find("guid").text if self.post1.post_id in guid: post1_item = item break self.assertIsNotNone(post1_item) # Then: Author should be the profile nick author = post1_item.find("author") self.assertIn(self.profile1.nick, author.text) def test_rss_feed_empty_when_no_posts(self): """Test RSS feed when there are no posts.""" # Given: No posts exist Post.objects.all().delete() # When: We request the RSS feed response = self.client.get(self.rss_url) # Then: Should still return valid RSS with no items self.assertEqual(response.status_code, status.HTTP_200_OK) # Parse XML root = ET.fromstring(response.content) channel = root.find("channel") items = channel.findall("item") # Should have 0 items self.assertEqual(len(items), 0) def test_rss_feed_link_is_post_url(self): """Test that link element is the post URL.""" # Given: Posts exist # When: We request the RSS feed response = self.client.get(self.rss_url) # Parse XML root = ET.fromstring(response.content) channel = root.find("channel") items = channel.findall("item") # Then: Each link should be feed#post_id format for item in items: link = item.find("link") guid = item.find("guid") # Link and GUID should be the same self.assertEqual(link.text, guid.text) self.assertIn("#", link.text) def test_rss_feed_excludes_empty_content(self): """Test that RSS feed excludes posts with empty content (reactions, votes, etc.).""" # Given: Posts with and without content # Create posts with empty content (reactions, votes) Post.objects.create( profile=self.profile1, post_id="2025-01-01T17:00:00+00:00", content="", # Empty content (reaction/vote) tags="reaction", ) Post.objects.create( profile=self.profile2, post_id="2025-01-01T18:00:00+00:00", content=" ", # Whitespace only tags="vote", ) # When: We request the RSS feed response = self.client.get(self.rss_url) # Parse XML root = ET.fromstring(response.content) channel = root.find("channel") items = channel.findall("item") # Then: Should only include posts with content (5 original posts) # Empty and whitespace-only posts should be excluded self.assertEqual(len(items), 5) # Verify all items have non-empty description for item in items: description = item.find("description") # Description should exist and have content self.assertIsNotNone(description.text) # After HTML conversion, should have actual content self.assertIn("

", description.text) @override_settings( CACHES={ "default": { "BACKEND": "django.core.cache.backends.locmem.LocMemCache", "LOCATION": "test-cache", } } ) def test_rss_feed_xml_caching(self): """Test that RSS feed XML response is cached for performance.""" from django.core.cache import cache # Given: Posts exist and cache is cleared cache_key = "rss_xml_all" cache.delete(cache_key) # When: We request the RSS feed for the first time response1 = self.client.get(self.rss_url) self.assertEqual(response1.status_code, status.HTTP_200_OK) # Then: The XML should be cached cached_xml = cache.get(cache_key) self.assertIsNotNone(cached_xml) # When: We request the RSS feed again response2 = self.client.get(self.rss_url) self.assertEqual(response2.status_code, status.HTTP_200_OK) # Then: Both responses should be identical self.assertEqual(response1.content, response2.content) self.assertEqual(response1["Content-Type"], response2["Content-Type"])