# How do I set up automated (E2E) testing?

> Build an end-to-end test: use a sending_access API key, send to the mailbox simulator addresses, then assert on the returned email id and the webhook events.

You can verify your whole sending integration automatically — request, response, and event delivery — without mailing real people. The recipe: send to the [mailbox simulator addresses](https://www.mailblastr.com/docs/kb/test-addresses) with a scoped key, assert on the returned email `id`, then assert on the [webhook events](https://www.mailblastr.com/docs/webhooks/overview) MailBlastr fires for that email.

## What to set up

1. **Use a sending_access key** — Create a dedicated `sending_access` API key for tests (see [Create API key](https://www.mailblastr.com/docs/api/api-keys-create)). It can send but cannot delete domains or other keys, so a leaked CI secret is far less dangerous. Store it as a CI secret, never in the repo.
2. **Send to simulator addresses** — Send to `delivered@`, `bounced@`, and `complained@mailblastr.dev` to exercise the delivered, bounce, and complaint paths deterministically — from your verified domain.
3. **Assert on the response** — A successful `POST /emails` returns `{ "id": "…" }` with HTTP 200. Capture that `id`; everything downstream keys off it.
4. **Assert on events** — Confirm the expected event arrives — either by receiving a [webhook](https://www.mailblastr.com/docs/webhooks/overview) for that `id`, or by polling `GET /emails/:id`, whose response includes an `events` array.

## Example test

Send to the delivered simulator, then poll the email until a `delivered` event shows up. The same pattern works for `bounced@` (expect a `bounced` event) and `complained@` (expect a `complained` event).

**Node.js**

```js
import { MailBlastr } from 'mailblastr';

const mb = new MailBlastr(process.env.MAILBLASTR_TEST_KEY); // a sending_access key, from CI secrets

async function send(to) {
  const { data, error } = await mb.emails.send({
    from: 'Acme <hello@yourdomain.com>',
    to: [to],
    subject: 'E2E delivery test',
    html: '<p>ping</p>',
  });
  if (error) throw new Error(`send failed: ${JSON.stringify(error)}`);
  return data.id; // assert: id is a non-empty string
}

async function waitForEvent(id, type, { tries = 20, delayMs = 3000 } = {}) {
  for (let i = 0; i < tries; i++) {
    const { data: email } = await mb.emails.get(id);
    if ((email?.events || []).some((e) => e.type === type)) return email;
    await new Promise((r) => setTimeout(r, delayMs));
  }
  throw new Error(`timed out waiting for ${type} on ${id}`);
}

// Delivered path
const id = await send('delivered@mailblastr.dev');
await waitForEvent(id, 'delivered');
console.log('delivered ✓');
```

**Python**

```python
import os, time, requests

API = "https://api.mailblastr.com"
KEY = os.environ["MAILBLASTR_TEST_KEY"]  # a sending_access key, from CI secrets
HEAD = {"Authorization": f"Bearer {KEY}"}

def send(to):
    res = requests.post(
        f"{API}/emails",
        headers=HEAD,
        json={
            "from": "Acme <hello@yourdomain.com>",
            "to": [to],
            "subject": "E2E delivery test",
            "html": "<p>ping</p>",
        },
    )
    res.raise_for_status()
    return res.json()["id"]  # assert: non-empty string

def wait_for_event(email_id, type_, tries=20, delay=3.0):
    for _ in range(tries):
        email = requests.get(f"{API}/emails/{email_id}", headers=HEAD).json()
        if any(e.get("type") == type_ for e in email.get("events", [])):
            return email
        time.sleep(delay)
    raise AssertionError(f"timed out waiting for {type_} on {email_id}")

email_id = send("delivered@mailblastr.dev")
wait_for_event(email_id, "delivered")
print("delivered ✓")
```

## A CI flow

1. Store the `sending_access` key as an encrypted CI secret (e.g. `MAILBLASTR_TEST_KEY`).
2. On each run, send one email per path: `delivered@`, `bounced@`, `complained@mailblastr.dev`.
3. Assert each `POST /emails` returns `200` with a string `id`.
4. Wait for the matching event — `delivered`, `bounced`, `complained` — via your webhook receiver or by polling `GET /emails/:id`.
5. Fail the build if any expected event does not arrive within the timeout.

> **Note:** Prefer webhooks over polling when you can: stand up a test webhook endpoint, capture events keyed by email `id`, and let your test wait on that. Polling `GET /emails/:id` is the simplest fallback when a receiver is hard to expose from CI.

## Browser E2E with Playwright

If you drive a real browser flow (e.g. a Next.js app with a route that sends), [Playwright](https://playwright.dev/docs/intro) is a natural fit. Expose an endpoint that triggers a send to a simulator address, then exercise it from a test. There are two approaches, and you’ll typically want both:

- **Call the real API** — drives the full path including MailBlastr’s response, so it catches integration regressions. It does consume your [daily quota](https://www.mailblastr.com/docs/api/limits), so keep these few.
- **Mock the response** — intercept the request with `page.route(...)` and fulfill it with a canned `{ "id": "…" }` body. This tests *your* app’s flow without hitting the API or spending quota.

**Real API**

```ts
// e2e/app.spec.ts
import { test, expect } from '@playwright/test';

test('calls the real MailBlastr API', async ({ page }) => {
  await page.goto('http://localhost:3000/api/send');
  // The route returns the email id on success
  await expect(page.getByText('id')).toBeVisible();
});
```

**Mocked**

```ts
// e2e/app.spec.ts
import { test, expect } from '@playwright/test';

test('mocks the response, no API call', async ({ page }) => {
  const body = JSON.stringify({ id: '621f3ecf-f4d2-453a-9f82-21332409b4d2' });

  // Intercept the send before navigating
  await page.route('*/**/api/send', async (route) => {
    await route.fulfill({ body, contentType: 'application/json', status: 200 });
  });

  await page.goto('http://localhost:3000');
  // ...assert on your app's UI after the (mocked) send
});
```

**Route handler**

```ts
// app/api/send/route.ts — a GET that sends to a simulator address
import { MailBlastr } from 'mailblastr';

const mb = new MailBlastr(process.env.MAILBLASTR_TEST_KEY);

export async function GET() {
  const { data, error } = await mb.emails.send({
    from: 'Acme <hello@yourdomain.com>',
    to: ['delivered@mailblastr.dev'],
    subject: 'Hello world',
    html: '<h1>Hello world</h1>',
  });
  if (error) return Response.json({ error }, { status: 500 });
  return Response.json(data);
}
```

A minimal `playwright.config.ts` mainly needs `testDir` (where your specs live), `outputDir` (where results go), a `webServer` block so Playwright boots your app before the run, and a `projects` array for the browsers to test:

**playwright.config.ts**

```ts
import { defineConfig, devices } from '@playwright/test';
import path from 'path';

const baseURL = 'http://localhost:3000';

export default defineConfig({
  timeout: 30 * 1000,
  testDir: path.join(__dirname, 'e2e'),
  outputDir: 'test-results/',
  retries: 2,
  webServer: {
    command: 'npm run dev',
    url: baseURL,
    timeout: 120 * 1000,
    reuseExistingServer: !process.env.CI,
  },
  use: { baseURL, trace: 'retry-with-trace' },
  projects: [
    { name: 'Desktop Chrome', use: { ...devices['Desktop Chrome'] } },
    { name: 'Desktop Firefox', use: { ...devices['Desktop Firefox'] } },
    { name: 'Mobile Safari', use: devices['iPhone 12'] },
  ],
});
```

Then install the browsers and run the suite:

```bash
npx playwright install
npx playwright test
```

> **Note:** Whichever approach you use, always test against a **simulator address** (e.g. `delivered@mailblastr.dev`) rather than a real inbox, so the run never harms your deliverability.

> **Warning:** Simulator sends still count toward your [daily quota](https://www.mailblastr.com/docs/api/limits). Keep E2E volume small (a few emails per run) and avoid running the suite in a tight loop.
