feat: hardening grab bag — error sanitization, alembic helpers, test split#10
Merged
jeanpaulsio merged 5 commits intomainfrom Apr 13, 2026
Merged
feat: hardening grab bag — error sanitization, alembic helpers, test split#10jeanpaulsio merged 5 commits intomainfrom
jeanpaulsio merged 5 commits intomainfrom
Conversation
- 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%.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Two-commit PR of backend + frontend hardening for the starter.
Backend (commit 1)
sanitize_validation_errorsdrops Pydantic'sctxfield (regex patterns, expected values,ValueErrorinstances) and redactsinputwhenever the field path matchesSENSITIVE_FIELD_FRAGMENTS(password, token, secret, authorization, api_key).tests/integration/test_error_sanitization.pysends a real plaintext password and asserts it never reappears in the response body.app/db/migration_helpers.pyprovidesensure_enum_exists,drop_enum,add_enum_valueso re-runs, downgrades, and shared enums across tables are safe. The two existing migrations (0001,0002) now use them withcreate_type=Falseon the columns. Unit tests assert the SQL shape via a monkeypatchedop.execute.Frontend (commit 2)
tests/unit/is one test file per source file;tests/integration/holds multi-step flow tests that are each self-contained (vitest does not sharevi.mockacross files).tests/setup.tsstubsResizeObserver,scrollIntoView,HTMLDialogElement.showModal/close, andwindow.matchMediaglobally.lib/query-keys.ts—authKeys+itemKeysfactories so hierarchicalinvalidateQueriesworks.useCurrentUseruses the new shape; the oldCURRENT_USER_KEYexport stays with a@deprecatednote.lib/sentry.ts— dynamic init wrapper. StripsAuthorizationheaders from events + breadcrumbs, excludes token-bearing URLs viadenyUrls, and deliberately omitsreplayIntegration(it installs pointer-event listeners that swallow clicks on radix/shadcn). Wired from+Layout.tsx. No-op withoutVITE_SENTRY_DSN.lib/analytics.ts— typedtrack(event, props)wrapper over anEventMapso event names and payload shapes are compile-checked at every call site.pages/_error/+Page.tsx— 404 + generic error page.lib/sse-client.ts— unused placeholder.auth-flow.test.tsxwalks register → verify → login → dashboard with all network boundaries mocked at the service layer.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% coveragecd web && npm run lint && npm run typecheck && npm run test:coverage— 132 passed, 95.33% stmts / 90.16% branchespages/_error/+Page.tsxrenders the correct copy for 404s, generic errors, and abort reasons in dev