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:
@@ -1,17 +1,18 @@
|
||||
"""Smoke tests for the public-site skeleton routes.
|
||||
"""Smoke tests for the public-site routes.
|
||||
|
||||
These tests focus on contract rather than styling:
|
||||
|
||||
- every public route returns 200 with an HTML content-type
|
||||
- each page contains a page-specific substring (proves the template
|
||||
actually rendered, not just that the route exists)
|
||||
- the homepage renders the empty-state copy when PostService returns []
|
||||
- the shared layout emits the logo image path so nav/logo aren't broken
|
||||
by a future refactor
|
||||
- the homepage renders the Phase 2 seeded welcome post title
|
||||
- the About page renders the Phase 2 seeded About markdown
|
||||
- the shared layout emits the logo image path
|
||||
- the About nav link carries ``aria-current="page"``
|
||||
|
||||
No mocks of the DB (there is no DB in Phase 1). The PostService stub
|
||||
already returns an empty list, which is exactly what we want to assert
|
||||
against.
|
||||
Phase 2 updates: the homepage no longer shows "No posts yet" because
|
||||
the seed inserts a welcome post, and the About page content now comes
|
||||
from the DB-backed ``pages`` row rather than the old static template.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -27,7 +28,8 @@ def client() -> TestClient:
|
||||
"""Return a module-scoped FastAPI TestClient.
|
||||
|
||||
TestClient uses the module-level `app` built by `create_app()` at
|
||||
import time, i.e. the exact same app uvicorn runs in production.
|
||||
import time — i.e. the exact same app uvicorn runs in production,
|
||||
including migrations + seed.
|
||||
"""
|
||||
return TestClient(app)
|
||||
|
||||
@@ -36,7 +38,9 @@ def client() -> TestClient:
|
||||
"path,expected_substring",
|
||||
[
|
||||
("/", "Chicken Babies"),
|
||||
("/about", "About the farm"),
|
||||
# Phase 2: the About page renders the seeded page title
|
||||
# "About the Farm" (h1 from the template + page.title).
|
||||
("/about", "About the Farm"),
|
||||
("/contact", "Get in touch"),
|
||||
("/shop", "Coming soon"),
|
||||
],
|
||||
@@ -67,15 +71,30 @@ def test_public_route_renders_html(
|
||||
)
|
||||
|
||||
|
||||
def test_home_shows_empty_state_when_no_posts(client: TestClient) -> None:
|
||||
"""With the Phase 1 stub service, the home page shows 'No posts yet'.
|
||||
def test_home_shows_welcome_post(client: TestClient) -> None:
|
||||
"""The Phase 2 seed inserts a welcome post; its title appears on /.
|
||||
|
||||
This is the canonical empty-state marker; Phase 2 seeds a welcome
|
||||
post so this test will need to be updated when the DB lands.
|
||||
Replaces the Phase 1 "No posts yet" assertion now that the DB
|
||||
has a real published row on first boot.
|
||||
"""
|
||||
response = client.get("/")
|
||||
assert response.status_code == 200
|
||||
assert "No posts yet" in response.text
|
||||
assert "Welcome to the Farm" in response.text
|
||||
|
||||
|
||||
def test_about_renders_seeded_markdown(client: TestClient) -> None:
|
||||
"""The About page body comes from the seeded ``pages`` row.
|
||||
|
||||
Picks a distinctive substring from the seeded Markdown so the
|
||||
assertion fails if the old static template ever comes back.
|
||||
"""
|
||||
response = client.get("/about")
|
||||
assert response.status_code == 200
|
||||
# Substring from the seeded About markdown paragraph 1.
|
||||
assert "small family farm" in response.text
|
||||
# Seeded copy explicitly does not expose a street address.
|
||||
# Spot-check: the word "Morrison" appears (town-level).
|
||||
assert "Morrison" in response.text
|
||||
|
||||
|
||||
def test_layout_includes_logo_image(client: TestClient) -> None:
|
||||
|
||||
Reference in New Issue
Block a user