Files
chicken_babies_site/tests/test_admin_pages_service.py
Phillip Tarrant 9a8506970c feat: phase 4 admin CMS — dashboard, editor, media, CSRF
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>
2026-04-21 20:42:01 -05:00

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"