Files
chicken_babies_site/tests/test_public_routes.py
Phillip Tarrant 0306f71763 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.
2026-04-21 15:40:35 -05:00

123 lines
4.1 KiB
Python

"""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 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"``
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
import pytest
from fastapi.testclient import TestClient
from app.main import app
@pytest.fixture(scope="module")
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,
including migrations + seed.
"""
return TestClient(app)
@pytest.mark.parametrize(
"path,expected_substring",
[
("/", "Chicken Babies"),
# 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"),
],
)
def test_public_route_renders_html(
client: TestClient,
path: str,
expected_substring: str,
) -> None:
"""Every public page returns 200 HTML containing a page-specific string.
The substring is intentionally a headline the template owns so the
test fails loudly if the wrong template is accidentally wired up.
"""
response = client.get(path)
assert response.status_code == 200, (
f"{path} returned {response.status_code}: {response.text[:200]}"
)
content_type = response.headers.get("content-type", "")
assert content_type.startswith("text/html"), (
f"{path} returned unexpected content-type: {content_type!r}"
)
assert expected_substring in response.text, (
f"{path} body missing expected substring {expected_substring!r}"
)
def test_home_shows_welcome_post(client: TestClient) -> None:
"""The Phase 2 seed inserts a welcome post; its title appears on /.
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 "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:
"""Shared layout references the generated logo asset paths.
We check for the stem (``/static/img/logo.``) rather than a specific
extension so both the <source srcset="...webp"> and <img src="...png">
markup are covered by a single assertion.
"""
response = client.get("/")
assert response.status_code == 200
assert "/static/img/logo." in response.text
def test_nav_marks_active_page(client: TestClient) -> None:
"""The About page renders ``aria-current=\"page\"`` on its nav link.
Exercises the shared layout's active-nav logic end-to-end without
inspecting internals.
"""
response = client.get("/about")
assert response.status_code == 200
# The pair should appear exactly once per active link; we only need
# to prove it's present at all.
assert 'aria-current="page"' in response.text