Step-by-Step Guide — Follow each step exactly as written. All values are provided — do not improvise.

TC-AUTH-007: Purpose-Aware Magic Links (Detailed)

Module: Authentication & Identity — Magic Links (passwordless auth)

Admin Test User: admin@rcme.membervu.com / Admin123!

Priority: P1

URLs for this test:
Frontend: https://stg-rcme.membervu.com
Admin edit-member: https://stg-rcme.membervu.com/admin/members
MailHog (email viewer): https://stg-webmail.membervu.com
Do NOT deactivate a shared seed test user (e.g. testmember@rcme.membervu.com). Create a throwaway test member first, or use one already known to be disposable. Deactivating breaks that account for anyone else relying on it.

Step 1: Set up an INACTIVE test member

StepAction (EXACT clicks/typing)Expected Result (EXACT text/behavior)Test Value
1Go to https://stg-rcme.membervu.com/login; sign in as admin.Dashboard loads.admin@rcme.membervu.com / Admin123!
2Create (or pick) a throwaway ACTIVE test member with a real inbox you can check in MailHog. Note their email.Member exists, status ACTIVE.e.g. qa-auth007-@membervu.local-style throwaway address you control
3Open the member's edit page, click Deactivate (POST /api/membership/members/:id/deactivate).Member status becomes INACTIVE.-

Step 2: Admin activation/reissue link reactivates INACTIVE member (AU7-01)

StepActionExpected ResultTest Value
1Still on the same member's edit page (now showing a "Pending activation" banner), click "Reissue activation link" (data-testid="reissue-activation-btn"), confirm the send.Success toast. Request hits POST /api/membership/members/:id/activation/reissue. This mints a MagicLinkToken with purpose: "ACTIVATION" and invalidates any prior outstanding tokens for this user.-
2Open https://stg-webmail.membervu.com, find the activation email for this member, and copy/open its activation link.Email received, subject/body branded as an activation email, contains a URL of the form /<tenantSlug>/auth/magic/verify?token=…&redirect=/dashboard.-
3Open the activation link in a fresh/incognito browser window (logged out).The link resolves, the browser signs in as this member, and redirects to /dashboard.-
4Back in the admin portal, refresh the member's record.Member status is now ACTIVE (not INACTIVE). If the member previously held an EXPIRED_MEMBER or GUEST role, confirm it was removed / restored to MEMBER.-
Data assertion: the member row's status column must read ACTIVE after this step — not just "the link worked" or "I got logged in." Check the actual admin member list/detail status badge.

Step 3: Self-service LOGIN magic link blocked for INACTIVE member (AU7-02)

StepActionExpected ResultTest Value
1Create/pick a second throwaway ACTIVE test member and deactivate them the same way as Step 1 (Admin → Members → edit → Deactivate).Member 2 status INACTIVE.-
2Log out of the admin session. As an anonymous/logged-out visitor, go to https://stg-rcme.membervu.com/auth/magic (self-service magic-link request page) and request a magic link for Member 2's email.Generic success message shown ("If an account exists, a magic link has been sent…") — the request does NOT reveal account status (no enumeration). Backend: POST /api/auth/magic-link/request creates a token with purpose: "LOGIN".Member 2's email
3Open MailHog. Check whether an email actually arrived for Member 2.The current backend code (magicLinkHandlers.ts) checks INACTIVE/SUSPENDED status BEFORE sending and skips the send in that case — so no email should arrive for Member 2 even though the API returned generic success. Note the finding either way; this is testable.-
4If an email did arrive (e.g. from a token issued before deactivation, or if the pre-send check is bypassed some other way), open its link.Verify returns 403 ACCOUNT_DISABLED{ error: { code: "ACCOUNT_DISABLED", message: "Your account is not active" } }. The member is NOT logged in and status remains INACTIVE.-
Report both outcomes. The important assertion is that Member 2 is NEVER reactivated and NEVER logged in via this self-service path — whether that's enforced at "no email sent" or "link sent but verify blocks it" is a secondary detail worth noting precisely.

Step 4: Legacy null-purpose link behaves as LOGIN (AU7-03)

Needs DB access or a pre-#484 token. If you don't have DB access to insert a purpose: NULL row directly, this scenario can be verified by code-reading + reasoning instead: magicLinkHandlers.ts computes isActivation = magicToken.purpose === "ACTIVATION" — any other value including NULL/undefined evaluates to false, so it is treated identically to LOGIN. If you have DB access, do the live steps below.
StepActionExpected ResultTest Value
1Via DB access, find or insert a MagicLinkToken row for an INACTIVE member with purpose = NULL, a valid (unused, unexpired) token hash you can reconstruct, and use its plaintext to build a verify URL.Row exists with purpose IS NULL.purpose: NULL
2Visit the verify URL for that token.403 ACCOUNT_DISABLED — same rejection as AU7-02. The member is NOT reactivated, NOT logged in.-

Step 5: SUSPENDED member blocked regardless of purpose (AU7-04)

StepActionExpected ResultTest Value
1As admin, suspend a throwaway test member (not the ones from Steps 1–3).Member status SUSPENDED.-
2Issue an admin activation/reissue link for this member (same as Step 2.1) and attempt to verify it.403 ACCOUNT_DISABLED — even an ACTIVATION-purpose link cannot clear a SUSPENDED status. This check runs BEFORE the ACTIVATION-purpose branch in verifyMagicLinkHandler.-

Step 6: Used/expired token still rejected — regression check (AU7-05)

StepActionExpected ResultTest Value
1Take the activation link you already used successfully in Step 2.3. Open it again in a fresh browser.400 TOKEN_USED{ error: { code: "TOKEN_USED", message: "This link has already been used" } }.-

CLEANUP