MailSlurp logo

blog

OTP Testing for Email and SMS Login

Build an OTP checker that receives the right email or SMS, submits the code, and tests login, expiry, resends and one-time use.

OTP Testing for Email and SMS Login article preview

An OTP test should follow the code all the way from the login request to the account it opens. A message can arrive successfully while the code is expired, belongs to another user, or fails when entered.

Checking this by hand works for one login. It becomes harder when a suite runs several accounts at once, retries a failed request, or tests both email and SMS. Your test needs its own recipient and a way to identify the message for the current request.

MailSlurp provides private email inboxes and phone numbers that tests can control through an API. Use them to receive the real message, extract the OTP, and submit it through your application. The application remains responsible for accepting or rejecting the code.

What an OTP checker needs to check

An OTP, or one-time password, is a short-lived code used during sign-in, verification or recovery. A checker that only recognizes six digits checks the format. An application test also checks delivery, the intended account, expiry and one-time use.

For example, a login test can request a code for a new test user, receive it in that user's inbox, enter it in the browser, and assert that the account page shows the expected email address. A generic "Success" message is weaker evidence: it might appear before the session has been established.

Different authentication methods need different inputs:

Method What the test receives or generates What to verify in the application
Email OTP Code from a real email The code verifies or signs in the intended account
SMS OTP Code from a real text message The number belongs to the test user and the code completes the challenge
Magic link URL from the delivered email The URL has the expected origin and path, and opens the intended session
TOTP Time-based code from an enrolled authenticator secret Enrollment, the accepted time window and recovery behavior

A format checker can accept a sample such as 004281 without knowing whether any application issued it. To check a real OTP, keep the issuing request, intended user and purpose together. Only the application can establish whether that code is valid for the pending challenge.

Email and SMS OTPs require message delivery. TOTP is generated from a secret and time, so its test does not wait for an email or SMS. See the TOTP testing walkthrough for that setup.

A password followed by an SMS code can be a two-factor flow. An email code by itself is not automatically MFA; count the independent factors your application actually requires.

Receive the code for this login attempt

Start by creating an inbox for the test and entering its address in your application's login or signup form. Record the time immediately before requesting the code, then wait for a matching message with a bounded timeout.

The Playwright email example shows inbox creation, subject matching and cleanup. Its wait uses the inbox ID and request time together. That prevents an earlier message from satisfying the current check, while separate inboxes keep parallel tests apart.

Use your application's expected sender or subject as an additional filter. If several messages can arrive within one test, match a request-specific value when your application exposes one. A fixed sleep cannot tell you which message arrived; a loose "latest email" check can select a resend or a different notification.

Once you have the received message ID, the email verification code API can extract code candidates. That guide includes a Node.js helper which rejects missing or ambiguous six-digit results and preserves leading zeros. Use the length and template rules your application actually issues.

Do not select the first number in the body without checking its context. A verification email may also contain an order number, support number or date. If extraction returns several candidates, fail the test and inspect the message rather than trying each value against the login form.

Submit the OTP and check the account

Start this scenario without an authenticated session. After requesting the code but before submitting it, check that the protected account page or API still rejects access under your application's contract. This catches a test that would pass because a previous login left a session behind.

Enter the extracted code through the same browser journey that requested it. Keep it as a string: converting 004281 to a number changes the value. After submission, check an account-specific result such as the signed-in user's email address or access to the protected page.

Keep the failure paths in separate tests so a successful login does not consume the code needed for another assertion:

Test Expected result
Correct current code The intended account completes verification
Incorrect code The challenge fails without creating a session
Code issued for another test user The requesting account remains unverified
Code used a second time The consumed code is rejected
Code submitted after expiry The application rejects it under its configured expiry rule
Resend followed by submission Old and new codes behave according to the application's resend policy
Repeated failed attempts The configured attempt limit or throttling takes effect

The OWASP MFA guidance recommends short-lived, single-use OTPs with limits on failed attempts. Turn your application's chosen rules into assertions. For expiry tests, use a controlled clock or a short test-environment lifetime where supported; do not lengthen every browser test with a production-length sleep.

Resend behavior deserves its own test. Record which request produced each message, then verify whether the previous code should remain valid. Do not assume that all products invalidate the old code at the same point.

For SMS OTP testing, give the test a MailSlurp phone number, request the text through your application, and wait for the matching SMS. The SMS and 2FA testing guide explains number selection and the difference between receiving a text and testing the application's code policy. The Playwright SMS walkthrough carries the received code back into the browser.

For a magic link, extract the intended action URL rather than every link in the email. Check its origin and path before opening it, then assert the resulting account. The parallel password-reset example demonstrates current-message matching, old-link rejection and one-time use with a runnable application.

When an OTP test fails

Separate a delivery timeout from an extraction failure and an application rejection. A timeout means the expected message did not satisfy the wait within its budget. An ambiguous extraction means the message arrived but did not provide one usable code under your rules. A rejected code means the application refused the submitted value; check the recipient, request, age and resend history.

Retain inbox and message IDs with restricted failure evidence. Avoid putting usable OTPs, full email bodies or reset URLs into shared CI logs. If you open the message in the dashboard while debugging, a wait with unreadOnly: true can exclude it. Use explicit request matching and unreadOnly: false when manual reading should not change the result.

For a checker that fails only after a template change, isolate the two checks: feed a known safe sample into the parser, then run the delivered-code application test. A parser failure points to message structure or ambiguity; a successful extraction followed by rejection points to the account, purpose or challenge policy. Keep those failures distinct in the test report.

Clean up temporary inboxes after the test and remove application test accounts through your own teardown. Those are separate resources. For a complete starting test, use the Playwright email verification example and adapt its selectors, message rules and account assertions to your application.