208 lines
7.3 KiB
Python
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
|