Head Hen CMS end-to-end: dashboard lists all posts (drafts + published), Markdown editor with live preview + drag-drop image upload, Pillow media pipeline re-encoding every upload to JPEG, post CRUD + publish toggle + hard delete, About page edit, and double-submit CSRF cookie enforced on every admin mutating endpoint (Phase 3's TODO markers resolved). Slug auto-generated on create and server-locked once a post has been published. Unpublish preserves `published_at` so re-publish keeps original date ordering. Every admin write invalidates the read-side Post/Page TTL caches and records an `auth_events` audit row. CSRF middleware is narrow by design — issues/refreshes the `cb_csrf` cookie only on `GET /admin*`, and mutating endpoints opt in via `require_csrf_form` or `require_csrf_header` Depends. Public routes, healthz, and pre-auth login stay untouched. 64 new tests cover slugs, CSRF, media, admin posts/pages services, and end-to-end CMS routes. Tests never mock the DB — real temp SQLite files per the CLAUDE.md mandate. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
98 lines
2.8 KiB
Python
98 lines
2.8 KiB
Python
"""Tests for :class:`app.services.admin_pages.AdminPagesService`."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from pathlib import Path
|
|
from typing import Iterator
|
|
|
|
import pytest
|
|
from sqlalchemy import Engine
|
|
|
|
from app.db import build_engine, run_migrations
|
|
from app.models.seed import run_seed
|
|
from app.services.admin_pages import AdminPagesService
|
|
from app.services.audit import AuditService
|
|
from app.services.markdown import MarkdownService
|
|
from app.services.pages import PageService
|
|
from app.services.posts import PostService
|
|
|
|
|
|
@pytest.fixture
|
|
def engine(tmp_path: Path) -> Iterator[Engine]:
|
|
"""Return an isolated migrated + seeded engine."""
|
|
db_path = tmp_path / "phase4_pages.db"
|
|
eng = build_engine(f"sqlite:///{db_path}")
|
|
run_migrations(eng)
|
|
run_seed(eng)
|
|
yield eng
|
|
eng.dispose()
|
|
|
|
|
|
@pytest.fixture
|
|
def service(engine: Engine) -> AdminPagesService:
|
|
"""Return a fully-wired :class:`AdminPagesService`."""
|
|
return AdminPagesService(
|
|
engine=engine,
|
|
markdown=MarkdownService(),
|
|
page_service=PageService(engine),
|
|
post_service=PostService(engine),
|
|
audit=AuditService(engine),
|
|
)
|
|
|
|
|
|
def test_get_about_returns_seeded_page(service: AdminPagesService) -> None:
|
|
"""The seeded About page loads."""
|
|
page = service.get_about()
|
|
assert page is not None
|
|
assert page.slug == "about"
|
|
assert page.title == "About the Farm"
|
|
|
|
|
|
def test_update_about_rewrites_html_cache(
|
|
service: AdminPagesService,
|
|
) -> None:
|
|
"""Update regenerates the sanitized HTML and bumps ``updated_at``."""
|
|
before = service.get_about()
|
|
assert before is not None
|
|
updated = service.update_about(
|
|
title="New About Title",
|
|
body_md="Some **bold** copy.",
|
|
actor_user_id=1,
|
|
)
|
|
assert updated is not None
|
|
assert updated.slug == "about" # slug immutable
|
|
assert updated.title == "New About Title"
|
|
assert "<strong>bold</strong>" in updated.body_html_cached
|
|
# updated_at should bump (or at least not go backwards).
|
|
assert updated.updated_at >= before.updated_at
|
|
|
|
|
|
def test_update_about_invalidates_public_page_cache(engine: Engine) -> None:
|
|
"""After an update the public :class:`PageService` cache returns fresh copy."""
|
|
md = MarkdownService()
|
|
pages = PageService(engine)
|
|
posts = PostService(engine)
|
|
audit = AuditService(engine)
|
|
service = AdminPagesService(
|
|
engine=engine,
|
|
markdown=md,
|
|
page_service=pages,
|
|
post_service=posts,
|
|
audit=audit,
|
|
)
|
|
|
|
primed = pages.get_by_slug("about")
|
|
# Cache hit confirmation:
|
|
assert pages.get_by_slug("about") is primed
|
|
|
|
service.update_about(
|
|
title="After",
|
|
body_md="updated copy",
|
|
actor_user_id=1,
|
|
)
|
|
|
|
after = pages.get_by_slug("about")
|
|
assert after is not None
|
|
assert after is not primed
|
|
assert after.title == "After"
|