Authentication testing guide

How to test email OTP flows

An email OTP test should preserve the code exactly as delivered, submit it through the same endpoint or form a user would use, and verify the provider's expiry, attempt, and resend rules. InboxTap supplies the isolated recipient and bounded wait; the application test proves the authentication outcome.

Write down the OTP contract

Confirm the code length and alphabet, validity period, maximum attempts, resend strategy, and whether requesting a second code invalidates the first. Do not label six digits as a universal OTP standard: Better Auth defaults to six but allows configuration, and Supabase accepts lengths from six through ten digits.

Keep the value as a string throughout the test. Numeric conversion drops leading zeroes and can make a valid code impossible to enter exactly as the user received it.

Capture the first code

Create an inbox for the test, trigger the application's send-code action, and call waitForCode() with a subject filter. Its default expression matches a six-digit value in the captured text or HTML; provide a regular expression or string pattern when the application uses another format.

Submit the returned string through the real verification form or endpoint, then assert the authenticated identity and session. A test that only finds digits in an email does not prove that the backend accepts the intended code.

Distinguish a resent message

Capture the first complete message and retain its ID. After requesting another code, waitForMessage() with afterId returns a later delivery; extract the new code from that specific message as shown.

Do not use waitForCode({ afterId }) for this assertion. The helper first scans existing messages for a matching value, so it can return the earlier code before its long-poll request applies afterId. Waiting for the second message makes the delivery boundary explicit.

email-otp.test.ts
const firstEmail = await inbox.waitForMessage({
  subject: /code/i,
  timeoutMs: 20_000,
});

await requestAnotherCode();

const secondEmail = await inbox.waitForMessage({
  subject: /code/i,
  afterId: firstEmail.id,
  timeoutMs: 20_000,
});

const secondCode = secondEmail.text.match(/\b\d{6}\b/)?.[0];
expect(secondCode).toBeDefined();

Test rotation and attempt limits

When the configured resend strategy rotates codes, prove that the first code fails and the second succeeds. If the provider deliberately reuses an unexpired code, assert that behavior instead. Avoid making a product guarantee from whichever default happened to be installed.

Submit invalid values up to the configured limit and confirm that the next attempt is rejected as documented. Use application responses and stored session state for this assertion; InboxTap does not implement or observe the provider's verification counter.

Handle longer and custom codes

CapturedEmail.codes is a parser convenience for unique four- through eight-digit sequences. A custom waitForCode() pattern scans the message body directly, so use it for a nine- or ten-digit Supabase code or an application-specific alphanumeric format.

Make the regular expression narrow enough to avoid dates, support numbers, and unrelated identifiers in the template. A stable subject plus a format-specific boundary is safer than selecting the first sequence of digits.

Test expiry with a controlled clock

Prefer a provider-supported virtual clock, injected time source, or short test-only expiry. Sleeping for the complete production lifetime makes the suite slow and still leaves timing races around the boundary.

The expired code should fail without creating or extending a session. Requesting a fresh code after expiry should follow the documented resend policy and produce a distinguishable delivery.

Avoid secret output

Do not include the OTP in test names, assertion messages, screenshots, or routine logs. Assert its shape and the resulting application state instead of snapshotting the complete email body.

If a CI artifact is necessary, use InboxTap's bounded report collector with project-specific redaction patterns and review the result. Best-effort token detection cannot guarantee that every custom personal or secret value was found.

Adapt the recipe to your OTP format

Use the SDK reference to choose the right wait pattern, distinguish successive messages, inspect captured content, and add safe matcher diagnostics.

Explore the client SDK