from __future__ import annotations from pathlib import Path import shutil import sys import unittest import uuid sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "scripts")) from blog_editor import BlogEditorError, BlogRepository, suggest_slug # noqa: E402 class BlogEditorTests(unittest.TestCase): def setUp(self) -> None: cache_root = Path(__file__).resolve().parents[1] / ".cache" cache_root.mkdir(exist_ok=True) self.site_root = cache_root / f"blog-editor-test-{uuid.uuid4().hex}" (self.site_root / "content" / "blog").mkdir(parents=True) (self.site_root / "content" / "tags" / "writing").mkdir(parents=True) (self.site_root / "models").mkdir() (self.site_root / "Test.lektorproject").write_text( "[project]\nname = Test\n", encoding="utf-8" ) (self.site_root / "content" / "blog" / "contents.lr").write_text( "_model: section\n---\ntitle: Blog\n", encoding="utf-8" ) (self.site_root / "content" / "tags" / "writing" / "contents.lr").write_text( "_model: tag\n---\ntitle: Writing\n---\nsummary: Writing.\n", encoding="utf-8", ) source_model = Path(__file__).resolve().parents[1] / "models" / "entry.ini" shutil.copy2(source_model, self.site_root / "models" / "entry.ini") self.repository = BlogRepository(self.site_root) def tearDown(self) -> None: shutil.rmtree(self.site_root) @staticmethod def values(**overrides: str) -> dict[str, str]: values = { "title": "Test Entry", "date": "2026-08-13", "updated": "", "author": "Test Author", "tags": "writing", "kicker": "Blog", "summary": "A test summary.", "body": "A **Markdown** body.", "external_url": "", "published_urls": "https://example.test/post", } values.update(overrides) return values def test_current_site_blog_records_and_tracked_tags_load(self) -> None: repository = BlogRepository(Path(__file__).resolve().parents[1]) entries = repository.list_entries() self.assertGreaterEqual(len(entries), 7) self.assertGreaterEqual(len(repository.tags), 40) self.assertTrue(all(entry.document.get("kicker") == "Blog" for entry in entries)) for entry in entries: with self.subTest(slug=entry.slug): self.assertEqual( entry.document.render().encode("utf-8"), entry.original_bytes ) def test_create_uses_existing_record_conventions_and_refuses_collision(self) -> None: entry = self.repository.create_entry( "test-entry", self.values(body=" preserved indentation\n\nBody text.") ) rendered = entry.path.read_text(encoding="utf-8") self.assertIn("_model: entry\n---\ntitle: Test Entry\n", rendered) self.assertIn("tags: writing\n", rendered) self.assertIn( "published_urls:\n\nhttps://example.test/post\n---\nbody:\n", rendered, ) with self.assertRaises(BlogEditorError): self.repository.create_entry("test-entry", self.values()) def test_edit_preserves_unmanaged_and_unchanged_blocks_exactly(self) -> None: entry = self.repository.create_entry( "test-entry", self.values(body=" preserved indentation\n\nBody text.") ) original = entry.path.read_text(encoding="utf-8") marker = "---\neditor_note:\nKeep this unknown field exactly. \n" original = original.replace("---\nbody:\n", marker + "---\nbody:\n") entry.path.write_text(original, encoding="utf-8") loaded = self.repository.load_entry("test-entry") values = self.repository.values_for(loaded.document) values["summary"] = "An edited summary." saved = self.repository.save_entry(loaded, values) rendered = saved.path.read_text(encoding="utf-8") self.assertIn(marker, rendered) self.assertIn("summary: An edited summary.\n", rendered) self.assertIn("body:\n preserved indentation\n\nBody text.\n", rendered) self.assertIn("published_urls:\n\nhttps://example.test/post\n", rendered) def test_save_detects_an_external_change(self) -> None: entry = self.repository.create_entry("test-entry", self.values()) with entry.path.open("a", encoding="utf-8") as output: output.write("\n") with self.assertRaisesRegex(BlogEditorError, "changed on disk"): self.repository.save_entry(entry, self.values(summary="Changed")) def test_delete_removes_only_the_loaded_entry_directory(self) -> None: entry = self.repository.create_entry("test-entry", self.values()) attachment = entry.path.parent / "narration.mp3" attachment.write_bytes(b"disposable attachment") neighbor = self.repository.create_entry("neighbor-entry", self.values()) self.repository.delete_entry(entry) self.assertFalse(entry.path.parent.exists()) self.assertTrue(neighbor.path.is_file()) def test_delete_detects_an_external_change(self) -> None: entry = self.repository.create_entry("test-entry", self.values()) with entry.path.open("a", encoding="utf-8") as output: output.write("\n") with self.assertRaisesRegex(BlogEditorError, "changed on disk"): self.repository.delete_entry(entry) self.assertTrue(entry.path.is_file()) def test_freeform_tags_are_normalized_deduplicated_and_do_not_create_records(self) -> None: entry = self.repository.create_entry( "test-entry", self.values(tags="writing, Weird Experiment, weird-experiment") ) self.assertEqual(entry.document.get("tags"), "writing, weird-experiment") self.assertFalse((self.site_root / "content" / "tags" / "weird-experiment").exists()) def test_loading_and_saving_preserves_tracked_and_freeform_tags(self) -> None: entry = self.repository.create_entry( "test-entry", self.values(tags="writing, retrocomputing") ) values = self.repository.values_for(entry.document) values["summary"] = "Edited without changing tags." saved = self.repository.save_entry(entry, values) self.assertEqual(saved.document.get("tags"), "writing, retrocomputing") def test_slug_suggestion_matches_repository_style(self) -> None: self.assertEqual(suggest_slug("We’re Testing: A GUI!"), "were-testing-a-gui") if __name__ == "__main__": unittest.main()