API route contract checks
Sham Bus now includes a static integration test that prevents app-local API drift:
pnpm --dir tests/integration test route-contract.test.ts
The test scans customer, dashboard, and admin source files for local /api/... references outside src/app/api/**, then compares them with the real src/app/api/**/route.ts manifest for the same app.
Remediated routes also have a live smoke test:
pnpm --dir tests/integration test route-handler-completion.test.ts
That test runs against the local dashboard/admin services. It verifies public DB-backed handlers such as dashboard cities and waitlist can complete a real happy path, and verifies protected dashboard/admin handlers return an auth/permission response instead of 404.
Authenticated UI smoke tests now run through the same Playwright smoke command:
pnpm test:e2e
The smoke config opens public login/landing pages plus authenticated customer, dashboard, and admin routes using saved sessions from tests/e2e/fixtures/.auth. Each page fails on runtime exceptions, visible Next.js error overlays, same-origin /api/* responses with status >= 400, and non-ignored console.error output.
Ecosystem route-flow coverage is tracked separately from generic smoke pages:
SKIP_WEBSERVER=1 pnpm --dir tests/e2e exec playwright test --config=playwright.smoke.config.ts --project=route-inventory
SKIP_WEBSERVER=1 pnpm --dir tests/e2e exec playwright test --config=playwright.smoke.config.ts --project=customer-public-route-flow --project=customer-auth-route-flow
SKIP_WEBSERVER=1 pnpm --dir tests/e2e exec playwright test --config=playwright.smoke.config.ts --project=dashboard-public-route-flow --project=dashboard-route-flow
SKIP_WEBSERVER=1 pnpm --dir tests/e2e exec playwright test --config=playwright.smoke.config.ts --project=admin-public-route-flow --project=admin-route-flow
SKIP_WEBSERVER=1 pnpm --dir tests/e2e exec playwright test --config=playwright.smoke.config.ts --project=mobile-customer-route-flow --project=mobile-driver-route-flow
For a single all-route sweep across the ecosystem:
SKIP_WEBSERVER=1 pnpm --dir tests/e2e exec playwright test --config=playwright.smoke.config.ts --project=route-inventory --project=customer-public-route-flow --project=customer-auth-route-flow --project=dashboard-public-route-flow --project=dashboard-route-flow --project=admin-public-route-flow --project=admin-route-flow --project=mobile-customer-route-flow --project=mobile-driver-route-flow
The route inventory test requires every customer, dashboard, and admin page.tsx route to be listed in tests/e2e/route-flows/manifest.ts, including compatibility routes such as /customer-auth/login. Route-flow tests then navigate each public/authenticated route with live fixture IDs and fail on document 500s, same-origin API errors, page exceptions, visible error overlays, and unexpected console errors. Mobile route-flow projects exercise the Flutter web shells for every declared customer/driver mobile route. Fixture data must allocate real free seats and valid booking codes; hardcoded demo IDs or already-booked seats make the route sweep invalid.
Resource-level coverage is tracked in tests/RESOURCE_COVERAGE_MATRIX.md and enforced by:
pnpm --dir tests/integration test resource-coverage-matrix.test.ts
That matrix maps product resources to database tables, related apps, API routes, UI routes, backend tests, and UI/action E2E tests. A route-load test is not sufficient evidence for a resource with mutations.
Live schema/RPC contracts run with the integration suite:
pnpm --dir tests/integration test live-database-contract.test.ts
These checks select the exact columns required by dashboard/customer flows and call app-used RPCs such as get_company_feedback, get_company_stats, get_or_create_loyalty_account, and generate_referral_code. A missing column or PGRST202 function error is treated as a product/schema bug, not a test issue.
For local customer-authenticated E2E, the customer service must run with explicit development OTP enabled:
USE_DEV_OTP=true DEV_OTP_CODE=123456 pnpm dev:customer
The Docker dev customer service passes these values by default for local development. USE_DEV_OTP is ignored by the OTP service when NODE_ENV=production.
What It Catches
- UI or service code calling a missing route.
- Copied hooks/services that reference endpoints from another app.
- Stale endpoints left after route refactors.
- Dynamic URL patterns such as
/api/trips/${tripId}/seats/lockwhen the matching[id]route is missing.
Seat-lock cleanup
POST /api/trips/[id]/seats/lock is rate-limited because it acquires scarce
inventory. The normal acquisition budget is keyed by the signature-verified
Traveler subject for authenticated requests and by public IP for guests. A
separate, broader public-IP ceiling remains in front of authorization so token
rotation and invalid credentials cannot bypass abuse protection. This prevents
unrelated authenticated Travelers behind the same carrier-grade NAT from
consuming one another's normal seat-lock budget without weakening the public
boundary.
DELETE is deliberately not charged to either acquisition budget: it must
remain available to return inventory even after excessive lock attempts.
Release is still protected by CSRF, trip/demo access, UUID validation, and exact
(trip_id, session_id) ownership, and is idempotent when no lock exists.
Booking-creation throttling
Every Booking and Journey confirmation boundary uses the same two-stage guard. Before credential parsing, a broad public-IP ceiling limits invalid tokens, automated guest traffic, and account rotation. After identity resolution, the normal creation allowance is keyed by the signature-verified Supabase subject; only a true guest checkout uses the public IP as its normal key. This applies to one-way, round-trip, batch, Journey Booking, and Journey Order confirmation routes, so authenticated Travelers behind one carrier-grade NAT cannot consume one another's normal allowance.
The shared helper owns the key format and response headers. Routes must not
reintroduce direct rateLimiters.booking calls or derive an actor key from an
unverified request field.
Current Policy
- Do not delete a caller only to satisfy the contract test unless the product feature is intentionally removed.
- If a feature is still in scope, implement the missing route handler with the correct auth, permission, CSRF, and database behavior.
- Public landing endpoints may be unauthenticated, but admin/dashboard mutations must keep CSRF protection.
- Protected browser mutations must call the application
csrfFetchhelper (or an equivalent shared wrapper), never rawfetch. A cookie alone is not the double-submit proof: the helper mirrors the app-specific CSRF cookie into the request header expected by the server guard. - Passing the static contract test proves only that a route exists. It does not replace live handler, RLS, or provider tests.
- When adding a route to close contract drift, add or extend live route-handler coverage for at least one public happy path or one protected non-404 assertion.
- When a UI/E2E/integration test fails, investigate the product behavior first. Update the test only when the product requirement changed or the assertion was objectively testing the wrong contract.