Files
github_release_monitor/tests/test_rss.py
T
ARIA 5cd809930e Add per-repo cover image support for RSS feed items
Add image_url column to repos table with migration, CLI image add/remove
commands, and Media RSS <media:content> elements in generated feed output.

New CLI commands: ghrel image add <repo> <url>, ghrel image remove <repo>
2026-08-05 11:42:32 +02:00

185 lines
6.7 KiB
Python

"""Tests for RSS feed generation."""
import xml.etree.ElementTree as ET
from src.rss import generate_feed, strip_markdown, format_rfc822
class TestStripMarkdown:
def test_plain_text(self):
assert strip_markdown("Hello world") == "Hello world"
def test_bold_and_lists(self):
result = strip_markdown("**bold** and - item1\n- item2")
assert "bold" in result
assert "item1" in result
def test_empty_string(self):
assert strip_markdown("") == ""
def test_none(self):
assert strip_markdown("") == ""
def test_code_blocks(self):
result = strip_markdown("```python\nprint('hello')\n```")
assert "print" in result or "hello" in result
class TestFormatRfc822:
def test_iso_to_rfc822(self):
result = format_rfc822("2025-01-15T10:30:00Z")
assert "Wed" in result or "15" in result
assert "2025" in result
def test_with_tz_offset(self):
result = format_rfc822("2025-01-15T10:30:00+00:00")
assert "2025" in result
class TestGenerateFeed:
def _entries(self, count=1):
return [
{
"tag_name": f"v1.{i}.0",
"title": f"Release v1.{i}.0",
"body": f"# Release notes\n\n- Feature {i}",
"published_at": "2025-01-15T10:30:00Z",
"html_url": f"https://github.com/owner/repo/releases/tag/v1.{i}.0",
"owner_repo": "owner/repo",
}
for i in range(count)
]
def test_basic_feed(self):
entries = self._entries(2)
xml_str = generate_feed(entries)
root = ET.fromstring(xml_str)
assert root.tag == "rss"
assert root.get("version") == "2.0"
channel = root[0]
assert channel.tag == "channel"
assert channel.find("title").text == "GitHub Release Monitor"
assert channel.find("description").text == "Latest releases from tracked GitHub repositories"
items = channel.findall("item")
assert len(items) == 2
def test_item_structure(self):
entries = self._entries(1)
xml_str = generate_feed(entries)
root = ET.fromstring(xml_str)
channel = root[0]
item = channel.find("item")
assert item.find("title").text == "[owner/repo] Release v1.0.0"
assert "Feature 0" in item.find("description").text
assert item.find("link").text == "https://github.com/owner/repo/releases/tag/v1.0.0"
assert item.find("pubDate").text is not None
assert item.find("guid").text == "github.com/owner/repo/releases/tag/v1.0.0"
def test_description_truncation(self):
entry = {
"tag_name": "v1.0.0",
"title": "Release",
"body": "A" * 400,
"published_at": "2025-01-15T10:30:00Z",
"html_url": "https://example.com",
"owner_repo": "owner/repo",
}
xml_str = generate_feed([entry])
root = ET.fromstring(xml_str)
desc = root[0].find("item").find("description").text
assert len(desc) == 301 # 300 chars + 1 ellipsis char
def test_tag_entry_no_body(self):
entry = {
"tag_name": "v1.0.0",
"title": "v1.0.0",
"body": "",
"published_at": "2025-01-15T10:30:00Z",
"html_url": "https://github.com/owner/repo/tags/v1.0.0",
"owner_repo": "owner/repo",
}
xml_str = generate_feed([entry])
root = ET.fromstring(xml_str)
desc = root[0].find("item").find("description").text
assert desc == "Tag v1.0.0"
def test_item_cap(self):
entries = self._entries(100)
xml_str = generate_feed(entries)
root = ET.fromstring(xml_str)
items = root[0].findall("item")
assert len(items) == 50 # RSS_MAX_ITEMS
def test_xml_escaping(self):
entry = {
"tag_name": "v1.0.0",
"title": "Release with <special> & 'chars'",
"body": "Some **bold** text",
"published_at": "2025-01-15T10:30:00Z",
"html_url": "https://example.com?a=1&b=2",
"owner_repo": "owner/repo",
}
xml_str = generate_feed([entry])
# Should parse without error
root = ET.fromstring(xml_str)
assert root is not None
def test_base_url(self):
entries = self._entries(1)
xml_str = generate_feed(entries, base_url="http://example.com:9000")
root = ET.fromstring(xml_str)
assert root[0].find("link").text == "http://example.com:9000/"
def test_media_content_with_image(self):
entries = self._entries(1)
entries[0]["image_url"] = "https://example.com/logo.png"
xml_str = generate_feed(entries)
root = ET.fromstring(xml_str)
assert root is not None
media_el = root[0].find("item").find("{http://search.yahoo.com/mrss/}content")
assert media_el is not None
assert media_el.get("url") == "https://example.com/logo.png"
assert media_el.get("medium") == "image"
assert "media" in xml_str or "search.yahoo.com/mrss" in xml_str
def test_no_media_content_without_image(self):
entries = self._entries(1)
entries[0]["image_url"] = None
xml_str = generate_feed(entries)
root = ET.fromstring(xml_str)
media_el = root[0].find("item").find("{http://search.yahoo.com/mrss/}content")
assert media_el is None
def test_no_media_content_when_key_missing(self):
entries = self._entries(1)
entries[0]["image_url"] = None
xml_str = generate_feed(entries)
root = ET.fromstring(xml_str)
media_el = root[0].find("item").find("{http://search.yahoo.com/mrss/}content")
assert media_el is None
def test_media_content_multiple_items_selective(self):
entries = self._entries(2)
entries[0]["image_url"] = "https://example.com/logo1.png"
entries[1]["image_url"] = None
xml_str = generate_feed(entries)
root = ET.fromstring(xml_str)
items = root[0].findall("item")
media_1 = items[0].find("{http://search.yahoo.com/mrss/}content")
media_2 = items[1].find("{http://search.yahoo.com/mrss/}content")
assert media_1 is not None
assert media_1.get("url") == "https://example.com/logo1.png"
assert media_2 is None
def test_image_with_special_chars_in_url(self):
entries = self._entries(1)
entries[0]["image_url"] = "https://example.com/logo.png?w=200&h=200"
xml_str = generate_feed(entries)
root = ET.fromstring(xml_str)
assert root is not None
media_el = root[0].find("item").find("{http://search.yahoo.com/mrss/}content")
assert "w=200" in media_el.get("url")