Project

General

Profile

Feature #3273 » live-verify-3273.txt

Bot AI, 09/08/2026 10:55 AM

 
W30_live_verify — Forgot Password v4 (ticket #3273 / oms-forgt-passwd-v4)
Node: W30_live_verify (Web)
Date: 2026-09-08

SCOPE
Feature files under test (per sides.web.feature_files):
app/(auth)/forgot-password/page.tsx
app/(auth)/forgot-password/verify/page.tsx
app/(auth)/forgot-password/reset/page.tsx
components/auth/ForgotPasswordForm.tsx
components/auth/VerifyOtpForm.tsx
components/auth/ResetPasswordForm.tsx
components/auth/SignInForm.tsx
Server Actions under test (lib/actions/forgot-password.ts):
requestPasswordResetOtp, resendOtp, verifyResetOtp, resetPasswordViaOtp
ticket_spec.acceptance_criteria was an empty array; scope was taken from
requirement_text's numbered requirements + "Final Verification" checklist,
scoped strictly to the forgot-password flow (not a full app re-verification).

ENVIRONMENT SETUP (real Postgres, real Next.js dev server — no mocks)
1. Local Postgres 16 already running (oms_test / oms_test_pw @ localhost:5432,
per .env DATABASE_URL). `prisma migrate status` showed the DB's applied
migration history had DIVERGED from this checkout's prisma/migrations/:
- DB had `20260904000000_password_reset_flow` applied (created a
`PasswordResetFlow` table) which does not exist in this checkout's
migrations directory at all — leftover from an earlier iteration of
this ticket (branches feat/3254-... and feat/3269-... exist in git
reflog, i.e. this is v4 of the same ticket).
- This checkout's own `20260904090000_item_code` and
`20260904120000_password_reset_otp` (which creates the actual
`PasswordResetOtp` table the current code in lib/actions/forgot-password.ts
and prisma/schema.prisma reads/writes) had NOT been applied.
Ran `npx prisma migrate deploy` — applied both pending migrations cleanly,
no conflicts with the stray `PasswordResetFlow` table (untouched, unrelated).
Ran `npx prisma generate`.
2. Ran `SUPER_ADMIN_MOBILE="9840421770" npx tsx prisma/seed.ts` — .env had no
SUPER_ADMIN_MOBILE set, so the seeded superadmin user would otherwise carry
the intentionally-invalid "0000000" sentinel mobile (see .env.example /
prisma/seed.ts comments). Set a real 10-digit mobile so the forgot-password
flow has a real registered user to exercise end to end.
3. Started `npm run dev` (NODE_ENV=development implicit) on :3000. In this
mode lib/sms.ts's sendOtp() writes the plaintext OTP to server stdout
(`[OTP dev] <mobile>: <code>`) instead of calling a real SMS gateway — this
is documented in-code as a dev-only escape hatch. Captured real OTPs from
this stdout stream for each test run (never fabricated).
4. Drove the app with a real headless Chromium (Playwright, cached browser
binaries under ~/.cache/ms-playwright, driven via node scripts run from
/opt/drcarrot-deploy where the playwright package is available) against
http://localhost:3000 — real HTTP requests, real Server Actions, real
Postgres reads/writes. Full click-by-click transcript with timestamps:
docs/gate-evidence/live-verify-3273-transcript.txt

RESULTS — golden path & edge cases (all against real DB/server, see transcript)
PASS Sign In page has a working "Forgot Password?" link -> /forgot-password
PASS Golden path: mobile -> OTP (real, captured from stdout) -> verify ->
reset -> redirect to /login -> login with the NEW password succeeds
(lands on /orders, i.e. a real authenticated session)
PASS Old (pre-reset) password is rejected after a successful reset
PASS Unregistered mobile: stays on /forgot-password (no redirect to verify),
shows the generic "If a matching account is found..." anti-enumeration
message — matches lib/actions/forgot-password.ts's documented behavior
PASS Invalid OTP: real "Invalid code. Please check and try again." field
error, stays on /verify
PASS Expired OTP: forced expiry via direct DB UPDATE (expiresAt = now() -
1 min) on a real just-issued OTP row, then submitted the original code
-> real "This code has expired or has already been used." rejection
PASS Reuse of an already-consumed OTP: after a full first pass consumed the
OTP (usedAt set) and cleared fp_otp_token, re-visiting /verify directly
redirects to /forgot-password (cookie-guard in the page component)
PASS Resend Code: 30s countdown shown on load, button hidden until it
elapses (waited out the real 30s, not simulated); clicking Resend
issued a genuinely new OTP hash (verified: the pre-resend code is
rejected afterwards, the new one succeeds); PasswordResetOtp.attempts
incremented to 1 in the DB after one resend, confirming the
OTP_MAX_ATTEMPTS=5 rate-limit counter is live
PASS Password policy: a policy-violating password ("weak") leaves the
submit button disabled client-side
PASS Password mismatch: "Passwords do not match" inline error shown
PASS Password history (last-3): attempting to reuse the just-set password
is rejected with "You have used this password recently."
PASS A second, genuinely new, compliant password completes the reset
PASS Direct navigation to /forgot-password/verify or /forgot-password/reset
with no fp_otp_token/fp_reset_token cookie redirects back to
/forgot-password (guard in both page.tsx files)
PASS Refresh mid-flow on /verify (cookie still valid) does not break the
page; browser back/forward between step 1 and step 2 does not crash
and the verify page remains reachable while its cookie is alive
PASS Existing login/logout: a wrong-password login attempt for an unrelated
seeded user (staff_test) still correctly rejects — forgot-password
changes did not regress the existing session/auth path

CRITICAL FINDING — sendOtp() cannot deliver a real SMS outside local dev
lib/sms.ts's sendOtp() only writes the OTP to stdout when
`process.env.NODE_ENV === "development"`. In every other environment
(explicitly including production — scripts/build-prod.sh runs
`NODE_ENV=production npx next build`, and `next start`/`next build` both run
under NODE_ENV=production) it throws:
"SMS provider not configured. Set SMS_PROVIDER_API_KEY ... Mobile: 984***"
Verified directly (not just by reading the code):
$ NODE_ENV=production npx tsx -e "import('./lib/sms.ts').then(...)"
-> sendOtp threw as expected in production: SMS provider not configured...
requestPasswordResetOtp's catch block swallows this and returns the generic
"We could not send the verification code. Please try again." to the user.
Net effect: in any deployed (non-dev) environment, EVERY forgot-password
attempt for EVERY registered user fails at step 1 — no OTP is ever sent, no
user can ever complete this flow. The ticket's own requirement list states
"Implement both frontend and backend/API changes if the required
forgot-password/verification functionality does not already exist" and
"Verification code must be securely generated, stored, validated, and
expired on the backend" — generation/storage/validation/expiry are all
implemented and verified above, but delivery is not, and the code says so
explicitly (lib/sms.ts's own header comment flags the SMS provider as an
unresolved "clarifications_needed" item). This is not a pre-existing/
unrelated-flow issue — it is this ticket's own OTP-delivery mechanism, and
it is unconditionally broken for any real deployment.

SECONDARY FINDING — destination mobile number not shown on the Verify screen
Requirement (explicit, with the design's own example): "Show the destination
number the code was sent to (design shows 'Enter the 6-digit code sent to
+91 98404 21770')." Verified live: components/auth/VerifyOtpForm.tsx renders
"Enter the 6-digit code we sent to your registered mobile number." — no
digits of the actual number appear anywhere on the page (checked via a live
DOM text-content scan for the seeded number's last 4 digits: not found).
Functionally the flow still works (the OTP itself is correctly scoped to the
right user via the signed cookie), but the explicit, named requirement is
not met.

ENDPOINTS SUMMARY
declared: 4 Server Actions (requestPasswordResetOtp, resendOtp,
verifyResetOtp, resetPasswordViaOtp) in lib/actions/forgot-password.ts
tested: 4/4, each driven end-to-end at least once above with a real DB
permission_gated: 0 — all four are deliberately unauthenticated
(pre-login) actions by design; none carry or should carry a permission
check. permission_matrix is empty accordingly.

REPRODUCIBILITY
seed command: SUPER_ADMIN_MOBILE="9840421770" npx tsx prisma/seed.ts
(run after `npx prisma migrate deploy`)
seeded user used for all scenarios: username=superadmin, mobile=9840421770
(final password at end of this run: FinalPassw0rd!9 — the DB is left in
this end state; re-running the verification flow from scratch requires
re-seeding or resetting the password history/passwordHash for this user)
env: NODE_ENV=development, `npm run dev` on :3000, DATABASE_URL from this
checkout's own .env (postgresql://oms_test:oms_test_pw@localhost:5432/oms_test)
(10-10/10)