Files
chicken_babies_site/tests/test_slugs.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

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)