removal of authoring service
All checks were successful
Build Org Website / build (push) Successful in 50s
All checks were successful
Build Org Website / build (push) Successful in 50s
This commit is contained in:
73
Makefile
73
Makefile
@@ -6,19 +6,9 @@ PIP := $(VENV)/bin/pip
|
|||||||
OUTPUT_DIR ?= $(if $(SITE_OUTPUT_DIR),$(SITE_OUTPUT_DIR),output)
|
OUTPUT_DIR ?= $(if $(SITE_OUTPUT_DIR),$(SITE_OUTPUT_DIR),output)
|
||||||
PERM_DIR := /home/zaine/master-folder/projects/scripts/bash-scripts
|
PERM_DIR := /home/zaine/master-folder/projects/scripts/bash-scripts
|
||||||
|
|
||||||
AUTHOR_PORT := 8765
|
AUTHOR_SERVICE_DIR := /home/zaine/master-folder/projects/authoring-service
|
||||||
AUTHOR_URL := http://127.0.0.1:$(AUTHOR_PORT)
|
|
||||||
AUTHOR_PAGES_CHECK := /tmp/authoring-server-pages.json
|
|
||||||
AUTHOR_SERVICE := org-web-authoring.service
|
|
||||||
AUTHOR_UNIT_SRC := systemd/user/$(AUTHOR_SERVICE)
|
|
||||||
SYSTEMD_USER_DIR := $(HOME)/.config/systemd/user
|
|
||||||
AUTHOR_UNIT_DST := $(SYSTEMD_USER_DIR)/$(AUTHOR_SERVICE)
|
|
||||||
USER_ID := $(shell id -u)
|
|
||||||
SYSTEMD_RUNTIME_DIR ?= /run/user/$(USER_ID)
|
|
||||||
SYSTEMCTL_USER := env XDG_RUNTIME_DIR=$(SYSTEMD_RUNTIME_DIR) systemctl --user
|
|
||||||
JOURNALCTL_USER := env XDG_RUNTIME_DIR=$(SYSTEMD_RUNTIME_DIR) journalctl --user
|
|
||||||
|
|
||||||
all: set-perms clean-output build search author-restart clean-venv
|
all: set-perms clean-output build search clean-venv
|
||||||
|
|
||||||
build:
|
build:
|
||||||
@echo "Building project..."
|
@echo "Building project..."
|
||||||
@@ -32,8 +22,8 @@ search: $(VENV)
|
|||||||
@echo "Generating search index"
|
@echo "Generating search index"
|
||||||
$(PY) search-index-json.py
|
$(PY) search-index-json.py
|
||||||
|
|
||||||
test: $(VENV)
|
test:
|
||||||
$(PY) -m unittest discover -s tests -p 'test_authoring_server.py' -v
|
$(MAKE) -C "$(AUTHOR_SERVICE_DIR)" test
|
||||||
|
|
||||||
$(VENV):
|
$(VENV):
|
||||||
@echo "Generating virtual environment"
|
@echo "Generating virtual environment"
|
||||||
@@ -54,63 +44,32 @@ norm:
|
|||||||
|
|
||||||
author: author-start
|
author: author-start
|
||||||
|
|
||||||
author-install: $(VENV)
|
author-install:
|
||||||
@echo "Installing $(AUTHOR_SERVICE) for the current user..."
|
$(MAKE) -C "$(AUTHOR_SERVICE_DIR)" author-install
|
||||||
@mkdir -p "$(SYSTEMD_USER_DIR)"
|
|
||||||
install -m 0644 "$(AUTHOR_UNIT_SRC)" "$(AUTHOR_UNIT_DST)"
|
|
||||||
$(SYSTEMCTL_USER) daemon-reload
|
|
||||||
$(SYSTEMCTL_USER) enable "$(AUTHOR_SERVICE)"
|
|
||||||
|
|
||||||
author-start: author-install author-stop-legacy
|
author-start:
|
||||||
@echo "Starting authoring UI with systemd..."
|
$(MAKE) -C "$(AUTHOR_SERVICE_DIR)" author-start
|
||||||
$(SYSTEMCTL_USER) start "$(AUTHOR_SERVICE)"
|
|
||||||
@$(MAKE) --no-print-directory author-health
|
|
||||||
|
|
||||||
author-stop:
|
author-stop:
|
||||||
@echo "Stopping authoring UI with systemd..."
|
$(MAKE) -C "$(AUTHOR_SERVICE_DIR)" author-stop
|
||||||
$(SYSTEMCTL_USER) stop "$(AUTHOR_SERVICE)" || true
|
|
||||||
@$(MAKE) --no-print-directory author-stop-legacy
|
|
||||||
|
|
||||||
author-stop-legacy:
|
author-stop-legacy:
|
||||||
@echo "Stopping any legacy process on authoring port $(AUTHOR_PORT)..."
|
$(MAKE) -C "$(AUTHOR_SERVICE_DIR)" author-stop-legacy
|
||||||
@fuser -k $(AUTHOR_PORT)/tcp >/dev/null 2>&1 || true
|
|
||||||
|
|
||||||
author-restart: author-install author-stop-legacy
|
author-restart:
|
||||||
@echo "Restarting authoring UI with systemd..."
|
$(MAKE) -C "$(AUTHOR_SERVICE_DIR)" author-restart
|
||||||
$(SYSTEMCTL_USER) restart "$(AUTHOR_SERVICE)"
|
|
||||||
@$(MAKE) --no-print-directory author-health
|
|
||||||
|
|
||||||
author-health:
|
author-health:
|
||||||
@echo "Waiting for authoring UI and page list to respond..."
|
$(MAKE) -C "$(AUTHOR_SERVICE_DIR)" author-health
|
||||||
@for i in 1 2 3 4 5 6 7 8 9 10; do \
|
|
||||||
if ! $(SYSTEMCTL_USER) is-active --quiet "$(AUTHOR_SERVICE)"; then \
|
|
||||||
echo "Authoring UI service is not active. Service status:"; \
|
|
||||||
$(SYSTEMCTL_USER) status --no-pager "$(AUTHOR_SERVICE)" || true; \
|
|
||||||
echo "Recent logs:"; \
|
|
||||||
$(JOURNALCTL_USER) -u "$(AUTHOR_SERVICE)" -n 80 --no-pager || true; \
|
|
||||||
exit 1; \
|
|
||||||
fi; \
|
|
||||||
if curl -fsS "$(AUTHOR_URL)/api/pages" -o "$(AUTHOR_PAGES_CHECK)" && $(PY) -c 'import json,sys; data=json.load(sys.stdin); sys.exit(0 if isinstance(data, list) and data else 1)' < "$(AUTHOR_PAGES_CHECK)"; then \
|
|
||||||
echo "Authoring UI is running on $(AUTHOR_URL)/"; \
|
|
||||||
exit 0; \
|
|
||||||
fi; \
|
|
||||||
sleep 1; \
|
|
||||||
done; \
|
|
||||||
echo "Authoring UI failed to respond. Service status:"; \
|
|
||||||
$(SYSTEMCTL_USER) status --no-pager "$(AUTHOR_SERVICE)" || true; \
|
|
||||||
echo "Recent logs:"; \
|
|
||||||
$(JOURNALCTL_USER) -u "$(AUTHOR_SERVICE)" -n 80 --no-pager || true; \
|
|
||||||
exit 1
|
|
||||||
|
|
||||||
author-status:
|
author-status:
|
||||||
@$(SYSTEMCTL_USER) status --no-pager "$(AUTHOR_SERVICE)" || true
|
$(MAKE) -C "$(AUTHOR_SERVICE_DIR)" author-status
|
||||||
@curl -fsS "$(AUTHOR_URL)/api/pages" -o "$(AUTHOR_PAGES_CHECK)" && $(PY) -c 'import json,sys; print(f"Pages: {len(json.load(sys.stdin))}")' < "$(AUTHOR_PAGES_CHECK)" || true
|
|
||||||
|
|
||||||
author-diagnostics:
|
author-diagnostics:
|
||||||
@curl -fsS "$(AUTHOR_URL)/api/diagnostics" || true
|
$(MAKE) -C "$(AUTHOR_SERVICE_DIR)" author-diagnostics
|
||||||
|
|
||||||
author-logs:
|
author-logs:
|
||||||
$(JOURNALCTL_USER) -u "$(AUTHOR_SERVICE)" -n 100 --no-pager
|
$(MAKE) -C "$(AUTHOR_SERVICE_DIR)" author-logs
|
||||||
|
|
||||||
help:
|
help:
|
||||||
@echo "Available targets:"
|
@echo "Available targets:"
|
||||||
|
|||||||
@@ -6,9 +6,11 @@ The site is written using Emacs and Org-mode, and exported using the `org-publis
|
|||||||
|
|
||||||
## Local authoring UI
|
## Local authoring UI
|
||||||
|
|
||||||
Run `make author` to install and start the user systemd service, then open `http://127.0.0.1:8765` to create or edit blog and post Org files from a browser. When deployed behind the reverse proxy, use `https://author.zainezq.com`.
|
The authoring UI now lives in `/home/zaine/master-folder/projects/authoring-service`.
|
||||||
|
|
||||||
The service unit is checked in at `systemd/user/org-web-authoring.service` and is installed to `~/.config/systemd/user/` by the Makefile. Useful service commands:
|
Run `make author` from either repository to install and start the user systemd service, then open `http://127.0.0.1:8765` to create or edit blog and post Org files from a browser. When deployed behind the reverse proxy, use `https://author.zainezq.com`.
|
||||||
|
|
||||||
|
Useful service commands:
|
||||||
|
|
||||||
- `make author-restart` restarts the service and waits for the page API to respond
|
- `make author-restart` restarts the service and waits for the page API to respond
|
||||||
- `make author-stop` stops the service
|
- `make author-stop` stops the service
|
||||||
@@ -24,4 +26,4 @@ The editor writes normal `.org` pages with the metadata used by the publishing p
|
|||||||
|
|
||||||
Lima pages are Markdown files under `lima/`. Selecting the Lima page type shows a Markdown toolbar for common formatting and an attachment button. Uploaded images and videos are stored under `assets/images/hzone/YYYY/MM/` and inserted into the Markdown page.
|
Lima pages are Markdown files under `lima/`. Selecting the Lima page type shows a Markdown toolbar for common formatting and an attachment button. Uploaded images and videos are stored under `assets/images/hzone/YYYY/MM/` and inserted into the Markdown page.
|
||||||
|
|
||||||
Saves enqueue builds. The editor remains usable while publishing runs, and the queue section shows the current build, pending builds, and recent build results.
|
Saves enqueue builds. The editor remains usable while publishing runs, and the queue section shows the current build, pending builds, and recent build results.
|
||||||
|
|||||||
5201
authoring_server.py
5201
authoring_server.py
File diff suppressed because it is too large
Load Diff
@@ -60,8 +60,9 @@ $Config = @{
|
|||||||
RetryCount = 3
|
RetryCount = 3
|
||||||
RetryDelaySeconds = 2
|
RetryDelaySeconds = 2
|
||||||
AuthoringTestsEnabled = $true
|
AuthoringTestsEnabled = $true
|
||||||
|
AuthoringServiceDirectory = '/home/zaine/master-folder/projects/authoring-service'
|
||||||
AuthoringTestsPattern = 'test_authoring_server.py'
|
AuthoringTestsPattern = 'test_authoring_server.py'
|
||||||
AuthoringTestsDirectory = Join-Path $PSScriptRoot 'tests'
|
AuthoringTestsDirectory = '/home/zaine/master-folder/projects/authoring-service/tests'
|
||||||
AuthoringTestTimeoutSec = 120
|
AuthoringTestTimeoutSec = 120
|
||||||
|
|
||||||
# Tune these patterns to your build tooling.
|
# Tune these patterns to your build tooling.
|
||||||
@@ -991,8 +992,8 @@ function Send-BuildStatusNotification {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function Get-PythonCommand {
|
function Get-PythonCommand {
|
||||||
$linuxVenvPython = Join-Path $PSScriptRoot '.venv/bin/python'
|
$linuxVenvPython = Join-Path $Config.AuthoringServiceDirectory '.venv/bin/python'
|
||||||
$windowsVenvPython = Join-Path $PSScriptRoot '.venv/Scripts/python.exe'
|
$windowsVenvPython = Join-Path $Config.AuthoringServiceDirectory '.venv/Scripts/python.exe'
|
||||||
|
|
||||||
if (Test-Path -Path $linuxVenvPython) {
|
if (Test-Path -Path $linuxVenvPython) {
|
||||||
return $linuxVenvPython
|
return $linuxVenvPython
|
||||||
@@ -1032,7 +1033,7 @@ function Invoke-AuthoringServerTests {
|
|||||||
foreach ($argument in $arguments) {
|
foreach ($argument in $arguments) {
|
||||||
$process.StartInfo.ArgumentList.Add([string]$argument)
|
$process.StartInfo.ArgumentList.Add([string]$argument)
|
||||||
}
|
}
|
||||||
$process.StartInfo.WorkingDirectory = $PSScriptRoot
|
$process.StartInfo.WorkingDirectory = $Config.AuthoringServiceDirectory
|
||||||
$process.StartInfo.UseShellExecute = $false
|
$process.StartInfo.UseShellExecute = $false
|
||||||
$process.StartInfo.RedirectStandardOutput = $true
|
$process.StartInfo.RedirectStandardOutput = $true
|
||||||
$process.StartInfo.RedirectStandardError = $true
|
$process.StartInfo.RedirectStandardError = $true
|
||||||
|
|||||||
@@ -1,17 +0,0 @@
|
|||||||
[Unit]
|
|
||||||
Description=Org web authoring UI
|
|
||||||
After=network.target
|
|
||||||
|
|
||||||
[Service]
|
|
||||||
Type=simple
|
|
||||||
WorkingDirectory=/home/zaine/master-folder/org_files/org_web
|
|
||||||
Environment=AUTHOR_ROOT=/home/zaine/master-folder/org_files/org_web
|
|
||||||
Environment=AUTHOR_PORT=8765
|
|
||||||
Environment=PYTHONUNBUFFERED=1
|
|
||||||
ExecStartPre=/home/zaine/master-folder/org_files/org_web/.venv/bin/python -c "import authoring_server as s; pages=s.list_pages(); print(f'Authoring root: {s.ROOT}'); print(f'Editable pages: {len(pages)}'); raise SystemExit(0 if pages else 1)"
|
|
||||||
ExecStart=/home/zaine/master-folder/org_files/org_web/.venv/bin/python -u /home/zaine/master-folder/org_files/org_web/authoring_server.py
|
|
||||||
Restart=on-failure
|
|
||||||
RestartSec=5
|
|
||||||
|
|
||||||
[Install]
|
|
||||||
WantedBy=default.target
|
|
||||||
@@ -1,355 +0,0 @@
|
|||||||
import os
|
|
||||||
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_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(server.Path, "cwd", return_value=self.root):
|
|
||||||
self.assertEqual(server.resolve_root(), self.root)
|
|
||||||
|
|
||||||
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"))
|
|
||||||
|
|
||||||
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"],
|
|
||||||
"",
|
|
||||||
)
|
|
||||||
|
|
||||||
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")
|
|
||||||
|
|
||||||
def test_run_build_commands_streams_output_to_callback(self):
|
|
||||||
root = Path(tempfile.mkdtemp())
|
|
||||||
try:
|
|
||||||
venv_python = root / ".venv" / "bin" / "python"
|
|
||||||
venv_python.parent.mkdir(parents=True)
|
|
||||||
venv_python.write_text("", encoding="utf-8")
|
|
||||||
seen = []
|
|
||||||
|
|
||||||
class FakeProcess:
|
|
||||||
def __init__(self, lines, return_code=0):
|
|
||||||
self.stdout = iter(lines)
|
|
||||||
self.return_code = return_code
|
|
||||||
|
|
||||||
def wait(self):
|
|
||||||
return self.return_code
|
|
||||||
|
|
||||||
processes = [
|
|
||||||
FakeProcess(["emacs line 1\n", "emacs line 2\n"]),
|
|
||||||
FakeProcess(["index line\n"]),
|
|
||||||
]
|
|
||||||
|
|
||||||
with mock.patch.object(server, "ROOT", root), mock.patch.object(server.subprocess, "Popen", side_effect=processes):
|
|
||||||
ok, message, log = server.run_build_commands(seen.append)
|
|
||||||
|
|
||||||
self.assertTrue(ok)
|
|
||||||
self.assertIn("Build complete", message)
|
|
||||||
self.assertIn("emacs line 1\n", seen)
|
|
||||||
self.assertIn("index line\n", seen)
|
|
||||||
self.assertEqual(log, "".join(seen))
|
|
||||||
finally:
|
|
||||||
for path in sorted(root.rglob("*"), reverse=True):
|
|
||||||
if path.is_file():
|
|
||||||
path.unlink()
|
|
||||||
else:
|
|
||||||
path.rmdir()
|
|
||||||
root.rmdir()
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
unittest.main()
|
|
||||||
Reference in New Issue
Block a user