151 lines
5.2 KiB
Python
151 lines
5.2 KiB
Python
"""Coverage tests for ``Post`` properties — non-happy-path branches."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from datetime import date, timedelta
|
|
|
|
from volumen.post import Post
|
|
|
|
|
|
def _post(metadata: dict[str, object] | None = None, body: str = "") -> Post:
|
|
return Post(metadata=metadata, body=body)
|
|
|
|
|
|
class TestTagsProperty:
|
|
def test_tags_default_is_empty(self) -> None:
|
|
assert _post().tags == []
|
|
|
|
def test_tags_list_of_strings(self) -> None:
|
|
assert _post({"tags": ["python", "blog"]}).tags == ["python", "blog"]
|
|
|
|
def test_tags_non_list_coerces_to_empty(self) -> None:
|
|
# When ``tags`` is set to something that isn't a list, the property
|
|
# returns an empty list rather than raising.
|
|
assert _post({"tags": "python"}).tags == []
|
|
assert _post({"tags": 42}).tags == []
|
|
assert _post({"tags": None}).tags == []
|
|
|
|
|
|
class TestTranslationsProperty:
|
|
def test_default_is_empty(self) -> None:
|
|
assert _post().translations == {}
|
|
|
|
def test_dict_coerces_values_to_strings(self) -> None:
|
|
out = _post({"translations": {"cs": "cs-slug", "de": "de-slug"}}).translations
|
|
assert out == {"cs": "cs-slug", "de": "de-slug"}
|
|
|
|
def test_non_dict_returns_empty(self) -> None:
|
|
assert _post({"translations": ["cs", "de"]}).translations == {}
|
|
assert _post({"translations": "cs"}).translations == {}
|
|
|
|
|
|
class TestFediverseCreator:
|
|
def test_none(self) -> None:
|
|
assert _post().fediverse_creator is None
|
|
|
|
def test_empty_string_is_none(self) -> None:
|
|
assert _post({"fediverse_creator": ""}).fediverse_creator is None
|
|
assert _post({"fediverse_creator": " "}).fediverse_creator is None
|
|
|
|
def test_valid_fediverse_creator_regex(self) -> None:
|
|
p = _post({"fediverse_creator": "@alice@example.com"})
|
|
assert p.fediverse_creator == "@alice@example.com"
|
|
assert p.valid_fediverse_creator is True
|
|
|
|
def test_invalid_fediverse_creator(self) -> None:
|
|
# No `@host` part → regex doesn't match.
|
|
p = _post({"fediverse_creator": "alice"})
|
|
assert p.fediverse_creator == "alice"
|
|
assert p.valid_fediverse_creator is False
|
|
|
|
|
|
class TestPublishAt:
|
|
def test_none(self) -> None:
|
|
assert _post().publish_at is None
|
|
|
|
def test_date_object(self) -> None:
|
|
assert _post({"publish_at": date(2026, 6, 1)}).publish_at == date(2026, 6, 1)
|
|
|
|
def test_iso_string(self) -> None:
|
|
assert _post({"publish_at": "2026-06-01"}).publish_at == date(2026, 6, 1)
|
|
|
|
def test_invalid_string_returns_none(self) -> None:
|
|
assert _post({"publish_at": "not-a-date"}).publish_at is None
|
|
assert _post({"publish_at": 12345}).publish_at is None
|
|
|
|
|
|
class TestScheduled:
|
|
def test_past_date_is_not_scheduled(self) -> None:
|
|
past = (date.today() - timedelta(days=1)).isoformat()
|
|
assert _post({"publish_at": past}).scheduled is False
|
|
|
|
def test_future_date_is_scheduled(self) -> None:
|
|
future = (date.today() + timedelta(days=1)).isoformat()
|
|
assert _post({"publish_at": future}).scheduled is True
|
|
|
|
def test_no_publish_at(self) -> None:
|
|
assert _post().scheduled is False
|
|
|
|
|
|
class TestReadingTime:
|
|
def test_empty_body_minimum_one_minute(self) -> None:
|
|
assert _post(body="").reading_time == 1
|
|
|
|
def test_short_body(self) -> None:
|
|
# 5 words → ceil(5/200) = 1.
|
|
assert _post(body="one two three four five").reading_time == 1
|
|
|
|
def test_long_body(self) -> None:
|
|
body = " ".join(f"word{i}" for i in range(500))
|
|
# 500 words → ceil(500/200) = 3.
|
|
assert _post(body=body).reading_time == 3
|
|
|
|
|
|
class TestExcerpt:
|
|
def test_explicit_excerpt(self) -> None:
|
|
assert _post({"excerpt": "Short summary"}).excerpt == "Short summary"
|
|
|
|
def test_derived_from_body(self) -> None:
|
|
# No explicit excerpt → derived from first non-heading paragraph.
|
|
p = _post(body="Hello **world**.")
|
|
assert "Hello" in p.excerpt
|
|
assert "<" not in p.excerpt # HTML stripped
|
|
assert "*" not in p.excerpt # Markdown stripped
|
|
|
|
def test_derived_empty_when_only_headings(self) -> None:
|
|
assert _post(body="# Heading\n\n## Subheading").excerpt == ""
|
|
|
|
|
|
class TestSummaryAndDetail:
|
|
def test_summary_includes_all_known_fields(self) -> None:
|
|
p = _post(
|
|
{
|
|
"title": "T",
|
|
"slug": "s",
|
|
"date": "2026-01-15",
|
|
"lang": "en",
|
|
"tags": ["x"],
|
|
"author": "A",
|
|
"fediverse_creator": "@a@example.com",
|
|
"cover": "media/c.webp",
|
|
},
|
|
body="Body.",
|
|
)
|
|
s = p.summary()
|
|
assert s["slug"] == "s"
|
|
assert s["title"] == "T"
|
|
assert s["date"] == "2026-01-15"
|
|
assert s["lang"] == "en"
|
|
assert s["tags"] == ["x"]
|
|
assert s["author"] == "A"
|
|
assert s["fediverse_creator"] == "@a@example.com"
|
|
assert s["cover"] == "media/c.webp"
|
|
assert s["url"] == "/api/volumen/posts/s"
|
|
|
|
def test_detail_includes_body_and_html(self) -> None:
|
|
p = _post({"title": "T", "slug": "s"}, body="Hello.")
|
|
d = p.detail()
|
|
assert d["body"] == "Hello."
|
|
assert "html" in d
|
|
assert "<p>" in d["html"]
|