mirror of
https://github.com/tanrax/org-social-relay
synced 2026-01-09 22:53:32 +01:00
459 lines
16 KiB
Python
459 lines
16 KiB
Python
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=<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=<feed_url> 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("<p>", 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("<p>", 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"])
|