refactor: rewrite from Ruby/Sinatra to Python/FastAPI
This commit is contained in:
@@ -0,0 +1,207 @@
|
||||
"""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
|
||||
Reference in New Issue
Block a user