Files

208 lines
7.3 KiB
Python

"""Tests for Post model — parsing, metadata, rendering, serialisation."""
from __future__ import annotations
from datetime import date
import pytest
from volumen.post import Post
class TestPostParsing:
def test_parse_builds_post(self) -> None:
content = (
"+++\n"
'title = "Hello"\n'
'slug = "hello"\n'
'lang = "en"\n'
'tags = ["intro", "ruby"]\n'
"date = 2026-01-15\n"
"draft = true\n"
"+++\n\n"
"First paragraph.\n"
)
post = Post.parse(content)
assert post.slug == "hello"
assert post.title == "Hello"
assert post.lang == "en"
assert post.tags == ["intro", "ruby"]
assert post.draft is True
assert post.date_string == "2026-01-15"
def test_parse_preserves_body_newlines(self) -> None:
content = '+++\ntitle = "X"\nslug = "x"\n+++\n\nLine1\n\nLine2\n'
post = Post.parse(content)
assert "Line1" in post.body
assert "Line2" in post.body
class TestExcerpt:
def test_excerpt_from_metadata(self) -> None:
post = Post(metadata={"slug": "x", "excerpt": "Custom blurb."}, body="Long body text here.")
assert post.excerpt == "Custom blurb."
def test_excerpt_derived_from_body(self) -> None:
post = Post(metadata={"slug": "x"}, body="# Heading\n\nThe body paragraph.")
assert post.excerpt == "The body paragraph."
def test_derived_excerpt_strips_html_tags(self) -> None:
post = Post(metadata={"slug": "x"}, body="<strong>Bold</strong> and <em>italic</em>.")
assert post.excerpt == "Bold and italic."
def test_derived_excerpt_skips_heading(self) -> None:
post = Post(metadata={"slug": "x"}, body="# Heading\n\nText here.")
assert post.excerpt == "Text here."
def test_derived_excerpt_empty_body(self) -> None:
post = Post(metadata={"slug": "x"}, body="")
assert post.excerpt == ""
def test_derived_excerpt_truncates_long(self) -> None:
long_text = "A" * 300
post = Post(metadata={"slug": "x"}, body=long_text)
assert len(post.excerpt) <= 203 # limit + ellipsis
assert post.excerpt.endswith("\u2026")
class TestSummaryDict:
def test_summary_shape(self) -> None:
post = Post(metadata={"slug": "x", "title": "X"}, body="Body")
summary = post.summary()
assert summary["url"] == "/api/volumen/posts/x"
assert summary["title"] == "X"
assert summary["slug"] == "x"
def test_summary_includes_cover_and_reading_time(self) -> None:
post = Post(metadata={"slug": "x", "cover": "/media/c.png"}, body="word " * 250)
assert post.summary()["cover"] == "/media/c.png"
assert post.summary()["reading_time"] == 2
def test_summary_includes_fediverse_creator(self) -> None:
post = Post(
metadata={"slug": "x", "fediverse_creator": "@user@example.social"},
body="body",
)
assert post.summary()["fediverse_creator"] == "@user@example.social"
def test_summary_omits_fediverse_creator_when_absent(self) -> None:
post = Post(metadata={"slug": "x"}, body="body")
assert post.summary()["fediverse_creator"] is None
class TestDetailDict:
def test_detail_includes_rendered_html(self) -> None:
post = Post(metadata={"slug": "x"}, body="**hi**")
assert "<strong>hi</strong>" in post.detail()["html"]
def test_detail_includes_body(self) -> None:
post = Post(metadata={"slug": "x"}, body="raw body")
assert post.detail()["body"] == "raw body"
class TestReadingTime:
def test_reading_time_is_at_least_one(self) -> None:
post = Post(metadata={"slug": "x"}, body="short")
assert post.reading_time == 1
def test_reading_time_200_words(self) -> None:
post = Post(metadata={"slug": "x"}, body="word " * 200)
assert post.reading_time == 1
def test_reading_time_201_words(self) -> None:
post = Post(metadata={"slug": "x"}, body="word " * 201)
assert post.reading_time == 2
class TestScheduled:
def test_scheduled_post_when_publish_at_is_future(self) -> None:
future = date.today().replace(year=date.today().year + 1)
post = Post(metadata={"publish_at": future})
assert post.scheduled is True
def test_scheduled_post_when_publish_at_is_past(self) -> None:
past = date(2000, 1, 1)
post = Post(metadata={"publish_at": past})
assert post.scheduled is False
def test_post_without_publish_at_is_not_scheduled(self) -> None:
post = Post(metadata={})
assert post.scheduled is False
class TestFediverseCreator:
def test_fediverse_creator_returns_metadata_value(self) -> None:
post = Post(metadata={"slug": "x", "fediverse_creator": "@user@example.social"})
assert post.fediverse_creator == "@user@example.social"
def test_fediverse_creator_is_none_when_missing(self) -> None:
post = Post(metadata={"slug": "x"})
assert post.fediverse_creator is None
def test_fediverse_creator_is_none_when_blank(self) -> None:
post = Post(metadata={"slug": "x", "fediverse_creator": " "})
assert post.fediverse_creator is None
@pytest.mark.parametrize(
"handle",
[
"@ada@lovelace.org",
"@user@example.com",
" @user@example.com ",
],
)
def test_valid_fediverse_creator_accepts_well_formed(self, handle: str) -> None:
post = Post(metadata={"slug": "x", "fediverse_creator": handle})
assert post.valid_fediverse_creator is True
@pytest.mark.parametrize(
"handle",
[
"ada@example.com",
"@only-one-at",
"@user@example.com extra",
"@user@",
"@@example.com",
],
)
def test_valid_fediverse_creator_rejects_malformed(self, handle: str) -> None:
post = Post(metadata={"slug": "x", "fediverse_creator": handle})
assert post.valid_fediverse_creator is False
class TestDateString:
def test_date_string_from_string(self) -> None:
post = Post(metadata={"date": "2026-01-15"})
assert post.date_string == "2026-01-15"
def test_date_string_from_date(self) -> None:
post = Post(metadata={"date": date(2026, 1, 15)})
assert post.date_string == "2026-01-15"
def test_date_string_none_when_missing(self) -> None:
post = Post(metadata={})
assert post.date_string is None
class TestTranslations:
def test_translations_from_metadata(self) -> None:
post = Post(metadata={"slug": "x", "translations": {"cs": "ahoj", "de": "hallo"}})
assert post.translations == {"cs": "ahoj", "de": "hallo"}
def test_translations_empty_when_missing(self) -> None:
post = Post(metadata={"slug": "x"})
assert post.translations == {}
class TestFileSerialization:
def test_round_trip(self) -> None:
metadata = {"slug": "hello", "title": "Hello", "tags": ["intro"], "draft": False}
post = Post(metadata=metadata, body="Hello **world**.\n")
serialised = post.to_file()
reloaded = Post.parse(serialised)
assert reloaded.slug == "hello"
assert reloaded.title == "Hello"
assert reloaded.tags == ["intro"]
assert reloaded.draft is False
assert "Hello **world**." in reloaded.body