惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

MongoDB | Blog
MongoDB | Blog
宝玉的分享
宝玉的分享
博客园 - 三生石上(FineUI控件)
小众软件
小众软件
罗磊的独立博客
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
S
SegmentFault 最新的问题
Last Week in AI
Last Week in AI
人人都是产品经理
人人都是产品经理
博客园 - 聂微东
博客园 - 司徒正美
博客园 - 叶小钗
T
Tailwind CSS Blog
博客园 - Franky
V
V2EX
有赞技术团队
有赞技术团队
美团技术团队
雷峰网
雷峰网
爱范儿
爱范儿
Jina AI
Jina AI
D
DataBreaches.Net
H
Help Net Security
酷 壳 – CoolShell
酷 壳 – CoolShell

DEV Community

Authentication Security Deep Dive: From Brute Force to Salted Hashing (With Java Examples) Why AI Systems Don’t Fail — They Drift Spilling beans for how i learn for exam😁"Reinforcement Learning Cheat Sheet" I Replaced Chrome with Safari for AI Browser Automation. Here's What Broke (and What Finally Worked) How Python Borrows Other People's Work The $40 Architecture: Processing 1 Billion API Requests with 99.99% Uptime Vibe Coding: A Workflow Guide (From Zero to SaaS) Most webhook security guides protect the wrong side. The scary part is delivery. Headless CMS for TanStack Start: Build a Blog with Cosmic EU Age Verification App "Hacked in 2 Minutes" — What Actually Happened Comfy Cloud’s delete function does not actually remove files Running AI Models on GPU Cloud Servers: A Beginner Guide Event-driven media intelligence with AWS Step Functions and Bedrock I scored 500 AI prompts across 8 quality dimensions — here's what broke How to Call Google Gemini API from Next.js (Free Tier, No Backend Needed) The Portal Protocol: Reclaiming Human Connection in the Age of AI How to Fix Your Team's Scattered Knowledge Problem With a Self-Hosted Forum Intro to tc Cloud Functors: A Graph-First Mental Model for the Modern Cloud Designing Multi-Tenant Backends With Both Ownership and Team Access I Built a Neumorphic CSS Library with 77+ Components — Here's What I Learned PostgreSQL Performance Optimization: Why Connection Pooling Is Critical at Scale Cómo construí un SaaS multi-rubro para gestionar expensas en Argentina con FastAPI + Vue 3 🚀 I Built an Ethical Hacking Scanner Tool – Open Source Project I Replaced /usage and /context in Claude Code With a Single Statusline A Pythonic Way to Handle Emails (IMAP/SMTP) with Auto-Discovery and AI-Ready Design I Collected 8.9 Million Polymarket Price Points — Here's What I Found About How Markets Really Move EcoTrack AI — Carbon Footprint Tracker & Dashboard Everyone's Using AI. No One Agrees How. 5 self-hosted ebook managers worth trying in 2026 Building Your First AI Agent with LangChain: From Chatbot to Autonomous Assistant
How to E2E Test Amazon SES Email Workflows in Playwright
zerodrop · 2026-06-20 · via DEV Community
Cover image for How to E2E Test Amazon SES Email Workflows in Playwright

zerodrop

Amazon SES is the go-to transactional email service for teams already in the AWS ecosystem. But testing SES emails end-to-end in CI is notoriously painful — IAM permissions, sandbox restrictions, and no easy way to catch sent emails programmatically.

This guide shows the complete setup for testing Amazon SES email flows in Playwright using ZeroDrop — no Docker, no shared inboxes, no mocking.


The app we're testing

A Next.js API route that sends a verification email via Amazon SES:

// app/api/auth/signup/route.ts
import { SESClient, SendEmailCommand } from '@aws-sdk/client-ses';

const ses = new SESClient({
  region: process.env.AWS_REGION!,
  credentials: {
    accessKeyId: process.env.AWS_ACCESS_KEY_ID!,
    secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!,
  },
});

export async function POST(req: Request) {
  const { email } = await req.json();

  const token = crypto.randomUUID();
  const verifyUrl = `${process.env.NEXT_PUBLIC_URL}/verify?token=${token}`;

  await ses.send(new SendEmailCommand({
    Source: 'noreply@yourapp.com',
    Destination: { ToAddresses: [email] },
    Message: {
      Subject: { Data: 'Verify your email' },
      Body: {
        Html: {
          Data: `<p>Click <a href="${verifyUrl}">here</a> to verify your email.</p>`,
        },
      },
    },
  }));

  return Response.json({ success: true });
}


Stage 1 — Local development: SES sandbox mode

When you first create an Amazon SES account, it starts in sandbox mode. In sandbox mode:

  • You can only send emails to verified email addresses
  • You can only send emails from verified email addresses or domains
  • There's a sending limit of 200 emails per day

This is fine for local development — verify your own email address in the SES console and use it as the test recipient. But sandbox mode completely blocks automated CI testing because you can't verify a randomly generated @zerodrop-sandbox.online address.

What it solves: Basic local testing with a fixed test email address.

What it doesn't solve: Automated testing with random addresses, parallel test runs, or CI pipelines.


Stage 2 — Request production access

To use SES with ZeroDrop in CI, you need to move out of sandbox mode. Request production access in the AWS SES console:

  1. Go to SES → Account dashboard
  2. Click Request production access
  3. Fill in your use case — "transactional email for application testing"
  4. AWS typically approves within 24 hours

Once approved, SES can send to any email address including ZeroDrop inboxes.


Stage 3 — CI: SES production + ZeroDrop

npm install zerodrop-client

import { test, expect } from '@playwright/test';
import { ZeroDrop } from 'zerodrop-client';

const mail = new ZeroDrop();

test('user can sign up and verify email', async ({ page }) => {
  // 1. Generate a disposable inbox
  const inbox = process.env.TEST_INBOX ?? mail.generateInbox();
  // → "swift-x7k2m@zerodrop-sandbox.online"

  // 2. Sign up — SES sends a real verification email to this inbox
  await page.goto('/signup');
  await page.fill('[data-testid="email"]', inbox);
  await page.click('[data-testid="submit"]');

  await expect(page).toHaveURL('/check-email');

  // 3. ZeroDrop catches the email — magic link auto-extracted
  const email = await mail.waitForLatest(inbox, { timeout: 30000 });

  expect(email.subject).toContain('Verify your email');
  expect(email.magicLink).not.toBeNull();

  // 4. Click the verification link
  await page.goto(email.magicLink!);

  // 5. Assert verified
  await expect(page).toHaveURL('/dashboard');
});


OTP flows

await ses.send(new SendEmailCommand({
  Source: 'noreply@yourapp.com',
  Destination: { ToAddresses: [email] },
  Message: {
    Subject: { Data: 'Your verification code' },
    Body: {
      Html: { Data: `<p>Your code is: <strong>${otp}</strong></p>` },
    },
  },
}));

const email = await mail.waitForLatest(inbox, { timeout: 30000 });

// OTP auto-extracted at the edge — no regex needed
expect(email.otp).not.toBeNull();
await page.fill('[data-testid="otp"]', email.otp!);
await page.click('[data-testid="verify"]');


IAM permissions for CI

Create a dedicated IAM user for CI with minimal permissions:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "ses:SendEmail",
        "ses:SendRawEmail"
      ],
      "Resource": "*"
    }
  ]
}

Store AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY as GitHub Actions secrets — never in your codebase.


GitHub Actions workflow

name: E2E Tests

on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-node@v4
        with:
          node-version: '20'

      - run: npm ci

      - run: npx playwright install --with-deps chromium

      - name: Generate test inbox
        id: inbox
        uses: zerodrop-dev/create-inbox@8706a59 # v1.0.0

      - name: Run E2E tests
        run: npx playwright test
        env:
          TEST_INBOX: ${{ steps.inbox.outputs.inbox }}
          AWS_REGION: ${{ secrets.AWS_REGION }}
          AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
          AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
          NEXT_PUBLIC_URL: ${{ secrets.STAGING_URL }}

Pro tip: Use GitHub's OIDC integration with AWS IAM roles instead of long-lived access keys — more secure and no key rotation needed.


The full picture

SES sandbox (local) SES production (staging) SES production + ZeroDrop (CI)
Verified recipients only ✅ (limitation)
Random test addresses
Automated in CI
Parallel test runs
OTP auto-extraction
Tests real delivery

Request production SES access early — it unlocks automated testing and takes 24 hours to approve. Once approved, the live SES + ZeroDrop combination gives you full E2E coverage in CI.


ZeroDrop — disposable email inboxes for CI pipelines. Free, no signup, no Docker.
zerodrop.dev · docs · npm