← writing

Tests, Rough Edges, and What's Still Ahead for RhizoBook

5 min read
testingjestvitesttesting-librarynextjsnestjsRhizoBook

RhizoBook isn’t finished. This post covers where the test suite is, what writing those tests surfaced, and what’s still ahead.

Backend: Jest + mockPrismaService

The backend has 14 spec files across the auth, users, providers, and appointments modules. No test touches a real database. mockPrismaService() from backend/src/test-utils/prisma.mock.ts returns a typed mock of every Prisma model method — findMany, findFirst, create, update, and so on — each stubbed with jest.fn().

const prisma = mockPrismaService();
prisma.appointment.findMany.mockResolvedValue([...]);

This matters for service tests in particular: you can assert the exact select shape that was passed to Prisma. If someone changes the provider query back to include and re-exposes sensitive fields, the test that checks for select: { id: true, name: true, ... } catches it before it ships. The test suite enforces the field-selection policy, not just behavior.

Controller tests verify the HTTP contract separately — correct status codes, correct response shapes, that the guard is applied. They don’t test business logic; they test that the controller delegates correctly to the service and returns what the service returns.

Frontend: Vitest + Testing Library

The frontend has 8 test files in frontend/__tests__/. Vitest replaces Jest here because the Next.js frontend toolchain is Vite-based and Vitest integrates more cleanly.

Tests use Testing Library — render the component, query by accessible role or text, assert on what the user would see. Not on state, not on component internals.

The mocking setup for Next.js components has a few recurring patterns worth documenting:

Bug found writing tests: useSearchParams without Suspense

While writing tests for the provider search page, router.push() threw with “Failed to fetch” in certain test scenarios. The root cause turned out to be useSearchParams() being called outside a <Suspense> boundary.

In Next.js App Router, components that call useSearchParams() must be wrapped in <Suspense>. Without it, Next.js can’t statically render the outer shell of the page and falls back to client-side-only rendering in a way that breaks router.push() in some environments.

Fix: extract the component that calls useSearchParams() into a child component, and wrap it in <Suspense> at the parent:

<Suspense fallback={<div>Loading...</div>}>
  <ProviderSearchInner />
</Suspense>

See frontend/app/(app)/providers/page.tsx. This fix also resolved the “Failed to fetch” error that appeared in production navigation.

Hydration warnings from password managers

Auth forms (/login, /register) showed React hydration warnings in development. The cause: browser password managers inject autocomplete attributes and sometimes extra DOM nodes into <input> and <button> elements. React sees a mismatch between the server-rendered HTML and the client HTML after the password manager mutates the DOM.

The fix is suppressHydrationWarning on the affected elements:

<Input suppressHydrationWarning ... />
<Button suppressHydrationWarning type="submit" ... />

This tells React not to warn about attribute mismatches on these specific elements. It doesn’t suppress all warnings — just the ones on the elements where external mutation is expected. The warning is noise in this case; the hydration mismatch is benign.

Deployment

The rewrite proxy from ADR 002 means the frontend on Vercel never needs to know the Railway backend URL at build time — it’s a runtime environment variable on the Vercel server.

What’s still ahead

RhizoBook is a working app, not a finished one. These are the gaps that matter:

Location-based provider search. The biggest UX gap. Patients can only filter by specialty. Adding location requires a location column on ProviderProfile (plus a migration), a geocoding integration or at minimum a city/state field, and UI work on the search bar. The groundwork is in the roadmap but not started.

Email notifications and reminders. Booking confirmation, appointment reminders, cancellation notices. There’s no email integration yet. This is table stakes for a real scheduling app — patients shouldn’t have to check the dashboard to know their appointment was confirmed.

Calendar view for appointments. components/ui/calendar.tsx exists — it’s the shadcn UI Calendar primitive (a styled react-day-picker wrapper). It’s not wired to any appointment feature. Showing a patient’s appointments on a calendar, or letting providers see their week at a glance, requires connecting that component to the appointments API and making scheduling decisions based on a date selection.

Recurring availability schedules. Currently each availability slot is created individually. Providers can’t say “every Monday 9–5” — they’d have to create 52 slots. A recurrence model would fix this.

Timezone handling. All times are UTC end-to-end. A provider in Chicago and a patient in New York would see the same raw UTC times displayed differently depending on their browser locale, with no guarantee those match. Correct timezone handling requires storing a timezone preference and converting on display.

HIPAA compliance features. medicalNotes on PatientProfile is PHI. Right now it’s protected by database-level encryption at rest (Neon) and application-level auth, but there’s no audit trail for who accessed it, no field-level encryption, and no BAA infrastructure. These are hard requirements before this app could handle real patient data.

Key files:

// comments via github discussions