Authentication testing guide

How to test magic links end to end

A complete magic-link test proves more than delivery. It verifies that the application generated a link for the intended recipient, that the URL points to an allowed destination, and that redeeming the secret creates exactly the session and redirect the product promises.

Define the contract before the test

Record the expected sender, subject family, callback origin, callback path, expiry, signup behavior, and post-login destination. A provider may call several passwordless mechanisms a magic link, but the application remains responsible for the exact URL and account policy.

Decide whether an unknown address may create an account. Better Auth's magic-link plugin, for example, permits signup by default unless disableSignUp is enabled. The test should encode the product decision rather than inherit a provider default accidentally.

Use a new recipient for every case

Create the InboxTap inbox inside the test and enter inbox.address into the real sign-in form or API request. The generated recipient lets concurrent tests share one capture server while every read remains filtered to its own SMTP envelope.

Avoid a suite-wide mailbox and avoid clearing all messages in beforeEach. Global cleanup creates races, while a unique address preserves the evidence needed to diagnose a failed redirect or duplicate delivery.

Extract and validate before navigation

waitForLink() searches captured text and HTML for HTTP or HTTPS links and can require a stable path fragment. Templates often contain support, privacy, logo, and unsubscribe URLs, so a contains filter prevents the test from following the first unrelated link.

Treat the returned value as a credential. Parse it with URL, compare the complete expected origin, and compare the callback path before navigation. Do not use a loose hostname suffix check, print the query string, or place the token in the test title.

magic-link.spec.ts
const rawUrl = await inbox.waitForLink({
  contains: "/auth/callback",
  timeoutMs: 20_000,
});

const url = new URL(rawUrl);
expect(url.origin).toBe(appOrigin);
expect(url.pathname).toBe("/auth/callback");

await page.goto(url.href);

Assert the session, not only the page

After opening the link, verify the final URL and a server-backed indication of the authenticated identity. A visible success message alone can pass while the session cookie, user identity, or authorization state is wrong.

Also check the intended redirect and whether the email address became verified when the authentication provider combines ownership proof with verification. Those outcomes belong to the application assertion, while InboxTap only proves what crossed SMTP.

Cover replay, expiry, and recipient failures

Redeem the same URL a second time and assert the configured single-use behavior. Exercise expiry with the provider's supported clock or short test configuration rather than an unbounded sleep. A malformed or modified token should fail without creating a session.

If the product binds a link to an email or pending transaction, prove that it cannot authenticate a different account. Do not attempt to test this by swapping InboxTap addresses alone; make the assertion against the application's session and stored identity.

  • A valid link establishes only the expected account and redirect.
  • A replayed, expired, or modified token is rejected.
  • Failure pages and logs do not echo the secret value.

Keep evidence token-safe

InboxTap matcher diagnostics do not include bodies or token-bearing URL values. Redacted reports remove common secret surfaces and pseudonymize addresses, but redaction is best-effort. Review an artifact before sharing it outside the test environment.

Prefer assertions on URL origin, path, parameter presence, and final application state. Avoid snapshots of the raw email, browser address bar, or full captured link when a smaller structural assertion proves the same behavior.

Drive the link through the browser

Put the link into a real browser flow with isolated recipients, parallel workers, bounded waits, and runner-owned cleanup.

Read the Playwright guide