"""Coverage tests for feed helpers — RSS, JSON Feed, sitemap.""" from __future__ import annotations from datetime import date from typing import Any from volumen.feed_helpers import ( _iso_date, _rfc822_date, render_json_feed, render_rss_feed, render_sitemap, ) from volumen.post import Post SITE: dict[str, Any] = { "title": "My Blog", "description": "A blog.", "language": "en", } def _post( *, title: str = "Hello", slug: str = "hello", post_date: str = "2026-01-15", tags: list[str] | None = None, excerpt: str = "", fediverse_creator: str = "", body: str = "Body.", ) -> Post: """Build a Post with optional fields, simulating parse() without frontmatter I/O.""" metadata: dict[str, Any] = {"title": title, "slug": slug} if post_date: metadata["date"] = post_date if tags is not None: metadata["tags"] = tags if excerpt: metadata["excerpt"] = excerpt if fediverse_creator: metadata["fediverse_creator"] = fediverse_creator return Post(metadata=metadata, body=body) class TestRenderRSSFeed: def test_basic_two_posts(self) -> None: posts = [ _post(slug="a", title="Alpha"), _post(slug="b", title="Beta"), ] xml = render_rss_feed(posts, SITE, "https://example.com") assert 'My Blog" in xml assert "https://example.com" in xml assert "A blog." in xml assert "en" in xml assert "Alpha" in xml assert "Beta" in xml assert "https://example.com/a" in xml assert "https://example.com/b" in xml assert "pubDate" in xml def test_truncates_to_20_items(self) -> None: posts = [_post(slug=f"p{i:02d}") for i in range(25)] xml = render_rss_feed(posts, SITE, "https://x") assert xml.count("") == 20 def test_escapes_special_characters_in_title(self) -> None: xml = render_rss_feed( [_post(title="A & B ", slug="x")], SITE, "https://x", ) assert "A & B <C>" in xml def test_no_pub_date_when_date_missing(self) -> None: # No `date` in metadata → date_string is None → no in the item. xml = render_rss_feed([_post(post_date="")], SITE, "https://x") item_block = xml.split("")[1].split("")[0] assert "" not in item_block def test_dc_creator_present_when_fediverse_set(self) -> None: xml = render_rss_feed( [_post(fediverse_creator="@alice@example.com")], SITE, "https://x", ) assert "@alice@example.com" in xml def test_no_dc_creator_when_fediverse_empty(self) -> None: xml = render_rss_feed([_post()], SITE, "https://x") assert "" not in xml class TestRenderJSONFeed: def test_basic_shape(self) -> None: feed = render_json_feed([_post(slug="a", tags=["intro"])], SITE, "https://x") assert feed["version"] == "https://jsonfeed.org/version/1.1" assert feed["title"] == "My Blog" assert feed["home_page_url"] == "https://x" assert feed["feed_url"] == "https://x/api/volumen/feed.json" assert feed["language"] == "en" assert feed["items"][0]["id"] == "https://x/a" assert feed["items"][0]["url"] == "https://x/a" assert feed["items"][0]["title"] == "Hello" assert feed["items"][0]["tags"] == ["intro"] def test_filters_empty_authors_and_tags(self) -> None: # Default _post has no fediverse handle, no tags. ``summary`` is also # present (it falls back to the derived excerpt from ``body``). feed = render_json_feed([_post()], SITE, "https://x") item = feed["items"][0] assert "authors" not in item assert "tags" not in item def test_author_from_site_wide_fediverse(self) -> None: site_with_creator = {**SITE, "fediverse_creator": "@site@example.com"} feed = render_json_feed([_post()], site_with_creator, "https://x") assert feed["items"][0]["authors"] == [{"name": "@site@example.com"}] def test_author_from_post_overrides_site(self) -> None: site_with_creator = {**SITE, "fediverse_creator": "@site@example.com"} feed = render_json_feed( [_post(fediverse_creator="@post@example.com")], site_with_creator, "https://x", ) assert feed["items"][0]["authors"] == [{"name": "@post@example.com"}] class TestRenderSitemap: def test_basic_urlset(self) -> None: xml = render_sitemap([_post(slug="x"), _post(slug="y")], "https://x") assert "https://x/x" in xml assert "https://x/y" in xml assert "" in xml def test_lastmod_when_date_present(self) -> None: xml = render_sitemap([_post()], "https://x") assert "2026-01-15" in xml def test_no_lastmod_when_date_empty(self) -> None: # When post_date="" is passed, _post() omits "date" → date_string is None # → no rendered. xml = render_sitemap([_post(post_date="")], "https://x") url_block = xml.split("")[1].split("")[0] assert "" not in url_block class TestDateHelpers: def test_rfc822_from_date_object(self) -> None: out = _rfc822_date(date(2026, 1, 15)) assert "2026" in out and "GMT" in out def test_rfc822_from_iso_string(self) -> None: out = _rfc822_date("2026-01-15") assert "2026" in out assert "GMT" in out def test_rfc822_invalid_string_returns_as_is(self) -> None: assert _rfc822_date("not-a-date") == "not-a-date" def test_iso_none(self) -> None: assert _iso_date(None) is None def test_iso_date_object(self) -> None: assert _iso_date(date(2026, 1, 15)) == "2026-01-15" def test_iso_valid_string_passes_through(self) -> None: assert _iso_date("2026-01-15") == "2026-01-15" def test_iso_invalid_string_returns_as_is(self) -> None: assert _iso_date("not-a-date") == "not-a-date"