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:
Admin edit-member:
MailHog (email viewer):
Frontend:
https://stg-rcme.membervu.comAdmin edit-member:
https://stg-rcme.membervu.com/admin/membersMailHog (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
| Step | Action (EXACT clicks/typing) | Expected Result (EXACT text/behavior) | Test Value |
|---|---|---|---|
| 1 | Go to https://stg-rcme.membervu.com/login; sign in as admin. | Dashboard loads. | admin@rcme.membervu.com / Admin123! |
| 2 | Create (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 |
| 3 | Open 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)
| Step | Action | Expected Result | Test Value |
|---|---|---|---|
| 1 | Still 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. | - |
| 2 | Open 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. | - |
| 3 | Open 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. | - |
| 4 | Back 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)
| Step | Action | Expected Result | Test Value |
|---|---|---|---|
| 1 | Create/pick a second throwaway ACTIVE test member and deactivate them the same way as Step 1 (Admin → Members → edit → Deactivate). | Member 2 status INACTIVE. | - |
| 2 | Log 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 |
| 3 | Open 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. | - |
| 4 | If 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.
| Step | Action | Expected Result | Test Value |
|---|---|---|---|
| 1 | Via 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 |
| 2 | Visit 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)
| Step | Action | Expected Result | Test Value |
|---|---|---|---|
| 1 | As admin, suspend a throwaway test member (not the ones from Steps 1–3). | Member status SUSPENDED. | - |
| 2 | Issue 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)
| Step | Action | Expected Result | Test Value |
|---|---|---|---|
| 1 | Take 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
- Delete or reset the throwaway test members created in Steps 1, 3, and 5 (or leave them as clearly-labeled QA fixtures if your tenant supports that).
- If you inserted a manual DB row in Step 4, delete it after the test.
- Confirm no shared seed account (
testmember@rcme.membervu.com, etc.) was deactivated or suspended during this test. - Log out of all sessions opened during this test.