250 lines
9.8 KiB
Python
Executable File
250 lines
9.8 KiB
Python
Executable File
import tempfile
|
|
import unittest
|
|
from datetime import datetime
|
|
from pathlib import Path
|
|
from unittest import mock
|
|
|
|
import authoring_server as server
|
|
|
|
|
|
class AuthoringServerTestCase(unittest.TestCase):
|
|
def setUp(self):
|
|
self.tmp = tempfile.TemporaryDirectory()
|
|
self.root = Path(self.tmp.name)
|
|
self.blogs = self.root / "blogs"
|
|
self.posts = self.root / "posts"
|
|
self.lima = self.root / "lima"
|
|
self.home = self.root / "home"
|
|
self.tags = self.root / "tags"
|
|
self.hzone = self.root / "assets" / "images" / "hzone"
|
|
self.blogs.mkdir()
|
|
self.posts.mkdir()
|
|
self.lima.mkdir()
|
|
self.home.mkdir()
|
|
self.tags.mkdir()
|
|
|
|
patches = {
|
|
"ROOT": self.root,
|
|
"BLOGS_DIR": self.blogs,
|
|
"POSTS_DIR": self.posts,
|
|
"LIMA_DIR": self.lima,
|
|
"IMAGE_ASSETS_DIR": self.root / "assets" / "images",
|
|
"HZONE_ASSETS_DIR": self.hzone,
|
|
}
|
|
self.patchers = [mock.patch.object(server, name, value) for name, value in patches.items()]
|
|
for patcher in self.patchers:
|
|
patcher.start()
|
|
|
|
def tearDown(self):
|
|
for patcher in reversed(self.patchers):
|
|
patcher.stop()
|
|
self.tmp.cleanup()
|
|
|
|
|
|
class UtilityTests(AuthoringServerTestCase):
|
|
def test_slugify_normalises_text_and_keeps_fallback(self):
|
|
self.assertEqual(server.slugify("Hello, Org Web!"), "hello-org-web")
|
|
self.assertEqual(server.slugify(" "), "untitled")
|
|
|
|
def test_normalise_tags_accepts_strings_and_deduplicates(self):
|
|
self.assertEqual(
|
|
server.normalise_tags("Life, review:Life Emacs"),
|
|
["life", "review", "emacs"],
|
|
)
|
|
|
|
def test_parse_org_datetime_handles_date_and_optional_time(self):
|
|
self.assertEqual(
|
|
server.parse_org_datetime("<2026-05-07 Thu 14:35>"),
|
|
datetime(2026, 5, 7, 14, 35),
|
|
)
|
|
self.assertEqual(
|
|
server.parse_org_datetime("<2026-05-07 Thu>"),
|
|
datetime(2026, 5, 7, 12, 0),
|
|
)
|
|
self.assertIsNone(server.parse_org_datetime("2026-05-07"))
|
|
|
|
def test_safe_relative_path_allows_expected_content_roots(self):
|
|
self.assertEqual(
|
|
server.safe_relative_path("blogs/example.org"),
|
|
self.blogs / "example.org",
|
|
)
|
|
self.assertEqual(
|
|
server.safe_relative_path("posts/career/example.org"),
|
|
self.posts / "career" / "example.org",
|
|
)
|
|
self.assertEqual(
|
|
server.safe_relative_path("lima/index.md"),
|
|
self.lima / "index.md",
|
|
)
|
|
self.assertEqual(
|
|
server.safe_relative_path("home/notes.org"),
|
|
self.home / "notes.org",
|
|
)
|
|
|
|
def test_safe_relative_path_rejects_escapes_and_wrong_locations(self):
|
|
for path in ("../secret.org", "/tmp/secret.org", "sitemap.org", "assets/style.org", "tags/life.org"):
|
|
with self.subTest(path=path):
|
|
with self.assertRaises(ValueError):
|
|
server.safe_relative_path(path)
|
|
|
|
def test_image_dimensions_detects_png_and_gif(self):
|
|
png = b"\x89PNG\r\n\x1a\n" + b"\x00" * 8 + (640).to_bytes(4, "big") + (480).to_bytes(4, "big")
|
|
gif = b"GIF89a" + (320).to_bytes(2, "little") + (200).to_bytes(2, "little")
|
|
self.assertEqual(server.image_dimensions(png, ".png"), (640, 480))
|
|
self.assertEqual(server.image_dimensions(gif, ".gif"), (320, 200))
|
|
self.assertIsNone(server.image_dimensions(b"not an image", ".png"))
|
|
|
|
|
|
class PageRenderingTests(AuthoringServerTestCase):
|
|
def test_render_org_writes_metadata_and_body(self):
|
|
rendered = server.render_org(
|
|
{
|
|
"title": "A New Note",
|
|
"slug": "Custom Slug",
|
|
"tags": ["Life", "life", "Review"],
|
|
"content": "Body text",
|
|
"date": "<2026-05-07 Thu 10:30>",
|
|
"comments": False,
|
|
"wip": "draft",
|
|
},
|
|
previous=None,
|
|
)
|
|
|
|
self.assertIn("#+TITLE: A New Note", rendered)
|
|
self.assertIn("#+DATE: <2026-05-07 Thu 10:30>", rendered)
|
|
self.assertIn("#+filetags: :life:review:", rendered)
|
|
self.assertIn("#+COMMENTS: ", rendered)
|
|
self.assertIn("#+SLUG: custom-slug", rendered)
|
|
self.assertIn("#+WIP: draft", rendered)
|
|
self.assertTrue(rendered.endswith("Body text\n"))
|
|
|
|
def test_render_markdown_adds_or_replaces_title_heading(self):
|
|
self.assertEqual(
|
|
server.render_markdown({"title": "Family Update", "content": "Body"}),
|
|
"# Family Update\n\nBody\n",
|
|
)
|
|
self.assertEqual(
|
|
server.render_markdown({"title": "New Title", "content": "## Old\n\nBody"}),
|
|
"# New Title\n\nBody\n",
|
|
)
|
|
|
|
def test_save_page_creates_blog_and_round_trips_content(self):
|
|
saved = server.save_page(
|
|
{
|
|
"pageType": "blog",
|
|
"title": "Test Post",
|
|
"slug": "test-post",
|
|
"date": "<2026-05-07 Thu 09:00>",
|
|
"tags": "test, blog",
|
|
"content": "The body",
|
|
"comments": True,
|
|
}
|
|
)
|
|
|
|
self.assertEqual(saved["path"], "blogs/2026/05-may/test-post.org")
|
|
self.assertEqual(saved["title"], "Test Post")
|
|
self.assertEqual(saved["tags"], ["test", "blog"])
|
|
self.assertEqual(saved["content"], "The body")
|
|
|
|
def test_save_page_creates_lima_markdown(self):
|
|
saved = server.save_page(
|
|
{
|
|
"pageType": "lima",
|
|
"title": "Lima Entry",
|
|
"slug": "lima-entry",
|
|
"content": "Some markdown",
|
|
}
|
|
)
|
|
|
|
path = self.lima / "lima-entry.md"
|
|
self.assertEqual(saved["path"], "lima/lima-entry.md")
|
|
self.assertEqual(path.read_text(encoding="utf-8"), "# Lima Entry\n\nSome markdown\n")
|
|
|
|
def test_list_pages_excludes_generated_and_sync_conflict_files(self):
|
|
(self.blogs / "keep.org").write_text("#+TITLE: Keep\n#+DATE: <2026-05-07 Thu 12:00>\n", encoding="utf-8")
|
|
(self.posts / "posts-list.org").write_text("#+TITLE: Generated\n", encoding="utf-8")
|
|
(self.blogs / "note.sync-conflict-1.org").write_text("#+TITLE: Conflict\n", encoding="utf-8")
|
|
(self.lima / "index.md").write_text("# Lima Home\n", encoding="utf-8")
|
|
(self.home / "notes.org").write_text("#+TITLE: Notes\n", encoding="utf-8")
|
|
(self.tags / "life.org").write_text("#+TITLE: Tag\n", encoding="utf-8")
|
|
|
|
paths = [page["path"] for page in server.list_pages()]
|
|
|
|
self.assertEqual(set(paths), {"blogs/keep.org", "home/notes.org", "lima/index.md"})
|
|
|
|
def test_relative_asset_path_is_calculated_from_lima_page_directory(self):
|
|
self.assertEqual(
|
|
server.relative_asset_path("lima/family/update.md", "assets/images/hzone/pic.png"),
|
|
"../../assets/images/hzone/pic.png",
|
|
)
|
|
self.assertEqual(
|
|
server.relative_asset_path("blogs/example.org", "assets/images/hzone/pic.png"),
|
|
"../assets/images/hzone/pic.png",
|
|
)
|
|
|
|
def test_save_upload_stores_post_images_by_section_and_returns_org_link(self):
|
|
with mock.patch.object(server, "datetime") as datetime_mock:
|
|
datetime_mock.now.return_value = datetime(2026, 5, 7, 9, 30)
|
|
saved = server.save_upload(
|
|
"Stress In Workplace.png",
|
|
b"image bytes",
|
|
"posts/career/management-of-self.org",
|
|
)
|
|
|
|
target = self.root / "assets" / "images" / "career" / "2026-05-07-stress-in-workplace.png"
|
|
self.assertEqual(target.read_bytes(), b"image bytes")
|
|
self.assertEqual(saved["path"], "assets/images/career/2026-05-07-stress-in-workplace.png")
|
|
self.assertEqual(saved["relativeUrl"], "../../assets/images/career/2026-05-07-stress-in-workplace.png")
|
|
self.assertEqual(saved["insertText"], "[[../../assets/images/career/2026-05-07-stress-in-workplace.png]]")
|
|
|
|
def test_save_upload_keeps_blog_images_in_blog_folder(self):
|
|
with mock.patch.object(server, "datetime") as datetime_mock:
|
|
datetime_mock.now.return_value = datetime(2026, 5, 7, 9, 30)
|
|
saved = server.save_upload(
|
|
"Lunch.jpg",
|
|
b"image bytes",
|
|
"blogs/2026/05-may/lunch.org",
|
|
)
|
|
|
|
self.assertEqual(saved["path"], "assets/images/blogs/2026-05-07-lunch.jpg")
|
|
self.assertEqual(saved["insertText"], "[[../../../assets/images/blogs/2026-05-07-lunch.jpg]]")
|
|
|
|
def test_save_upload_rejects_disallowed_extensions(self):
|
|
with self.assertRaises(ValueError):
|
|
server.save_upload("shell.php", b"<?php")
|
|
|
|
with self.assertRaises(ValueError):
|
|
server.save_upload("movie.mp4", b"video")
|
|
|
|
|
|
class BuildQueueTests(unittest.TestCase):
|
|
def test_snapshot_reports_recent_completed_job(self):
|
|
queue = server.BuildQueue()
|
|
job = server.BuildJob(1, "blogs/post.org", "Post")
|
|
job.started_at = 1.0
|
|
job.finished_at = 2.0
|
|
job.ok = True
|
|
job.message = "Done"
|
|
job.log = "build log"
|
|
queue._recent.append(job)
|
|
|
|
snapshot = queue.snapshot()
|
|
|
|
self.assertFalse(snapshot["running"])
|
|
self.assertEqual(snapshot["message"], "Done")
|
|
self.assertEqual(snapshot["recent"][0]["status"], "done")
|
|
self.assertEqual(snapshot["log"], "build log")
|
|
|
|
def test_queue_build_enqueues_from_page_data(self):
|
|
queue = server.BuildQueue()
|
|
with mock.patch.object(server, "BUILD_QUEUE", queue), mock.patch.object(queue, "_run_worker"):
|
|
queued = server.queue_build({"path": "blogs/post.org", "title": "Post"})
|
|
|
|
self.assertEqual(queued["id"], 1)
|
|
self.assertEqual(queued["status"], "queued")
|
|
self.assertEqual(queued["path"], "blogs/post.org")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|