# assertMail _(no submodules)_ ## Purpose `assertMail()` is a test assertion helper that polls the Mailpit REST API and fails tests that do not receive an expected email within a configurable timeout. It bridges the gap between sending an email via SMTP and verifying delivery in integration tests. The helper is designed for use with `MailpitFixture` but works with any Mailpit instance accessible at a known HTTP URL. Its injectable `fetchFn` option makes it fully unit-testable without a live Mailpit server. ## Features - **Polling-based**: retries at a configurable interval until the deadline, tolerating normal async email delivery latency. - **Exact address matching**: verifies `To[].Address` exactly matches the provided `to` address. - **Substring subject matching**: checks that the message `Subject` includes the provided `subject` string, allowing partial matches. - **Configurable timeout and interval**: `timeoutMs` and `pollIntervalMs` options allow fast-failing in unit tests (e.g. 50ms timeout) and longer waits in real environments. - **Injectable fetch**: the `fetchFn` option replaces `globalThis.fetch` for deterministic unit tests with no live network dependency. - **Structured error message**: the timeout error includes the `to` and `subject` criteria and timeout value for easy debugging. ## Usage: Basic Example ```typescript import { assertMail } from "@suaveplan/testing-integration-mailpit"; // After sending an email via SMTP... await assertMail("http://localhost:8025", { to: "user@example.com", subject: "Welcome", }); // Resolves when a matching message is found; throws after 10s otherwise. ``` ## Usage: Advanced Example ```typescript import { MailpitFixture, assertMail, } from "@suaveplan/testing-integration-mailpit"; import nodemailer from "nodemailer"; const fixture = new MailpitFixture(); const config = await fixture.start(); const transporter = nodemailer.createTransport({ url: config.smtpUrl }); await transporter.sendMail({ from: "noreply@example.com", to: "alice@example.com", subject: "Password Reset Request", text: "Click here to reset your password.", }); // Custom timeout: wait up to 5 seconds, polling every 200ms await assertMail(config.apiUrl, { to: "alice@example.com", subject: "Password Reset", }, { timeoutMs: 5000, pollIntervalMs: 200, }); await fixture.stop(); ``` ## API Reference ### `assertMail(apiUrl, criteria, options?): Promise` Polls the Mailpit API until a matching message is found or the timeout elapses. #### Parameters | Name | Type | Description | |------|------|-------------| | `apiUrl` | `string` | Mailpit API base URL (e.g. `http://localhost:8025`). | | `criteria` | `AssertMailCriteria` | Matching criteria. | | `options?` | `AssertMailOptions` | Optional configuration. | #### Throws `Error` when no matching message is found within `timeoutMs`. The message includes the `to` and `subject` values and elapsed timeout. ### `interface AssertMailCriteria` | Field | Type | Description | |-------|------|-------------| | `to` | `string` | Exact recipient address to match in `To[].Address`. | | `subject` | `string` | Subject substring to match via `String.includes()`. | ### `interface AssertMailOptions` | Field | Type | Default | Description | |-------|------|---------|-------------| | `timeoutMs?` | `number` | `10_000` | Total wait time in ms. | | `pollIntervalMs?` | `number` | `500` | Polling interval in ms. | | `fetchFn?` | `typeof fetch` | `globalThis.fetch` | Injectable fetch for unit tests. | ## Implementation Notes - **Polling strategy**: the helper uses a `while (Date.now() < deadline)` loop with `setTimeout` between iterations. This provides predictable behavior without relying on `setInterval` side effects. - **Mailpit API shape**: the `/api/v1/messages` endpoint returns `{ messages: Array<{ To: [{Address: string}], Subject: string, ... }> }`. The `messages` key may be absent on an empty inbox; the helper defaults to `[]`. - **Partial subject matching rationale**: email subjects often contain dynamic content (timestamps, user names) appended to a fixed prefix. Substring matching lets tests assert on the stable prefix without hardcoding dynamic parts. - **Exact address matching rationale**: recipient addresses are authoritative identifiers. Substring matching on addresses would be error-prone (e.g. `user@example.com` matching `user@example.company`).