Mock Inbox - OTP/2FA Testing

Practice automating OTP verification flows with our mock inbox API

📧How Mock Inbox Works

In real applications, OTP codes are sent via email or SMS. Our mock inbox simulates this by storing OTPs in memory and providing an API endpoint to retrieve them for testing.

The Flow:

  1. Send OTP → POST /otp/send
  2. Retrieve OTP → GET /otp/inbox/:email
  3. Verify OTP → POST /otp/verify

🌐 API Base URL

https://www.passthenote.com/api/gateway

Interactive Demo

1Send OTP

🎯 Enterprise Testing Scenarios

API Endpoints

POST/otp/send

Generate and send OTP code to an email address.

Request Body:

{
  "email": "testuser@passthenote.com",
  "purpose": "login" // optional: login, signup, password-reset, verification
}

Response (200 OK):

{
  "message": "OTP sent successfully",
  "email": "testuser@passthenote.com",
  "expiresIn": "10 minutes"
}
GET/otp/inbox/:email

Retrieve OTP from mock inbox (for testing automation).

Example:

GET /otp/inbox/testuser@passthenote.com

Response (200 OK):

{
  "email": "testuser@passthenote.com",
  "otp": "123456",
  "purpose": "login",
  "expiresIn": "574 seconds"
}
POST/otp/verify

Verify an OTP code. Code is deleted after successful verification (one-time use).

Request Body:

{
  "email": "testuser@passthenote.com",
  "otp": "123456"
}

Success Response (200 OK):

{
  "message": "OTP verified successfully",
  "verified": true
}

Error Response (401 Unauthorized):

{
  "error": "Invalid OTP",
  "verified": false,
  "code": "INVALID_OTP"
}

Automation Examples

🔷Selenium (Java)

import okhttp3.*;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.By;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;

public class OTPTest {
    private static final String BASE_URL = "https://www.passthenote.com/api/gateway";
    
    @Test
    public void testOTPVerification() throws Exception {
        WebDriver driver = new ChromeDriver();
        OkHttpClient client = new OkHttpClient();
        String email = "testuser@passthenote.com";
        
        // Step 1: Navigate to app and trigger OTP send
        driver.get("https://www.passthenote.com/app/login");
        driver.findElement(By.id("email")).sendKeys(email);
        driver.findElement(By.id("request-otp-button")).click();
        
        // Step 2: Wait for "OTP sent" confirmation
        WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(10));
        wait.until(ExpectedConditions.visibilityOfElementLocated(By.id("otp-sent-message")));
        
        // Step 3: Poll for OTP with proper wait strategy
        String otp = pollForOTP(client, email, 10);
        System.out.println("Retrieved OTP: " + otp);
        
        // Step 4: Enter OTP in the UI
        driver.findElement(By.id("otp-input")).sendKeys(otp);
        driver.findElement(By.id("verify-button")).click();
        
        // Step 5: Verify success
        WebElement success = wait.until(
            ExpectedConditions.visibilityOfElementLocated(By.id("success-message"))
        );
        Assert.assertTrue(success.isDisplayed());
        
        driver.quit();
    }
    
    // Helper: Poll for delayed OTP (Enterprise scenario)
    private String pollForOTP(OkHttpClient client, String email, int maxAttempts) throws Exception {
        for (int i = 0; i < maxAttempts; i++) {
            Request request = new Request.Builder()
                .url(BASE_URL + "/otp/inbox/" + email)
                .build();
                
            Response response = client.newCall(request).execute();
            
            if (response.code() == 202) {
                // Email delayed - wait and retry
                System.out.println("Email delayed, retrying... (" + (i+1) + "/" + maxAttempts + ")");
                Thread.sleep(1000);
                continue;
            }
            
            if (response.code() == 200) {
                String jsonData = response.body().string();
                JsonObject json = JsonParser.parseString(jsonData).getAsJsonObject();
                
                // Check spam folder
                String folder = json.get("folder").getAsString();
                if ("SPAM".equals(folder)) {
                    System.out.println("⚠️ Warning: Email went to spam folder!");
                }
                
                return json.get("otp").getAsString();
            }
        }
        throw new RuntimeException("Failed to retrieve OTP after " + maxAttempts + " attempts");
    }
}

🎭Playwright (TypeScript) - With Magic Link

import { test, expect } from '@playwright/test';

const BASE_URL = 'https://www.passthenote.com';

// Helper: Poll for delayed OTP
async function pollForOTP(request: any, email: string, maxAttempts = 10) {
  for (let i = 0; i < maxAttempts; i++) {
    const response = await request.get(
      `${BASE_URL}/api/gateway/otp/inbox/${encodeURIComponent(email)}`
    );
    
    if (response.status() === 202) {
      console.log(`Email delayed, attempt ${i+1}/${maxAttempts}...`);
      await new Promise(resolve => setTimeout(resolve, 1000));
      continue;
    }
    
    if (response.ok()) {
      const data = await response.json();
      
      // Check spam folder
      if (data.folder === 'SPAM') {
        console.warn('⚠️ Email went to spam folder!');
      }
      
      return data;
    }
  }
  throw new Error('Failed to retrieve OTP');
}

test('OTP verification with polling', async ({ page, request }) => {
  const email = 'testuser@passthenote.com';
  
  // Step 1: Navigate and request OTP with delay
  await page.goto(`${BASE_URL}/app/login?testDelay=true`);
  await page.fill('[data-testid="email-input"]', email);
  await page.click('[data-testid="request-otp"]');
  await page.waitForSelector('[data-testid="otp-sent-message"]');
  
  // Step 2: Poll for OTP (handles 202 delayed response)
  const otpData = await pollForOTP(request, email);
  console.log('OTP Retrieved:', otpData.otp);
  
  // Step 3: Enter OTP in UI
  await page.fill('[data-testid="otp-input"]', otpData.otp);
  await page.click('[data-testid="verify-button"]');
  
  // Step 4: Verify success
  await expect(page.locator('[data-testid="success-message"]')).toBeVisible();
});

test('Magic Link extraction (Interview Challenge)', async ({ page, request }) => {
  const email = 'testuser@passthenote.com';
  
  // Step 1: Trigger magic link email
  await request.post(`${BASE_URL}/api/gateway/otp/send`, {
    data: { email, purpose: 'login' }
  });
  
  // Step 2: Poll and retrieve magic link
  const otpData = await pollForOTP(request, email);
  console.log('Magic Link:', otpData.magicLink);
  
  // Step 3: Navigate to magic link (auto-login!)
  await page.goto(otpData.magicLink);
  
  // Step 4: Verify logged in (no OTP needed!)
  await expect(page.locator('[data-testid="dashboard"]')).toBeVisible();
});

🌲Cypress (JavaScript)

describe('OTP Verification', () => {
  const BASE_URL = 'https://www.passthenote.com';
  const email = 'testuser@passthenote.com';
  
  // Helper: Recursive polling for delayed emails
  const pollForOTP = (email, maxAttempts = 10) => {
    if (maxAttempts === 0) {
      throw new Error('Polling timeout - email not received');
    }
    
    return cy.request({
      url: `${BASE_URL}/api/gateway/otp/inbox/${encodeURIComponent(email)}`,
      failOnStatusCode: false
    }).then((response) => {
      if (response.status === 202) {
        cy.log(`Email delayed, retrying... (${11 - maxAttempts}/10)`);
        cy.wait(1000);
        return pollForOTP(email, maxAttempts - 1);
      }
      
      if (response.status === 200) {
        // Check spam folder
        if (response.body.folder === 'SPAM') {
          cy.log('⚠️ Warning: Email in spam folder!');
        }
        return cy.wrap(response.body);
      }
      
      throw new Error(`Unexpected status: ${response.status}`);
    });
  };
  
  it('should complete OTP verification with polling', () => {
    // Step 1: Request OTP via UI (with delay)
    cy.visit(`${BASE_URL}/app/login?testDelay=true`);
    cy.get('[data-testid="email-input"]').type(email);
    cy.get('[data-testid="request-otp"]').click();
    cy.get('[data-testid="otp-sent-message"]').should('be.visible');
    
    // Step 2: Poll for OTP (handles 202 response)
    pollForOTP(email).then((otpData) => {
      cy.log(`Retrieved OTP: ${otpData.otp}`);
      
      // Step 3: Enter OTP in UI
      cy.get('[data-testid="otp-input"]').type(otpData.otp);
      cy.get('[data-testid="verify-button"]').click();
      
      // Step 4: Verify success
      cy.get('[data-testid="success-message"]').should('be.visible');
    });
  });
  
  it('Magic Link - extract and navigate (Interview Question)', () => {
    // Step 1: Send OTP with magic link
    cy.request('POST', `${BASE_URL}/api/gateway/otp/send`, {
      email,
      purpose: 'login'
    });
    
    // Step 2: Poll and get magic link
    pollForOTP(email).then((otpData) => {
      cy.log(`Magic Link: ${otpData.magicLink}`);
      
      // Step 3: Extract token and navigate
      const url = new URL(otpData.magicLink);
      const token = url.searchParams.get('token');
      cy.log(`Extracted Token: ${token}`);
      
      // Step 4: Visit magic link (auto-login!)
      cy.visit(otpData.magicLink);
      cy.get('[data-testid="dashboard"]').should('be.visible');
    });
  });
  
  it('should detect spam folder', () => {
    // Send to spam
    cy.request('POST', `${BASE_URL}/api/gateway/otp/send`, {
      email,
      purpose: 'login',
      triggerSpam: true
    });
    
    // Retrieve and check folder
    cy.request(`${BASE_URL}/api/gateway/otp/inbox/${encodeURIComponent(email)}`)
      .then((response) => {
        expect(response.body.folder).to.eq('SPAM');
        cy.log('✅ Test passed: Detected spam folder!');
      });
  });
});

REST Assured (Java)

import io.restassured.RestAssured;
import io.restassured.response.Response;
import static io.restassured.RestAssured.given;
import static org.hamcrest.Matchers.*;

public class OTPApiTest {
    private static final String BASE_URL = "https://www.passthenote.com/api/gateway";
    
    @BeforeClass
    public static void setup() {
        RestAssured.baseURI = BASE_URL;
    }
    
    @Test
    public void testCompleteOTPFlow() {
        String email = "testuser@passthenote.com";
        
        // Step 1: Send OTP
        given()
            .contentType("application/json")
            .body("{ \"email\": \"" + email + "\", \"purpose\": \"login\" }")
        .when()
            .post("/otp/send")
        .then()
            .statusCode(200)
            .body("message", equalTo("OTP sent successfully"));
            
        // Step 2: Retrieve OTP from inbox
        Response otpResponse = given()
            .when()
                .get("/otp/inbox/" + email)
            .then()
                .statusCode(200)
                .body("email", equalTo(email))
                .body("otp", matchesPattern("\\d{6}"))
                .extract().response();
                
        String otp = otpResponse.path("otp");
        System.out.println("Retrieved OTP: " + otp);
        
        // Step 3: Verify OTP
        given()
            .contentType("application/json")
            .body("{ \"email\": \"" + email + "\", \"otp\": \"" + otp + "\" }")
        .when()
            .post("/otp/verify")
        .then()
            .statusCode(200)
            .body("verified", equalTo(true))
            .body("message", equalTo("OTP verified successfully"));
    }
    
    @Test
    public void testInvalidOTP() {
        String email = "testuser@passthenote.com";
        
        given()
            .contentType("application/json")
            .body("{ \"email\": \"" + email + "\", \"otp\": \"000000\" }")
        .when()
            .post("/otp/verify")
        .then()
            .statusCode(401)
            .body("verified", equalTo(false))
            .body("error", equalTo("Invalid OTP"));
    }
}

💡 Best Practices for OTP Automation

✅ Do's

  • • Wait for OTP to be generated before retrieving (2-3 second delay)
  • • Check OTP expiration time before using
  • • Handle OTP_NOT_FOUND and OTP_EXPIRED errors
  • • Verify one-time use (second verification should fail)
  • • Test invalid OTP scenarios
  • • Use unique emails per test run when possible
  • • Clear OTPs after test completion

❌ Don'ts

  • • Don't hardcode OTP values in tests
  • • Don't skip wait time between send and retrieve
  • • Don't reuse expired OTPs
  • • Don't assume OTP format (always retrieve via API)
  • • Don't run tests in parallel with same email
  • • Don't forget to test error scenarios
  • • Don't skip verification response assertions

🎯 Real-World Application:

In production, you would integrate with email testing services like Mailtrap, MailHog, or use email forwarding to a test inbox. This mock inbox simulates that pattern, teaching you the essential skill of automating 2FA/MFA flows that are critical in modern applications.