"""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="Bold and italic.") 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 "hi" 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