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>
69 lines
2.5 KiB
Python
69 lines
2.5 KiB
Python
"""Tests for :mod:`app.services.slugs`."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import pytest
|
|
|
|
from app.services.slugs import ensure_unique, slugify
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# slugify
|
|
# ---------------------------------------------------------------------------
|
|
@pytest.mark.parametrize(
|
|
"title, expected",
|
|
[
|
|
("Hello World", "hello-world"),
|
|
("Hello, World!", "hello-world"),
|
|
(" leading and trailing ", "leading-and-trailing"),
|
|
("CamelCaseTitle", "camelcasetitle"),
|
|
("Multiple spaces", "multiple-spaces"),
|
|
("dashes---everywhere", "dashes-everywhere"),
|
|
("mix-of 99 items & stuff", "mix-of-99-items-stuff"),
|
|
("Café au lait", "caf-au-lait"), # non-ASCII dropped
|
|
("", "post"), # fallback
|
|
("...", "post"), # all-punctuation → fallback
|
|
("123", "123"), # digits allowed
|
|
("snake_case", "snake-case"),
|
|
],
|
|
)
|
|
def test_slugify_expected(title: str, expected: str) -> None:
|
|
""":func:`slugify` produces the documented output per input."""
|
|
assert slugify(title) == expected
|
|
|
|
|
|
def test_slugify_no_leading_or_trailing_hyphens() -> None:
|
|
"""Slugs never have dangling hyphens regardless of input shape."""
|
|
assert slugify("!!hello!!") == "hello"
|
|
assert slugify("--leading and trailing--") == "leading-and-trailing"
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# ensure_unique
|
|
# ---------------------------------------------------------------------------
|
|
def test_ensure_unique_returns_base_when_free() -> None:
|
|
"""If nothing collides, the base slug is returned unchanged."""
|
|
taken: set[str] = set()
|
|
result = ensure_unique("hello", lambda s: s in taken)
|
|
assert result == "hello"
|
|
|
|
|
|
def test_ensure_unique_suffixes_on_collision() -> None:
|
|
"""Collisions produce -2, -3, etc. in order."""
|
|
taken = {"hello", "hello-2", "hello-3"}
|
|
result = ensure_unique("hello", lambda s: s in taken)
|
|
assert result == "hello-4"
|
|
|
|
|
|
def test_ensure_unique_suffix_sequence_starts_at_two() -> None:
|
|
"""First collision uses ``-2`` — never ``-1`` or ``-0``."""
|
|
taken = {"hello"}
|
|
result = ensure_unique("hello", lambda s: s in taken)
|
|
assert result == "hello-2"
|
|
|
|
|
|
def test_ensure_unique_raises_when_exhausted() -> None:
|
|
"""A pathological ``exists`` callable hits the guard ceiling."""
|
|
with pytest.raises(RuntimeError):
|
|
ensure_unique("hello", lambda s: True, max_attempts=3)
|