Endpoints: GET /api/auth/me, POST /api/auth/refresh (implemented — see Test Plan §3/§4 and TC-AUTH-006 for the refresh-token mechanism itself).
🆕 Extended this release (2026-06-19 → 2026-07-01) with TC-SES-06 (deactivate → immediate 401, #446), TC-SES-07 (deep-link returnUrl, #346), TC-SES-08 (open-redirect rejection, #360), and TC-SES-09 (membership-application rate limit, #501). See the Test Plan banners.
Test Cases
TC-SES-01: Verify Active Session (/auth/me)
Step
Action
Expected Result
1
Login successfully (obtain token).
-
2
Send request to GET /auth/me with Authorization: Bearer .
-
3
Response.
Status 200 OK. Returns current user details (id, email, roles, tenantId).
TC-SES-02: Access Protected Route without Token
Step
Action
Expected Result
1
Clear local storage / cookie.
-
2
Send request to GET /auth/me (or other protected resource).
-
3
Response.
Status 401 Unauthorized.
TC-SES-03: Access with Malformed/Invalid Token
Step
Action
Expected Result
1
Send request with Authorization: Bearer invalid_string.
-
2
Response.
Status 401 Unauthorized (Token verification fails).
TC-SES-04: Logout
Step
Action
Expected Result
1
User clicks "Logout".
Frontend clears token from storage.
2
Verify Navigation.
Redirected to Public/Login page.
3
Verify Reuse.
Attempting to use the old token might still work if stateless JWT (unless blacklist implemented), but frontend should not have access to it. Note: If refresh tokens are used, logout should revoke the refresh token in DB.
TC-SES-05: Token Expiration Handling
Step
Action
Expected Result
1
Wait for token to expire (verify exp claim). ⚠️ Local dev overrides the access-token TTL to 60 min (docker-compose.yml); staging/prod use the documented 15 min. Prefer staging for this test.
-
2
Attempt API call after the access token has expired but with a valid, unexpired refresh cookie present (the normal case as of #359).
The client transparently calls POST /api/auth/refresh, receives a new access token, and the original request succeeds — no 401 surfaced to the user, no redirect to login. See TC-AUTH-006 for the full refresh-rotation test.
3
Repeat with the refresh cookie cleared/expired/revoked.
POST /api/auth/refresh returns 401. The session-expired modal (#223, SessionExpiredModal) appears — not a silent bounce. Re-authenticating restores the route you were on.
Purpose: Verify a deactivated user's still-unexpired access token is rejected on its next request, not just after the JWT's own exp.
Step
Action
Expected Result
1
Log in as an ACTIVE member in one browser session (e.g. a throwaway test member, NOT testmember@rcme.membervu.com — don't deactivate the shared seed account). Keep the access token.
Login succeeds; note the token / stay signed in.
2
In a separate admin session, deactivate that member: POST /api/membership/members/:id/deactivate (Admin → Members → edit → Deactivate), while status is currently ACTIVE.
200; member status becomes INACTIVE.
3
Immediately (well within the access token's remaining lifetime — do NOT wait for TTL expiry), make any authenticated request from the deactivated user's still-open session (e.g. refresh /dashboard or call GET /api/auth/me).
Response is 401 Unauthorized — the request is rejected on tokenVersion mismatch, not on JWT exp. The user is bounced to login.
TC-SES-07: Deep-Link returnUrl Preserved Through Login (#346)
Step
Action
Expected Result
1
While logged out, navigate directly to a protected deep link, e.g. https://stg-rcme.membervu.com/admin/members.
Redirected to /login?redirect=%2Fadmin%2Fmembers (URL-encoded original path appended as the redirect query param).
2
Log in as admin@rcme.membervu.com / Admin123!.
After successful login you land on the original deep-linked URL (/admin/members) — not the role-based default landing page (/admin/dashboard).
TC-SES-08: Open-Redirect Rejection (#360)
Step
Action
Expected Result
1
While logged out, navigate to https://stg-rcme.membervu.com/login?redirect=%2F%5Cevil.com (a backslash-based path, URL-decoded: /\evil.com).
Login page loads normally (no error, no crash).
2
Log in as testmember@rcme.membervu.com / Member123!.
You are NOT redirected to evil.com or any off-site host. validateRedirect() rejects any path containing \, ://, or a leading // and falls back to /home (or the role-based default) instead.
3
Repeat with ?redirect=https%3A%2F%2Fevil.com (absolute URL) and ?redirect=%2F%2Fevil.com (protocol-relative).
Both also fall back to a safe same-site path — none are followed off-site.
While logged out, submit a single membership application via the public apply flow (POST /api/membership/apply) with valid data.
200/201 — a normal single application still succeeds.
2
From the same client/IP, submit 5 more applications in rapid succession within the same 15-minute window (6th+ overall).
Requests beyond the 5th within the 15-min window return 429 TOO_MANY_REQUESTS with a RateLimit-Remaining response header trending to 0 on the earlier ones.
3
Reset (local only): docker restart membervu-backend clears the in-memory limiter.