Add blog and article narration import
Deploy production / deploy (push) Successful in 4s

This commit is contained in:
2026-08-13 15:12:33 -07:00
parent a2e405cb96
commit 2fe6b689b0
8 changed files with 2672 additions and 0 deletions
+187
View File
@@ -0,0 +1,187 @@
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 article_editor import ArticleEditorError, ArticleRepository, suggest_slug # noqa: E402
class ArticleEditorTests(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"article-editor-test-{uuid.uuid4().hex}"
(self.site_root / "content" / "articles").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" / "articles" / "contents.lr").write_text(
"_model: section\n---\ntitle: Articles\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 = ArticleRepository(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": "Article",
"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_article_records_and_controlled_tags_load(self) -> None:
repository = ArticleRepository(Path(__file__).resolve().parents[1])
entries = repository.list_entries()
self.assertGreaterEqual(len(entries), 7)
self.assertGreaterEqual(len(repository.tags), 45)
self.assertTrue(all(entry.document.get("kicker") == "Article" 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(ArticleEditorError):
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(ArticleEditorError, "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(ArticleEditorError, "changed on disk"):
self.repository.delete_entry(entry)
self.assertTrue(entry.path.is_file())
def test_unapproved_tag_is_rejected(self) -> None:
with self.assertRaisesRegex(ArticleEditorError, "unapproved tag"):
self.repository.create_entry("test-entry", self.values(tags="invented"))
def test_cover_image_is_copied_renamed_referenced_and_reopened(self) -> None:
entry = self.repository.create_entry("test-entry", self.values())
source = self.site_root / "source.png"
source.write_bytes(b"disposable png bytes")
saved, cover = self.repository.add_or_replace_cover(
entry, source, "Test cover", replace=False
)
self.assertEqual(cover.name, "cover-image.png")
self.assertEqual(cover.path.read_bytes(), source.read_bytes())
self.assertTrue(saved.document.get("body").startswith("![Test cover](cover-image.png)\n\n"))
reopened = self.repository.load_entry("test-entry")
self.assertEqual(self.repository.cover_image(reopened), cover)
def test_cover_replacement_requires_confirmation_and_removes_old_cover(self) -> None:
entry = self.repository.create_entry("test-entry", self.values())
first = self.site_root / "first.png"
second = self.site_root / "second.jpg"
first.write_bytes(b"first")
second.write_bytes(b"second")
saved, _cover = self.repository.add_or_replace_cover(
entry, first, "First", replace=False
)
with self.assertRaisesRegex(ArticleEditorError, "confirm replacement"):
self.repository.add_or_replace_cover(saved, second, "Second", replace=False)
replaced, cover = self.repository.add_or_replace_cover(
saved, second, "Second", replace=True
)
self.assertEqual(cover.name, "cover-image.jpg")
self.assertFalse((replaced.path.parent / "cover-image.png").exists())
self.assertTrue((replaced.path.parent / "cover-image.jpg").is_file())
self.assertTrue(replaced.document.get("body").startswith("![Second](cover-image.jpg)\n\n"))
def test_remove_cover_removes_reference_and_article_local_file(self) -> None:
entry = self.repository.create_entry("test-entry", self.values())
source = self.site_root / "source.png"
source.write_bytes(b"cover")
saved, cover = self.repository.add_or_replace_cover(
entry, source, "Test cover", replace=False
)
without_cover = self.repository.remove_cover(saved, remove_file=True)
self.assertIsNone(self.repository.cover_image(without_cover))
self.assertFalse(cover.path.exists())
self.assertEqual(without_cover.document.get("body"), self.values()["body"])
def test_slug_suggestion_matches_repository_style(self) -> None:
self.assertEqual(suggest_slug("Were Testing: A GUI!"), "were-testing-a-gui")
if __name__ == "__main__":
unittest.main()