DEV Community

Cover image for How to E2E Test Amazon SES Email Workflows in Playwright
zerodrop
zerodrop

Posted on

How to E2E Test Amazon SES Email Workflows in Playwright

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 });
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode
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');
});
Enter fullscreen mode Exit fullscreen mode

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>` },
    },
  },
}));
Enter fullscreen mode Exit fullscreen mode
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"]');
Enter fullscreen mode Exit fullscreen mode

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": "*"
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

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 }}
Enter fullscreen mode Exit fullscreen mode

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

Top comments (0)