Files
labyricorn-site/tests/test_article_editor.py
Labyricorn 6c762c3b33
Deploy production / deploy (push) Successful in 4s
Add tracked and freeform tag editing
2026-08-13 15:56:02 -07:00

202 lines
9.0 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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_tracked_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), 40)
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_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_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()