"""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