import os import sys import tempfile import unittest from datetime import datetime from pathlib import Path from unittest import mock sys.path.insert(0, str(Path(__file__).resolve().parents[1])) import authoring_service.build as build_server import authoring_service.config as config_server import authoring_service.content as server import authoring_service.utils as utils_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_resolve_root_ignores_invalid_author_root(self): wrong_root = self.root / "empty" wrong_root.mkdir() (self.root / "authoring_server.py").write_text("", encoding="utf-8") with mock.patch.dict(os.environ, {"AUTHOR_ROOT": str(wrong_root), "GITHUB_WORKSPACE": ""}), mock.patch.object(config_server.Path, "cwd", return_value=self.root): self.assertEqual(config_server.resolve_root(), self.root) def test_slugify_normalises_text_and_keeps_fallback(self): self.assertEqual(utils_server.slugify("Hello, Org Web!"), "hello-org-web") self.assertEqual(utils_server.slugify(" "), "untitled") def test_normalise_tags_accepts_strings_and_deduplicates(self): self.assertEqual( utils_server.normalise_tags("Life, review:Life Emacs"), ["life", "review", "emacs"], ) def test_parse_org_datetime_handles_date_and_optional_time(self): self.assertEqual( utils_server.parse_org_datetime("<2026-05-07 Thu 14:35>"), datetime(2026, 5, 7, 14, 35), ) self.assertEqual( utils_server.parse_org_datetime("<2026-05-07 Thu>"), datetime(2026, 5, 7, 12, 0), ) self.assertIsNone(utils_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")) def test_parse_upload_form_reads_attachment_and_page_path(self): boundary = "----authoring-test" content_type = f"multipart/form-data; boundary={boundary}" body = ( f"--{boundary}\r\n" 'Content-Disposition: form-data; name="pagePath"\r\n\r\n' "lima/index.md\r\n" f"--{boundary}\r\n" 'Content-Disposition: form-data; name="attachment"; filename="photo.png"\r\n' "Content-Type: image/png\r\n\r\n" ).encode("utf-8") + b"image bytes\r\n" + f"--{boundary}--\r\n".encode("utf-8") filename, payload, page_path = server.parse_upload_form(content_type, body) self.assertEqual(filename, "photo.png") self.assertEqual(payload, b"image bytes") self.assertEqual(page_path, "lima/index.md") 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_list_pages_skips_files_that_fail_to_read(self): good = self.blogs / "keep.org" bad = self.blogs / "bad.org" good.write_text("#+TITLE: Keep\n#+DATE: <2026-05-07 Thu 12:00>\n", encoding="utf-8") bad.write_text("#+TITLE: Bad\n", encoding="utf-8") original_read_page = server.read_page def read_page(path): if path == bad: raise OSError("file disappeared") return original_read_page(path) with mock.patch.object(server, "read_page", side_effect=read_page): paths = [page["path"] for page in server.list_pages()] self.assertEqual(paths, ["blogs/keep.org"]) def test_server_diagnostics_reports_root_and_page_count(self): (self.blogs / "keep.org").write_text("#+TITLE: Keep\n#+DATE: <2026-05-07 Thu 12:00>\n", encoding="utf-8") diagnostics = server.server_diagnostics() self.assertEqual(diagnostics["root"], self.root.as_posix()) self.assertEqual(diagnostics["pageCount"], 1) self.assertEqual(diagnostics["firstPage"], "blogs/keep.org") 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_inserts_absolute_public_url_for_markdown(self): with mock.patch.object(server, "datetime") as datetime_mock: datetime_mock.now.return_value = datetime(2026, 5, 9, 9, 30) saved = server.save_upload( "Screen Shot.png", b"image bytes", "lima/index.md", ) self.assertEqual(saved["path"], "assets/images/hzone/2026-05-09-screen-shot.png") self.assertEqual(saved["relativeUrl"], "../assets/images/hzone/2026-05-09-screen-shot.png") self.assertEqual( saved["insertText"], "![2026-05-09-screen-shot.png](https://zainezq.com/assets/images/hzone/2026-05-09-screen-shot.png)", ) def test_save_upload_rejects_disallowed_extensions(self): with self.assertRaises(ValueError): server.save_upload("shell.php", b"