Introduction
One-time passwords are everywhere in 2026: six-digit codes sent by email to verify logins, time-limited tokens gating account activation, numeric codes for passwordless signup. The fundamental promise of OTP authentication — expires fast, works only once — is exactly what makes testing it hard.
Manual OTP testing is tedious: trigger the email, open an inbox, copy the code, paste it into the form, click submit — all within the expiry window. At scale across 50 user scenarios in staging, this consumes hours.
Automating OTP flows requires infrastructure that:
Creates a programmable test inbox
Triggers the OTP email from your app
Receives the email reliably and immediately
Extracts the numeric code without manual parsing
Uses it in the test flow before expiry
Scales across parallel test workers
FreeCustom.Email is purpose-built for this. The official JavaScript and Python SDKs handle the entire lifecycle — register, trigger, wait, extract, cleanup — with typed APIs and no regex required.
Why General Email APIs Fail at OTP Testing
Problem | Impact |
|---|---|
No OTP extraction | Regex in every test file; breaks when templates change |
Client-side polling | Slow, burns API quota, fragile timing |
Public inboxes | Another process can consume your OTP first |
No timeout handling | Tests hang instead of failing fast |
FreeCustom.Email solves all four with native extraction, server-side long-poll, private inboxes, and typed timeout exceptions.
Installation
JavaScript / TypeScript
npm install freecustom-emailPython
pip install freecustom-emailCLI (for shell scripting)
npm install -g fcemailOTP Extraction: How It Works
On Growth+ plans, every incoming email is automatically scanned for:
Numeric OTP codes: 4–8 digit sequences in subject and body
Verification links: Complete URLs with authentication tokens
These appear as first-class fields on every message object:
{
"id": "msg_abc123",
"from": "noreply@yourapp.com",
"subject": "Your verification code: 847291",
"otp": "847291",
"verification_link": null,
"date": "2026-04-09T10:15:22.000Z"
}On lower plans, otp returns "__DETECTED__" as a signal to upgrade. See OTP extraction documentation.
SDK: The getOtpForInbox Pattern (Recommended)
The most concise and reliable pattern — one call handles register, trigger, wait, and optional cleanup:
JavaScript / TypeScript
import { FreecustomEmailClient } from 'freecustom-email';
const client = new FreecustomEmailClient({
apiKey: process.env.FCE_API_KEY!,
timeout: 10_000,
retry: { attempts: 2, initialDelayMs: 500 },
});
const otp = await client.getOtpForInbox(
'signup-test@ditapi.info',
async () => {
// Your app sends the OTP email here
await fetch('https://yourapp.com/api/send-otp', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email: 'signup-test@ditapi.info' }),
});
},
{
timeoutMs: 30_000,
autoUnregister: true,
},
);
console.log('OTP:', otp); // '847291'Python (async)
import asyncio, os, httpx
from freecustom_email import FreeCustomEmail
client = FreeCustomEmail(api_key=os.environ["FCE_API_KEY"])
async def send_otp_email():
async with httpx.AsyncClient() as http:
await http.post(
"https://yourapp.com/api/send-otp",
json={"email": "signup-test@ditapi.info"},
)
async def main():
otp = await client.get_otp_for_inbox(
inbox="signup-test@ditapi.info",
trigger_fn=send_otp_email,
timeout_ms=30_000,
auto_unregister=True,
)
print(f"OTP: {otp}") # '847291'
asyncio.run(main())Python (sync — no asyncio)
from freecustom_email import FreeCustomEmail
import requests
client = FreeCustomEmail(api_key="fce_...", sync=True)
client.inboxes.register("signup-test@ditapi.info")
# Trigger the OTP email
requests.post(
"https://yourapp.com/api/send-otp",
json={"email": "signup-test@ditapi.info"},
)
otp = client.otp.wait_for("signup-test@ditapi.info", timeout_ms=30_000)
print(f"OTP: {otp}")
client.inboxes.unregister("signup-test@ditapi.info")Manual OTP Flow (Step by Step)
For cases where you need more control:
// JavaScript — step by step
await client.inboxes.register('mytest@ditapi.info');
// Trigger OTP send in your app
await fetch('https://yourapp.com/api/request-otp', {
method: 'POST',
body: JSON.stringify({ email: 'mytest@ditapi.info' }),
headers: { 'Content-Type': 'application/json' },
});
// Wait for email with optional sender filter
const msg = await client.messages.waitFor('mytest@ditapi.info', {
timeoutMs: 30_000,
pollIntervalMs: 2_000,
match: m => m.from.includes('noreply@yourapp.com'),
});
console.log('OTP:', msg.otp);
console.log('Link:', msg.verificationLink);
// Or use the dedicated OTP endpoint
const result = await client.otp.get('mytest@ditapi.info');
console.log('OTP:', result.otp);
console.log('Link:', result.verification_link);
await client.inboxes.unregister('mytest@ditapi.info');# Python — step by step
await client.inboxes.register("mytest@ditapi.info")
# Trigger in your app
async with httpx.AsyncClient() as http:
await http.post(
"https://yourapp.com/api/request-otp",
json={"email": "mytest@ditapi.info"},
)
# Wait with filter
msg = await client.messages.wait_for(
"mytest@ditapi.info",
timeout_ms=30_000,
poll_interval_ms=2_000,
match=lambda m: "noreply" in m.from_,
)
print(f"OTP: {msg.otp}")
# Or dedicated endpoint
result = await client.otp.get("mytest@ditapi.info")
print(f"OTP: {result.otp}")
print(f"From: {result.from_}")
await client.inboxes.unregister("mytest@ditapi.info")Pytest: Complete OTP Test Suite
# tests/test_otp_flow.py
import pytest, asyncio, os, time
from freecustom_email import FreeCustomEmail
from freecustom_email.errors import WaitTimeoutError, PlanError
@pytest.fixture(scope="session")
def client():
return FreeCustomEmail(api_key=os.environ["FCE_API_KEY"])
@pytest.fixture
async def otp_inbox(client):
"""Unique inbox per test, cleaned up after."""
email = f"otp-test-{int(time.time())}@ditapi.info"
await client.inboxes.register(email)
yield email
await client.inboxes.unregister(email)
@pytest.mark.asyncio
async def test_otp_is_6_digits(client, otp_inbox, app_api):
"""OTP is exactly 6 numeric digits."""
await app_api.send_otp(otp_inbox)
otp = await client.otp.wait_for(otp_inbox, timeout_ms=30_000)
assert otp.isdigit(), f"OTP contains non-numeric chars: {otp}"
assert len(otp) == 6, f"Expected 6-digit OTP, got {len(otp)} digits: {otp}"
@pytest.mark.asyncio
async def test_otp_allows_verification(client, otp_inbox, app_api):
"""Valid OTP completes signup flow."""
await app_api.signup(email=otp_inbox, password="SecurePass@2026!")
otp = await client.otp.wait_for(otp_inbox, timeout_ms=30_000)
result = await app_api.verify_otp(email=otp_inbox, otp=otp)
assert result.verified is True
@pytest.mark.asyncio
async def test_expired_otp_is_rejected(client, otp_inbox, app_api):
"""OTP rejected after expiry window."""
await app_api.send_otp(otp_inbox)
otp = await client.otp.wait_for(otp_inbox, timeout_ms=30_000)
# Wait past expiry (adjust to your app's test expiry setting)
await asyncio.sleep(65)
result = await app_api.verify_otp(email=otp_inbox, otp=otp)
assert result.status == 422 # Expired
@pytest.mark.asyncio
async def test_otp_is_single_use(client, otp_inbox, app_api):
"""OTP cannot be used a second time."""
await app_api.send_otp(otp_inbox)
otp = await client.otp.wait_for(otp_inbox, timeout_ms=30_000)
first = await app_api.verify_otp(email=otp_inbox, otp=otp)
assert first.verified is True
second = await app_api.verify_otp(email=otp_inbox, otp=otp)
assert second.status == 422 # Already used
@pytest.mark.asyncio
async def test_otp_timeout_raises(client, otp_inbox):
"""WaitTimeoutError raised when no email arrives."""
# Don't trigger any email — expect timeout
with pytest.raises(WaitTimeoutError) as exc:
await client.otp.wait_for(otp_inbox, timeout_ms=3_000)
assert exc.value.inbox == otp_inbox
assert exc.value.timeout_ms == 3_000Jest: OTP Testing with Node.js
// tests/otp-flow.test.js
const { FreecustomEmailClient, TimeoutError, PlanError } = require('freecustom-email');
const client = new FreecustomEmailClient({
apiKey: process.env.FCE_API_KEY,
retry: { attempts: 2, initialDelayMs: 500 },
});
describe('OTP Verification Flow', () => {
let testEmail;
beforeEach(async () => {
testEmail = `otp-jest-${Date.now()}@ditapi.info`;
await client.inboxes.register(testEmail);
});
afterEach(async () => {
await client.inboxes.unregister(testEmail);
});
test('OTP is 6 numeric digits', async () => {
const otp = await client.getOtpForInbox(
testEmail,
() => fetch('https://staging.yourapp.com/api/send-otp', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email: testEmail }),
}),
{ timeoutMs: 30_000, autoUnregister: false }, // we handle cleanup in afterEach
);
expect(otp).toMatch(/^\d{6}$/);
}, 40_000); // Jest timeout > wait timeout
test('Valid OTP verifies account', async () => {
const otp = await client.getOtpForInbox(
testEmail,
() => triggerSignup(testEmail),
{ timeoutMs: 30_000, autoUnregister: false },
);
const res = await fetch('https://staging.yourapp.com/api/verify-otp', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email: testEmail, otp }),
});
expect(res.status).toBe(200);
}, 40_000);
test('TimeoutError when no email arrives', async () => {
// No trigger — expect timeout
await expect(
client.otp.waitFor(testEmail, { timeoutMs: 3_000 })
).rejects.toBeInstanceOf(TimeoutError);
}, 10_000);
});Parallel OTP Testing
FreeCustom.Email's private inbox model is designed for parallel test workers:
// Test 3 services sending OTPs simultaneously
const emails = [
`auth-${Date.now()}@ditapi.info`,
`payment-${Date.now()}@ditapi.info`,
`admin-${Date.now()}@ditapi.info`,
];
// Register all inboxes in parallel
await Promise.all(emails.map(e => client.inboxes.register(e)));
// Trigger OTP emails from each service simultaneously
await Promise.all([
triggerAuthOtp(emails[0]),
triggerPaymentOtp(emails[1]),
triggerAdminOtp(emails[2]),
]);
// Wait for all OTPs in parallel — each isolated
const otps = await Promise.all(
emails.map(e => client.otp.waitFor(e, { timeoutMs: 30_000 }))
);
console.log('Auth OTP:', otps[0]);
console.log('Payment OTP:', otps[1]);
console.log('Admin OTP:', otps[2]);
// Cleanup
await Promise.all(emails.map(e => client.inboxes.unregister(e)));CLI: OTP in Shell Scripts
# Authenticate once
fce auth login
# One-liner OTP capture for shell scripts
OTP=$(fce otp test@ditapi.info)
echo "OTP: $OTP"
# CI step example
trigger_signup_email
OTP=$(fce otp $TEST_EMAIL)
curl -X POST https://staging.yourapp.com/api/verify \
-d "email=$TEST_EMAIL&otp=$OTP"See CLI documentation.
MCP: AI Agent OTP Testing (Growth+ plans)
{
"mcpServers": {
"fce-mcp": {
"command": "npx",
"args": ["-y", "fce-mcp-server"],
"env": { "FCE_API_KEY": "your_growth_key" }
}
}
}The create_and_wait_for_otp tool lets an AI agent test the entire OTP flow in a single call. See MCP documentation and AI agent use case.
Error Handling
import { TimeoutError, PlanError, RateLimitError, AuthError } from 'freecustom-email';
try {
const otp = await client.otp.waitFor('test@ditapi.info', { timeoutMs: 30_000 });
} catch (err) {
if (err instanceof TimeoutError) {
throw new Error('OTP email not received — check email delivery pipeline');
}
if (err instanceof PlanError) {
throw new Error(`OTP extraction requires Growth plan: ${err.message}`);
}
if (err instanceof RateLimitError) {
await new Promise(r => setTimeout(r, err.retryAfter * 1000));
// retry...
}
if (err instanceof AuthError) {
throw new Error('Invalid FCE_API_KEY');
}
}from freecustom_email.errors import (
WaitTimeoutError, PlanError, RateLimitError, AuthError
)
try:
otp = await client.otp.wait_for("test@ditapi.info", timeout_ms=30_000)
except WaitTimeoutError as e:
raise AssertionError(f"OTP not received in {e.timeout_ms}ms for {e.inbox}")
except PlanError as e:
raise RuntimeError(f"Growth plan required: {e}. Upgrade at {e.upgrade_url}")
except RateLimitError as e:
await asyncio.sleep(e.retry_after)
otp = await client.otp.wait_for("test@ditapi.info", timeout_ms=30_000)FAQ
Q: What OTP formats does FreeCustom.Email extract? 4–8 digit numeric codes and full verification URLs, including encoded tokens, JWTs, and UUIDs. See OTP documentation.
Q: What if the OTP is in an HTML button image? The extractor scans HTML and plaintext body for <a href> tags and text patterns. Image-embedded OTPs are not extracted. Use client.messages.get() for the full HTML body in that case.
Q: What plan is needed for OTP extraction? Growth ($49/mo). Developer and Startup plans return __DETECTED__ but not the actual code.
Q: Can I wait for OTP without the Growth plan? Yes — use client.messages.waitFor() to receive the full message, then parse the body yourself.
Q: How do I test OTP rate limiting? Send multiple OTP requests in rapid succession and assert the appropriate HTTP 429 response from your app.
Q: Is there a changelog? Yes — see the API changelog.
Magic Link Testing API in 2026: Automate Passwordless Login with FreeCustom.Email
Meta Description: Need to test magic links automatically? FreeCustom.Email extracts verification links from emails for automated Playwright and pytest magic link testing. Full JavaScript and Python SDK guide.
Introduction
Magic links — click-to-authenticate URLs sent by email — are the dominant pattern for passwordless login in 2026. They offer better security than passwords and better UX than OTP codes. But they create a specific testing challenge: your test must retrieve a URL embedded in an email, navigate to it, and assert authentication — all within the link's validity window.
Without the right infrastructure, magic link tests involve parsing <a href> tags from HTML bodies, URL-decoding tokens, and hoping the polling loop finds the email before the link expires.
FreeCustom.Email solves this natively: the verification_link field is automatically extracted from every email and available directly on the message object — no HTML parsing, no regex, no URL decoding required.
SDK: Magic Link Extraction
The verification_link field is extracted alongside otp on every message (Growth+ plans):
{
"id": "msg_abc123",
"from": "auth@yourapp.com",
"subject": "Sign in to YourApp",
"otp": null,
"verification_link": "https://yourapp.com/auth/verify?token=eyJhbGci...abc123",
"date": "2026-04-09T10:00:00.000Z"
}The complete URL — including token query parameters, regardless of length or encoding — is returned ready to use.
Installation
# JavaScript
npm install freecustom-email
# Python
pip install freecustom-emailComplete Magic Link Flow: JavaScript
import { FreecustomEmailClient } from 'freecustom-email';
const client = new FreecustomEmailClient({
apiKey: process.env.FCE_API_KEY!,
retry: { attempts: 2, initialDelayMs: 500 },
});
async function testMagicLinkLogin(appUrl: string): Promise<string> {
const email = `magic-${Date.now()}@ditapi.info`;
// Register → trigger → wait → return link, all in sequence
await client.inboxes.register(email);
// Trigger your app to send the magic link
await fetch(`${appUrl}/api/request-magic-link`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email }),
});
// Wait for email, match by subject if needed
const msg = await client.messages.waitFor(email, {
timeoutMs: 30_000,
pollIntervalMs: 2_000,
match: m => m.subject.toLowerCase().includes('sign in'),
});
if (!msg.verificationLink) {
throw new Error('Magic link not found in email body');
}
await client.inboxes.unregister(email);
return msg.verificationLink;
}
// Usage
const link = await testMagicLinkLogin('https://staging.yourapp.com');
console.log('Magic link:', link);
// https://staging.yourapp.com/auth/verify?token=eyJhbGci...Complete Magic Link Flow: Python
import asyncio, os, time, httpx
from freecustom_email import FreeCustomEmail
from freecustom_email.errors import WaitTimeoutError
client = FreeCustomEmail(api_key=os.environ["FCE_API_KEY"])
async def get_magic_link(app_url: str) -> str:
email = f"magic-{int(time.time())}@ditapi.info"
await client.inboxes.register(email)
async with httpx.AsyncClient() as http:
await http.post(
f"{app_url}/api/request-magic-link",
json={"email": email},
)
msg = await client.messages.wait_for(
email,
timeout_ms=30_000,
poll_interval_ms=2_000,
match=lambda m: "sign in" in m.subject.lower(),
)
if not msg.verification_link:
raise ValueError("No magic link found in email body")
await client.inboxes.unregister(email)
return msg.verification_link
async def main():
link = await get_magic_link("https://staging.yourapp.com")
print(f"Magic link: {link}")
asyncio.run(main())Playwright: Full Magic Link Test Suite
// tests/magic-link.spec.ts
import { test, expect } from '@playwright/test';
import { FreecustomEmailClient, TimeoutError } from 'freecustom-email';
const client = new FreecustomEmailClient({
apiKey: process.env.FCE_API_KEY!,
});
test.describe('Magic link authentication', () => {
let email: string;
test.beforeEach(async ({}, testInfo) => {
email = `magic-w${testInfo.workerIndex}-${Date.now()}@ditapi.info`;
await client.inboxes.register(email);
});
test.afterEach(async () => {
await client.inboxes.unregister(email);
});
test('new user authenticates via magic link', async ({ page }) => {
// Request magic link
await page.goto('/auth/magic-link');
await page.fill('[name="email"]', email);
await page.click('[type="submit"]');
await expect(page.locator('[data-testid="check-email"]')).toBeVisible();
// Get link — no HTML parsing, no URL decoding
const msg = await client.messages.waitFor(email, { timeoutMs: 30_000 });
expect(msg.verificationLink).toBeTruthy();
expect(msg.verificationLink).toContain('token=');
// Navigate to magic link
await page.goto(msg.verificationLink!);
// Assert authenticated
await expect(page).toHaveURL(/\/dashboard/);
await expect(page.locator('[data-testid="user-menu"]')).toBeVisible();
});
test('magic link is single-use', async ({ page }) => {
await page.goto('/auth/magic-link');
await page.fill('[name="email"]', email);
await page.click('[type="submit"]');
const msg = await client.messages.waitFor(email, { timeoutMs: 30_000 });
const link = msg.verificationLink!;
// First use — succeeds
await page.goto(link);
await expect(page).toHaveURL(/\/dashboard/);
// Second use — fails
await page.goto(link);
await expect(page.locator('[data-testid="link-expired"]')).toBeVisible();
});
test('expired magic link shows appropriate error', async ({ page }) => {
await page.goto('/auth/magic-link');
await page.fill('[name="email"]', email);
await page.click('[type="submit"]');
const msg = await client.messages.waitFor(email, { timeoutMs: 30_000 });
// Force expiry (if your app supports short test expiry)
await page.goto(msg.verificationLink! + '&_force_expire=1');
await expect(page.locator('[data-testid="link-expired"]')).toBeVisible();
await expect(page.locator('[data-testid="request-new-link"]')).toBeVisible();
});
test('link not found email shows error state', async () => {
// Don't trigger any email — expect timeout
await expect(
client.messages.waitFor(email, { timeoutMs: 3_000 })
).rejects.toBeInstanceOf(TimeoutError);
});
});Pytest: Magic Link Testing
# tests/test_magic_link.py
import pytest, asyncio, time
from freecustom_email import FreeCustomEmail
from freecustom_email.errors import WaitTimeoutError
from playwright.async_api import async_playwright
@pytest.fixture(scope="session")
def fce():
return FreeCustomEmail(api_key=os.environ["FCE_API_KEY"])
@pytest.fixture
async def magic_email(fce):
email = f"magic-{int(time.time())}@ditapi.info"
await fce.inboxes.register(email)
yield email
await fce.inboxes.unregister(email)
@pytest.mark.asyncio
async def test_magic_link_authenticates(fce, magic_email):
async with async_playwright() as p:
browser = await p.chromium.launch()
page = await browser.new_page()
# Request magic link
await page.goto("https://staging.yourapp.com/auth/magic-link")
await page.fill('[name="email"]', magic_email)
await page.click('[type="submit"]')
# Wait for email and get link
msg = await fce.messages.wait_for(magic_email, timeout_ms=30_000)
assert msg.verification_link, "No magic link in email"
assert "token=" in msg.verification_link
# Use the link
await page.goto(msg.verification_link)
assert "/dashboard" in page.url
await browser.close()
@pytest.mark.asyncio
async def test_no_email_raises_timeout(fce, magic_email):
"""WaitTimeoutError when no email arrives."""
with pytest.raises(WaitTimeoutError) as exc:
await fce.messages.wait_for(magic_email, timeout_ms=3_000)
assert exc.value.inbox == magic_emailWebSocket: Real-Time Magic Link Capture
For apps with very short link expiry windows, use WebSocket for the fastest possible capture:
const ws = client.realtime({
mailbox: 'magic-test@ditapi.info',
autoReconnect: false,
});
await new Promise<void>((resolve, reject) => {
const timer = setTimeout(() => {
ws.disconnect();
reject(new Error('Magic link not received within 30s'));
}, 30_000);
ws.on('email', async (email) => {
clearTimeout(timer);
ws.disconnect();
if (!email.verificationLink) {
reject(new Error('Email arrived but no magic link found'));
return;
}
console.log('Magic link:', email.verificationLink);
// Navigate browser to link...
resolve();
});
ws.connect();
});MCP for AI Agent Magic Link Testing
{
"mcpServers": {
"fce-mcp": {
"command": "npx",
"args": ["-y", "fce-mcp-server"],
"env": { "FCE_API_KEY": "your_growth_key" }
}
}
}The create_and_wait_for_otp MCP tool returns both otp and verification_link — AI agents can handle full magic link flows without additional code. See MCP documentation.
FAQ
Q: What if the magic link is inside an HTML button? FreeCustom.Email scans <a href="..."> tags in both HTML and plaintext bodies. Links inside button elements are captured. The full message body is also available via client.messages.get() if needed.
Q: Do complex tokens (JWTs, UUIDs, encoded params) work? Yes. The verification_link field returns the complete raw URL — no decoding or truncation.
Q: How fast is email delivery? Typically 1–3 seconds from send to receipt. WebSocket delivers in under 200ms. Long-poll returns the moment the email arrives.
Q: What plan is needed for verification_link extraction? Growth ($49/mo). On lower plans, use client.messages.waitFor() and parse msg.html or msg.text manually.
Q: How do I test that the app re-sends a new link correctly? Call client.messages.waitFor() twice — once for the initial link, click resend, then wait again. The second call receives the new message.
Q: Is there a security testing guide? Yes — see the security and penetration testing use case.
→ Get started free → npm install freecustom-email → pip install freecustom-email → Read OTP extraction docs → View all use cases
Written by
Dishant Singh
A full stack developer with good knowledge of email server, SEO, proxies, and networking, have more than 3 years of experience in building webapps for the netizens. Developing open source, fast, and free SaaS for all.
Frequently Asked Questions
Q: What if the OTP is in an HTML button image?+
The extractor scans HTML and plaintext body for <a href> tags and text patterns. Image-embedded OTPs are not extracted. Use client.messages.get() for the full HTML body in that case.
Q: What plan is needed for OTP extraction?+
Growth ($49/mo). Developer and Startup plans return __DETECTED__ but not the actual code.
Q: Can I wait for OTP without the Growth plan?+
Yes — use client.messages.waitFor() to receive the full message, then parse the body yourself.
Q: How do I test OTP rate limiting?+
Send multiple OTP requests in rapid succession and assert the appropriate HTTP 429 response from your app.
Q: Is there a changelog?+
Yes — see the API changelog.
Q: What if the magic link is inside an HTML button?+
FreeCustom.Email scans <a href="..."> tags in both HTML and plaintext bodies. Links inside button elements are captured. The full message body is also available via client.messages.get() if needed.
Q: Do complex tokens (JWTs, UUIDs, encoded params) work?+
Yes. The verification_link field returns the complete raw URL — no decoding or truncation.
Q: How fast is email delivery?+
Typically 1–3 seconds from send to receipt. WebSocket delivers in under 200ms. Long-poll returns the moment the email arrives.
Q: How do I test that the app re-sends a new link correctly?+
Call client.messages.waitFor() twice — once for the initial link, click resend, then wait again. The second call receives the new message.
Q: Is there a security testing guide?+
Yes — see the security and penetration testing use case.
No comments yet. Be the first to share your thoughts.