131 lines
5.2 KiB
Python
131 lines
5.2 KiB
Python
"""Tests for Store — post loading, saving, deleting, media handling."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
from volumen.post import Post
|
|
from volumen.store import Store
|
|
|
|
|
|
class TestLoading:
|
|
def test_all_loads_every_post(self, store: Store) -> None:
|
|
posts = store.all()
|
|
assert len(posts) == 2 # hello.md + draft.md
|
|
|
|
def test_slug_falls_back_to_filename(self, store: Store) -> None:
|
|
slugs = sorted(p.slug for p in store.all())
|
|
assert slugs == ["draft", "hello"]
|
|
|
|
def test_lang_derived_from_subdirectory(self, tmp_path: Path) -> None:
|
|
content = tmp_path / "posts"
|
|
content.mkdir()
|
|
(content / "hello.md").write_text('+++\ntitle = "Hello"\n+++\n\nBody\n')
|
|
cs_dir = content / "cs"
|
|
cs_dir.mkdir()
|
|
(cs_dir / "ahoj.md").write_text('+++\ntitle = "Ahoj"\n+++\n\nTelo\n')
|
|
st = Store(str(content), default_lang="en")
|
|
assert st.find("ahoj").lang == "cs"
|
|
assert st.find("hello").lang == "en"
|
|
|
|
def test_find_returns_none_when_missing(self, store: Store) -> None:
|
|
assert store.find("nope") is None
|
|
|
|
def test_all_langs_post_visible_in_any_language(self, tmp_content_dir: Path) -> None:
|
|
(tmp_content_dir / "global.md").write_text(
|
|
'+++\ntitle = "Global"\nlang = "en"\nall_langs = true\n+++\n\nVisible everywhere.\n'
|
|
)
|
|
store = Store(str(tmp_content_dir), default_lang="en")
|
|
assert store.find("global", lang="cs") is not None
|
|
assert store.find("global", lang="en") is not None
|
|
|
|
def test_draft_post_is_loaded(self, store: Store) -> None:
|
|
draft = store.find("draft")
|
|
assert draft is not None
|
|
assert draft.draft is True
|
|
|
|
def test_post_with_missing_slug_derived_from_path(self, tmp_content_dir: Path) -> None:
|
|
(tmp_content_dir / "noslug.md").write_text('+++\ntitle = "No Slug"\n+++\n\nBody\n')
|
|
store = Store(str(tmp_content_dir), default_lang="en")
|
|
post = store.find("noslug")
|
|
assert post is not None
|
|
assert post.slug == "noslug"
|
|
assert post.lang == "en"
|
|
|
|
|
|
class TestSave:
|
|
def test_save_new_post(self, tmp_content_dir: Path) -> None:
|
|
store = Store(str(tmp_content_dir), default_lang="en")
|
|
post = Post(metadata={"slug": "newpost", "title": "New"}, body="Content")
|
|
store.save(post)
|
|
assert os.path.isfile(os.path.join(str(tmp_content_dir), "newpost.md"))
|
|
|
|
def test_save_updates_existing_post(self, tmp_content_dir: Path) -> None:
|
|
store = Store(str(tmp_content_dir), default_lang="en")
|
|
post = store.find("hello")
|
|
assert post is not None
|
|
post.body = "Updated content."
|
|
store.save(post)
|
|
reloaded = store.find("hello")
|
|
assert reloaded is not None
|
|
assert "Updated content." in reloaded.body
|
|
|
|
def test_save_rejects_path_traversal_slug(self, tmp_content_dir: Path) -> None:
|
|
store = Store(str(tmp_content_dir))
|
|
post = Post(metadata={"slug": "../../etc/passwd"}, body="x")
|
|
with pytest.raises(ValueError, match="slug escapes"):
|
|
store.save(post)
|
|
|
|
def test_save_uses_lang_subdirectory(self, tmp_content_dir: Path) -> None:
|
|
store = Store(str(tmp_content_dir), default_lang="en")
|
|
post = Post(metadata={"slug": "bonjour", "lang": "fr"}, body="x")
|
|
store.save(post)
|
|
assert os.path.isfile(os.path.join(str(tmp_content_dir), "fr", "bonjour.md"))
|
|
|
|
def test_save_uses_root_for_default_language(self, tmp_content_dir: Path) -> None:
|
|
store = Store(str(tmp_content_dir), default_lang="en")
|
|
post = Post(metadata={"slug": "hello-new", "lang": "en"}, body="x")
|
|
store.save(post)
|
|
assert os.path.isfile(os.path.join(str(tmp_content_dir), "hello-new.md"))
|
|
assert not os.path.isfile(os.path.join(str(tmp_content_dir), "en", "hello-new.md"))
|
|
|
|
|
|
class TestDelete:
|
|
def test_delete_post(self, tmp_content_dir: Path) -> None:
|
|
store = Store(str(tmp_content_dir), default_lang="en")
|
|
path = os.path.join(str(tmp_content_dir), "hello.md")
|
|
assert os.path.isfile(path)
|
|
store.delete("hello")
|
|
assert not os.path.isfile(path)
|
|
|
|
|
|
class TestMedia:
|
|
def test_media_file_path_traversal_prevention(self, tmp_content_dir: Path) -> None:
|
|
store = Store(str(tmp_content_dir))
|
|
bad = store.media_file("../etc/passwd")
|
|
assert bad is None
|
|
|
|
def test_media_file_nonexistent(self, tmp_content_dir: Path) -> None:
|
|
store = Store(str(tmp_content_dir))
|
|
assert store.media_file("nonexistent.webp") is None
|
|
|
|
|
|
class TestCacheInvalidation:
|
|
def test_mtime_cache_invalidation(self, tmp_content_dir: Path) -> None:
|
|
store = Store(str(tmp_content_dir), default_lang="en")
|
|
initial = len(store.all())
|
|
|
|
# Add a new file
|
|
(tmp_content_dir / "new.md").write_text('+++\ntitle = "New"\n+++\n\nBody\n')
|
|
assert len(store.all()) == initial + 1
|
|
|
|
def test_cache_invalidation_on_save(self, tmp_content_dir: Path) -> None:
|
|
store = Store(str(tmp_content_dir), default_lang="en")
|
|
initial = len(store.all())
|
|
post = Post(metadata={"slug": "extra"}, body="x")
|
|
store.save(post)
|
|
assert len(store.all()) == initial + 1
|