Files
volumen/tests/test_feed_helpers.py
T
petrbalvin 3b8ed31f67
Test / test (push) Successful in 1m7s
Release / release (push) Successful in 1m11s
chore: prepare release v0.4.0
2026-07-26 18:15:29 +02:00

178 lines
6.3 KiB
Python

"""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 '<?xml version="1.0"' in xml
assert "<rss version=" in xml
assert "<title>My Blog</title>" in xml
assert "<link>https://example.com</link>" in xml
assert "<description>A blog.</description>" in xml
assert "<language>en</language>" in xml
assert "<title>Alpha</title>" in xml
assert "<title>Beta</title>" in xml
assert "<link>https://example.com/a</link>" in xml
assert "<link>https://example.com/b</link>" 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("<item>") == 20
def test_escapes_special_characters_in_title(self) -> None:
xml = render_rss_feed(
[_post(title="A & B <C>", slug="x")],
SITE,
"https://x",
)
assert "A &amp; B &lt;C&gt;" in xml
def test_no_pub_date_when_date_missing(self) -> None:
# No `date` in metadata → date_string is None → no <pubDate> in the item.
xml = render_rss_feed([_post(post_date="")], SITE, "https://x")
item_block = xml.split("<item>")[1].split("</item>")[0]
assert "<pubDate>" 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 "<dc:creator>@alice@example.com</dc:creator>" in xml
def test_no_dc_creator_when_fediverse_empty(self) -> None:
xml = render_rss_feed([_post()], SITE, "https://x")
assert "<dc:creator>" 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 "<urlset" in xml
assert "<loc>https://x/x</loc>" in xml
assert "<loc>https://x/y</loc>" in xml
assert "</urlset>" in xml
def test_lastmod_when_date_present(self) -> None:
xml = render_sitemap([_post()], "https://x")
assert "<lastmod>2026-01-15</lastmod>" in xml
def test_no_lastmod_when_date_empty(self) -> None:
# When post_date="" is passed, _post() omits "date" → date_string is None
# → no <lastmod> rendered.
xml = render_sitemap([_post(post_date="")], "https://x")
url_block = xml.split("<url>")[1].split("</url>")[0]
assert "<lastmod>" 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"