feat: phase 2 content model + cache — SQLite schema, markdown, TTL
Stand up the full SQLite content layer: all 7 tables from the authoritative schema with WAL + foreign-keys enforced per-connection, entity dataclasses plus row mappers, hand-rolled versioned migrations tracked in schema_migrations, and an idempotent Python seed (system user + welcome post + About page). Add a Markdown->HTML service using markdown-it-py with a strict bleach allowlist (tables intentionally omitted on both sides). Add a typed in-process TTLCache[K,V] and wire it into real DB-backed PostService and PageService, both exposing invalidate_all() for Phase 4 admin writes. Rewire / and /about to read from the DB; homepage renders the seeded welcome post, About renders page.title + sanitized body_html_cached. Update the Phase 1 route tests accordingly. Mark Phase 2 complete in docs/ROADMAP.md.
This commit is contained in:
53
tests/test_page_service.py
Normal file
53
tests/test_page_service.py
Normal file
@@ -0,0 +1,53 @@
|
||||
"""Tests for :class:`app.services.pages.PageService`.
|
||||
|
||||
Uses the session-scoped seeded ``db_engine`` fixture.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from sqlalchemy import Engine
|
||||
|
||||
from app.models.entities import Page
|
||||
from app.services.pages import PageService
|
||||
|
||||
|
||||
def test_get_by_slug_returns_seeded_about_page(db_engine: Engine) -> None:
|
||||
"""The seeded About page loads as a :class:`Page` dataclass."""
|
||||
service = PageService(db_engine)
|
||||
page = service.get_by_slug("about")
|
||||
|
||||
assert page is not None
|
||||
assert isinstance(page, Page)
|
||||
assert page.slug == "about"
|
||||
assert page.title == "About the Farm"
|
||||
# The sanitized HTML must contain a <p> since the seed Markdown
|
||||
# has multiple paragraphs; this also proves the Markdown pipeline
|
||||
# ran at seed time.
|
||||
assert "<p>" in page.body_html_cached
|
||||
assert page.published is True
|
||||
|
||||
|
||||
def test_get_by_slug_returns_none_for_unknown_slug(db_engine: Engine) -> None:
|
||||
"""Unknown slugs return ``None`` rather than raising."""
|
||||
service = PageService(db_engine)
|
||||
assert service.get_by_slug("does-not-exist") is None
|
||||
|
||||
|
||||
def test_get_by_slug_is_cached(db_engine: Engine) -> None:
|
||||
"""The TTL cache wraps page lookups keyed by slug."""
|
||||
service = PageService(db_engine)
|
||||
first = service.get_by_slug("about")
|
||||
second = service.get_by_slug("about")
|
||||
assert first is second
|
||||
|
||||
|
||||
def test_invalidate_all_forces_reload(db_engine: Engine) -> None:
|
||||
"""After :meth:`invalidate_all` the next call re-hits the DB."""
|
||||
service = PageService(db_engine)
|
||||
first = service.get_by_slug("about")
|
||||
service.invalidate_all()
|
||||
second = service.get_by_slug("about")
|
||||
assert first is not second
|
||||
# Same slug, same row — content equal, identity different.
|
||||
assert first is not None and second is not None
|
||||
assert first.slug == second.slug == "about"
|
||||
Reference in New Issue
Block a user