Skip to content

feat: hardening grab bag — error sanitization, alembic helpers, test split#10

Merged
jeanpaulsio merged 5 commits intomainfrom
feat/hardening-grab-bag
Apr 13, 2026
Merged

feat: hardening grab bag — error sanitization, alembic helpers, test split#10
jeanpaulsio merged 5 commits intomainfrom
feat/hardening-grab-bag

Conversation

@jeanpaulsio
Copy link
Copy Markdown
Owner

Summary

Two-commit PR of backend + frontend hardening for the starter.

Backend (commit 1)

  • Validation error sanitization. sanitize_validation_errors drops Pydantic's ctx field (regex patterns, expected values, ValueError instances) and redacts input whenever the field path matches SENSITIVE_FIELD_FRAGMENTS (password, token, secret, authorization, api_key). tests/integration/test_error_sanitization.py sends a real plaintext password and asserts it never reappears in the response body.
  • Alembic enum helpers. app/db/migration_helpers.py provides ensure_enum_exists, drop_enum, add_enum_value so re-runs, downgrades, and shared enums across tables are safe. The two existing migrations (0001, 0002) now use them with create_type=False on the columns. Unit tests assert the SQL shape via a monkeypatched op.execute.

Frontend (commit 2)

  • Test layout split. tests/unit/ is one test file per source file; tests/integration/ holds multi-step flow tests that are each self-contained (vitest does not share vi.mock across files). tests/setup.ts stubs ResizeObserver, scrollIntoView, HTMLDialogElement.showModal/close, and window.matchMedia globally.
  • lib/query-keys.tsauthKeys + itemKeys factories so hierarchical invalidateQueries works. useCurrentUser uses the new shape; the old CURRENT_USER_KEY export stays with a @deprecated note.
  • lib/sentry.ts — dynamic init wrapper. Strips Authorization headers from events + breadcrumbs, excludes token-bearing URLs via denyUrls, and deliberately omits replayIntegration (it installs pointer-event listeners that swallow clicks on radix/shadcn). Wired from +Layout.tsx. No-op without VITE_SENTRY_DSN.
  • lib/analytics.ts — typed track(event, props) wrapper over an EventMap so event names and payload shapes are compile-checked at every call site.
  • pages/_error/+Page.tsx — 404 + generic error page.
  • Deleted lib/sse-client.ts — unused placeholder.
  • Coverage — 95% statements / 90% branches, thresholds at 80%.
  • Integration testauth-flow.test.tsx walks register → verify → login → dashboard with all network boundaries mocked at the service layer.
  • CLAUDE.md — sanitization contract, enum helper usage, Vike UI conventions (cursor-pointer, scroll architecture, enter-to-submit hints), and the unit/integration testing split.

Test plan

  • cd server && ruff check app/ tests/ && ruff format --check app/ tests/ && mypy app/ --ignore-missing-imports && pytest tests/ -v — 113 passed, 91.40% coverage
  • cd web && npm run lint && npm run typecheck && npm run test:coverage — 132 passed, 95.33% stmts / 90.16% branches
  • Review each migration diff by hand against a clean DB and against production head
  • Verify pages/_error/+Page.tsx renders the correct copy for 404s, generic errors, and abort reasons in dev

- Sanitize Pydantic validation errors before the wire: drop `ctx` entirely
  and redact `input` on sensitive field paths (password, token, secret,
  authorization, api_key). Guarded by an integration test that sends a
  real plaintext password and asserts it never reappears in the response.
- Add `app/db/migration_helpers.py` with idempotent enum helpers
  (`ensure_enum_exists`, `drop_enum`, `add_enum_value`). Refactor the two
  existing Alembic migrations to use them so re-runs, downgrades, and
  shared enums across tables don't explode.
- Unit tests for the helpers (SQL shape assertions via monkeypatched op).
- Split tests into tests/unit/ (one file per source) and tests/integration/
  (multi-step flow tests, each self-contained because vitest does not share
  vi.mock across files). Add setup.ts stubs for ResizeObserver, scrollIntoView,
  HTMLDialogElement.showModal/close, and window.matchMedia.
- Add lib/query-keys.ts with `authKeys` + `itemKeys` factories so hierarchical
  invalidateQueries works consistently. Rewire useCurrentUser to the new shape.
- Add lib/sentry.ts with a dynamic init wrapper that strips Authorization
  headers from events and breadcrumbs, excludes token-bearing URLs via
  denyUrls, and deliberately omits replayIntegration (which breaks radix
  pointer events). Wired from pages/+Layout.tsx.
- Add lib/analytics.ts: typed `track(event, props)` wrapper over an `EventMap`
  so event names and payload shapes are compile-checked at every call site.
- Add pages/_error/+Page.tsx for 404 + generic error states.
- Delete unused lib/sse-client.ts.
- Bring vitest coverage to 95% statements / 90% branches; thresholds at 80%.
- Integration test for the register -> verify -> login -> dashboard flow.
- CLAUDE.md: document sanitization contract, alembic enum helpers, Vike
  UI conventions (cursor-pointer, scroll architecture, enter hints), and
  the unit/integration testing split.
Each `<label>` on login/register/forgot-password/reset-password now has
an `htmlFor` pointing at a stable `id` on its sibling input. Clicking the
label now focuses the field, screen readers read the label when the input
is focused, and integration tests can use `getByLabelText` instead of
falling back to `document.querySelector('input[type="..."]')`.

Updated `tests/integration/auth-flow.test.tsx` to use `getByLabelText`
accordingly, using exact-match labels on the register form's two password
fields so "Password" and "Confirm Password" don't collide.
`_is_sensitive_loc` now walks the full Pydantic `loc` tuple instead of
only checking the leaf segment. A validation error at
`("body", "api_key", "format")` redacts the input even though the leaf
is the harmless string `format`, because anything nested under a
sensitive ancestor is assumed tainted.

List-index segments (ints emitted by FastAPI for list positions) are
cast to str and harmlessly never match.

Added `tests/unit/test_error_sanitization.py` with 10 cases covering
nested locs, list-index segments, case-insensitive matching, a guard
that every fragment in `SENSITIVE_FIELD_FRAGMENTS` is actually checked,
and the `ctx`-stripping branch on non-sensitive fields.
Previously only the console backend was covered. The template advertises
production email via Resend or SMTP, so leaving those backends untested
means a consumer who relies on them could ship a broken production path.

Mocks the resend SDK at sys.modules and patches aiosmtplib.send as an
AsyncMock so the tests are hermetic but still assert on the exact
parameter shapes that hit the wire. Also covers the get_email_backend
factory including the unknown-backend fallthrough and singleton caching.

email_service.py coverage 71% -> 100%.
@jeanpaulsio jeanpaulsio merged commit a649823 into main Apr 13, 2026
3 checks passed
@jeanpaulsio jeanpaulsio deleted the feat/hardening-grab-bag branch April 13, 2026 03:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant