Files
chicken_babies_site/docs/MANUAL_TESTING.md
Phillip Tarrant d9090f5055 feat: phase 5 contact form — hCaptcha, honeypot, rate limit, notify
Working /contact POST flow: honeypot → hCaptcha server-verify →
field validation → SlowAPI 3/hr IP rate limit → contact_submissions
row → best-effort Resend notification (Reply-To = submitter) →
generic success page. Spam paths don't persist and render the same
success page (anti-enumeration). Send failures don't break the
request path — the row is already durable.

New services: HCaptchaService (async httpx + dev fallback),
ContactService. EmailService gains send_contact_notification.
Production config validator now requires ADMIN_CONTACT_EMAIL,
HCAPTCHA_SECRET, HCAPTCHA_SITE_KEY. 23 new tests, all green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 06:47:06 -05:00

11 KiB
Raw Blame History

Manual Testing Checklist

Living document. Each phase appends its own section — do not delete older sections when the code behind them changes; mark items as superseded instead so the audit trail stays intact.

Run the site locally before walking through the list:

source venv/bin/activate
uvicorn app.main:app --reload

Then open http://127.0.0.1:8000/ in a real browser (not just curl).


Phase 1 — Public Site Skeleton

Home (/)

  • Page returns 200 and renders without console errors.
  • Header shows the Chicken Babies R Us logo at ~48px tall.
  • <img> alt attribute reads Chicken Babies R Us.
  • Nav items appear in order: Home · About · Contact · Shop.
  • "Home" is visibly the active nav link and carries aria-current="page".
  • "Shop" nav link is visually muted (lower contrast) but still clickable.
  • Page intro ("Welcome to Chicken Babies R Us") is present.
  • With no posts in the DB, the empty-state reads "No posts yet — check back soon!".
  • Footer shows "Chicken Babies R Us · Morrison, Tennessee".
  • No street address is visible anywhere on the page (CLAUDE.md constraint).

About (/about)

  • Page returns 200 and renders without console errors.
  • H1 reads "About the farm".
  • Copy mentions Morrison, Tennessee by name.
  • Copy name-checks Head Hen.
  • No street address appears anywhere.
  • Nav marks "About" as active (aria-current="page").

Contact (/contact)

  • Page returns 200 and renders without console errors.
  • H1 reads "Get in touch".
  • When ADMIN_CONTACT_EMAIL is set, a mailto: link renders above the form.
  • When ADMIN_CONTACT_EMAIL is unset, the muted placeholder sentence appears and no mailto: link renders.
  • The note "Secure contact form coming soon" is visible.
  • Form fields (name, email, message) are visually disabled and cannot be typed into.
  • "Send message" button is visually disabled.
  • Form has no method="POST" attribute (view source).
  • Nav marks "Contact" as active.

Shop (/shop)

  • Page returns 200 and renders without console errors.
  • H1 reads "Shop".
  • "Coming soon" card is visible with mention of eggs, chicks, and waterfowl.
  • Nav marks "Shop" as active.

Responsive

Use the browser devtools responsive toolbar.

  • 360 × 800 (mobile): nav collapses behind a hamburger toggle; toggle opens/closes on click; logo remains legible; no horizontal scroll.
  • 768 × 1024 (tablet): nav appears inline; layout uses full container width; no horizontal scroll.
  • 1280 × 800 (desktop): content capped at --max-width (68rem ≈ 1088px); generous whitespace either side.

Accessibility

  • Tab-key order from top of page: skip link → logo → nav links → main content.
  • Pressing Tab from a cold page load reveals the skip link in the top-left corner.
  • Activating the skip link jumps focus into <main>.
  • Logo has a non-empty alt attribute ("Chicken Babies R Us").
  • Navigating with a screen reader announces each landmark (header, nav, main, footer).
  • Spot-check color contrast of --c-ink (#2B3A42) on --c-cream (#FAF3E7) — should be comfortably above WCAG AA for body text.

Assets

  • /static/img/logo.png loads and is roughly 256px tall.
  • /static/img/logo.webp loads with content-type image/webp.
  • /static/img/favicon.ico is requested by the browser and returns 200.
  • /static/img/apple-touch-icon.png is 180×180 and has a cream (#FAF3E7) background.

Ops smoke

  • pytest -q passes locally.
  • python -c "from app.main import app" exits cleanly.
  • python scripts/generate_static_assets.py regenerates the four asset files without error.
  • docker compose config still parses cleanly.

Phase 4 — Admin CMS

Pre-requisites:

  • Logged in via the Phase 3 magic-link flow (dev-fallback URL in server logs).
  • Landed on the Phase 4 dashboard at /admin.

Dashboard (/admin)

  • Page returns 200.
  • Page title reads "Dashboard".
  • Signed-in email appears in the greeting.
  • Posts table lists the seeded "Welcome to the Farm" row with status Published.
  • Each row shows Edit / Publish-or-Unpublish / Delete buttons.
  • Delete click triggers a confirmation dialog.
  • "New post" button is visible and links to /admin/posts/new.
  • "Edit About" button links to /admin/pages/about/edit.
  • <meta name="csrf-token"> is present in the rendered HTML (view source).
  • The cb_csrf cookie is set with SameSite=Lax, HttpOnly=false (readable by JS).

Create a post (/admin/posts/new)

  • Form renders without errors.
  • Title + status + body fields visible; preview pane on the right.
  • Drop zone is visible below the textarea with prompt copy.
  • Typing in the textarea causes the preview to update within ~300ms.
  • Dragging a JPG / PNG / WebP image onto the drop zone uploads it; a Markdown image tag is inserted at the cursor.
  • Dropping a GIF, plain text file, or anything >8 MB triggers the .is-error state on the drop zone.
  • Submitting with a blank title re-renders with "Title is required." and preserves other fields.
  • Submitting a valid form 303-redirects to /admin?msg=created.

Edit a post (/admin/posts/{id}/edit)

  • Form is pre-populated with the post's current title + body.
  • Slug is rendered read-only below the title.
  • For a published post, the UI notes the slug is locked.
  • Saving updates the row; public / reflects the change immediately (no caching delay).

Publish / unpublish / delete

  • Publish button on a draft row flips the status to published and shows a "published" flash on the next dashboard load.
  • Unpublish button on a published row flips the status back to draft; published_at is preserved in the DB (check via sqlite3 data/app.db).
  • Delete button on any row removes it entirely after confirmation.

About page edit (/admin/pages/about/edit)

  • Form renders with the current About title and body.
  • There is no slug editor.
  • Saving updates the row and the public /about page on next load.

Media upload

  • data/media/<yyyy>/<mm>/ is created lazily the first time an image is saved.
  • Uploaded files are stored under a random filename ending in .jpg regardless of the source format.
  • Hitting the /media/<yyyy>/<mm>/<name>.jpg URL directly serves the image at HTTP 200.
  • An uploaded transparent PNG comes through as an RGB JPEG (transparent areas become white).
  • Uploading an animated GIF is rejected with a generic error.
  • Uploading a >8 MB file is rejected.
  • Uploading while the X-CSRF-Token header is missing returns 403.

CSRF

  • Admin POST routes (/admin/logout, /admin/posts, /admin/posts/*/delete, /admin/posts/*/publish, /admin/pages/about, /admin/media/upload, /admin/preview) all return 403 when the submitted token does not match the cookie.
  • A legitimate form submission succeeds because both the cookie and the form field were issued during the previous GET.

Public site regression

  • / shows only published posts (newly created drafts do NOT appear).
  • A newly-published post shows at the top of / within one request.
  • /about shows the most recently edited copy.
  • No admin-facing text (status, dashboard wording) leaks into the public HTML.

Phase 5 — Contact Form

Pre-requisites:

  • ADMIN_CONTACT_EMAIL set in .env (the destination inbox).
  • For the production-like happy path: RESEND_API_KEY + RESEND_FROM set; otherwise the send path logs contact_notification_dev_fallback and the admin inbox will not actually receive mail.
  • Optionally set HCAPTCHA_SITE_KEY + HCAPTCHA_SECRET to exercise the real widget; with both unset the dev fallback auto-passes and logs hcaptcha_dev_fallback.

GET /contact

  • Page returns 200 and renders without console errors.
  • H1 reads "Get in touch".
  • Name, email, and message fields render as editable inputs (no disabled attribute).
  • "Send message" button is enabled.
  • Form has method="POST" and action="/contact" (view source).
  • Honeypot <input name="website"> is present in the markup but wrapped in a .visually-hidden container marked aria-hidden="true" — it is invisible to sighted users.
  • When HCAPTCHA_SITE_KEY is set, the h-captcha div and the https://js.hcaptcha.com/1/api.js script appear. When unset, neither appears.
  • Nav marks "Contact" as active.

Happy path

  • Fill in Name, Email, Message (>= 10 chars) and submit.
  • Response is HTTP 200 and renders "Thanks for reaching out".
  • sqlite3 data/app.db "SELECT id, name, email, length(message), handled FROM contact_submissions" shows the new row with handled=0.
  • Server log contains a contact_submitted structured event with a message_preview at most 40 chars long (no full body).
  • With RESEND_API_KEY set: admin inbox receives the notification email. From: matches RESEND_FROM; Reply-To: matches the submitted email; subject is New contact submission from {name}.
  • Without RESEND_API_KEY: server log contains contact_notification_dev_fallback with the submitter's name, email, and message length.

Validation errors

  • Submitting with a blank name shows "Please enter your name." inline.
  • Submitting with not-an-email shows "Please enter a valid email address.".
  • Submitting with a 9-character message shows "Message must be at least 10 characters.".
  • Submitting with a > 4000-character message shows "Message must be 4000 characters or fewer.".
  • Submitting with a > 80-character name shows "Name must be 80 characters or fewer.".
  • The response status code on every validation failure is 400.
  • Prior valid values remain filled in so the user doesn't retype.

Spam paths

  • Filling the honeypot website field and submitting returns "Thanks for reaching out" (same as success) AND no row is persisted AND an audit row with event_type='contact_spam_rejected' and reason=honeypot exists.
  • With a real hCaptcha configured: submitting without solving the widget returns the same generic thank-you page. Audit row: contact_spam_rejected / reason=hcaptcha. No DB row.

Rate limit

  • Submit the form 4 times from the same browser session within an hour. The fourth submission returns HTTP 429 and renders the "Too many attempts" template. A rate_limited audit row is added with scope=ip and endpoint=/contact.

Email send failure

  • With a valid form but RESEND_API_KEY pointed at an invalid key: the user still sees "Thanks for reaching out", the DB row is created, and the server log contains contact_notification_failed (logged by EmailService). The user experience is indistinguishable from success.

Ops smoke

  • pytest -q tests/test_hcaptcha_service.py tests/test_contact_service.py tests/test_contact_routes.py passes.
  • python -c "from app.main import app; print(len(app.routes))" prints a count greater than the Phase 4 count (the new POST /contact adds one route).