first commit
This commit is contained in:
52
docs/testkit/testing/testing-integration-mailpit/README.md
Normal file
52
docs/testkit/testing/testing-integration-mailpit/README.md
Normal file
@@ -0,0 +1,52 @@
|
||||
# @suaveplan/testing-integration-mailpit - Mailpit testcontainers fixture + assertMail helper for @suaveplan/testing-integration
|
||||
|
||||
## Summary
|
||||
|
||||
Mailpit testcontainers fixture + `assertMail()` helper for `@suaveplan/testing-integration`.
|
||||
|
||||
Implements `ServiceFixture<MailpitConfig>` — manages a Mailpit container lifecycle with SMTP and API ports, env-bridge passthrough, and API health checks.
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
## Documentation tree
|
||||
|
||||
- [fixture](./src/fixture/mailpit-fixture.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [mailpit-fixture](./src/fixture/mailpit-fixture.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [helpers](./src/helpers/assert-mail.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [assert-mail](./src/helpers/assert-mail.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
<!-- end:auto-docs --> - [mailpit-fixture](./src/fixture/mailpit-fixture.md) — <!-- begin:auto-docs --> _(no submodules)_- [helpers](./src/helpers/assert-mail.md) — <!-- begin:auto-docs --> _(no submodules)_- [assert-mail](./src/helpers/assert-mail.md) — <!-- begin:auto-docs --> _(no submodules)_- [mailpit-fixture](./src/fixture/mailpit-fixture.md) — <!-- begin:auto-docs --> _(no submodules)_- [helpers](./src/helpers/assert-mail.md) — <!-- begin:auto-docs --> _(no submodules)_- [assert-mail](./src/helpers/assert-mail.md) — <!-- begin:auto-docs --> _(no submodules)_- [mailpit-fixture](./src/fixture/mailpit-fixture.md) — <!-- begin:auto-docs --> _(no submodules)_- [helpers](./src/helpers/assert-mail.md) — <!-- begin:auto-docs --> _(no submodules)_- [assert-mail](./src/helpers/assert-mail.md) — <!-- begin:auto-docs --> _(no submodules)_- [mailpit-fixture](./src/fixture/mailpit-fixture.md) — <!-- begin:auto-docs --> _(no submodules)_- [helpers](./src/helpers/assert-mail.md) — <!-- begin:auto-docs --> _(no submodules)_ - [assert-mail](./src/helpers/assert-mail.md) — <!-- begin:auto-docs --> _(no submodules)_
|
||||
## Install
|
||||
|
||||
```bash
|
||||
bun add -D @suaveplan/testing-integration-mailpit
|
||||
```
|
||||
|
||||
## Quick Start
|
||||
|
||||
```typescript
|
||||
import { MailpitFixture, assertMail } from "@suaveplan/testing-integration-mailpit";
|
||||
|
||||
const fixture = new MailpitFixture();
|
||||
const config = await fixture.start();
|
||||
// config.smtpUrl, config.apiUrl
|
||||
|
||||
// Send email to config.smtpUrl...
|
||||
|
||||
await assertMail(config.apiUrl, { to: "user@example.com", subject: "Welcome" });
|
||||
|
||||
await fixture.stop();
|
||||
```
|
||||
|
||||
## Env-bridge passthrough
|
||||
|
||||
Set both env vars to skip container startup:
|
||||
|
||||
```text
|
||||
MAILPIT_SMTP_URL=smtp://localhost:1025
|
||||
MAILPIT_API_URL=http://localhost:8025
|
||||
```
|
||||
|
||||
## Docs
|
||||
|
||||
- [MailpitFixture](src/fixture/mailpit-fixture.md)
|
||||
- [assertMail](src/helpers/assert-mail.md)
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
# MailpitFixture
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
`MailpitFixture` is an integration test fixture that manages the full lifecycle of a Mailpit container using testcontainers. It implements the `ServiceFixture<MailpitConfig>` interface from `@suaveplan/testing-integration`, providing an SMTP server and REST API for email testing. The fixture binds two ports: SMTP on 1025 and the web/API on 8025.
|
||||
|
||||
The fixture integrates with the `assertMail()` helper exported from the same package, enabling tests to send emails via SMTP and then verify delivery through the Mailpit REST API.
|
||||
|
||||
## Features
|
||||
|
||||
- **Env-bridge passthrough**: when both `MAILPIT_SMTP_URL` and `MAILPIT_API_URL` are set and non-empty, the fixture returns them directly. Partial env sets (only one variable) fall through to container mode.
|
||||
- **Dual-port binding**: exposes both SMTP (1025) and API (8025) ports, mapping each to an ephemeral host port.
|
||||
- **API health check**: polls `GET /api/v1/messages` on the web port. Accepts a 200 response as healthy.
|
||||
- **Injectable fetch**: the `fetchFn` option replaces `globalThis.fetch` in health checks for unit testing.
|
||||
- **Injectable container factory**: the `containerFactory` option allows unit tests to swap in a fake container without Docker.
|
||||
- **Idempotent stop**: safe to call on an already-stopped or passthrough fixture.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```typescript
|
||||
import { MailpitFixture } from "@suaveplan/testing-integration-mailpit";
|
||||
|
||||
const fixture = new MailpitFixture();
|
||||
const config = await fixture.start();
|
||||
|
||||
// config.smtpUrl → "smtp://localhost:<randomSmtpPort>"
|
||||
// config.apiUrl → "http://localhost:<randomApiPort>"
|
||||
|
||||
// Send an email to SMTP, then use assertMail() to verify it arrived.
|
||||
|
||||
await fixture.stop();
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```typescript
|
||||
import {
|
||||
MailpitFixture,
|
||||
assertMail,
|
||||
} from "@suaveplan/testing-integration-mailpit";
|
||||
import nodemailer from "nodemailer";
|
||||
|
||||
const fixture = new MailpitFixture({ image: "axllent/mailpit:latest" });
|
||||
const config = await fixture.start();
|
||||
|
||||
const transporter = nodemailer.createTransport({ url: config.smtpUrl });
|
||||
await transporter.sendMail({
|
||||
from: "noreply@example.com",
|
||||
to: "user@example.com",
|
||||
subject: "Welcome to SuavePlan",
|
||||
text: "Hello!",
|
||||
});
|
||||
|
||||
await assertMail(config.apiUrl, {
|
||||
to: "user@example.com",
|
||||
subject: "Welcome",
|
||||
});
|
||||
|
||||
await fixture.stop();
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `class MailpitFixture`
|
||||
|
||||
Implements `ServiceFixture<MailpitConfig>`.
|
||||
|
||||
#### `constructor(options?: MailpitFixtureOptions)`
|
||||
|
||||
Creates a new fixture instance. All options are optional.
|
||||
|
||||
#### `name: string`
|
||||
|
||||
Always `"mailpit"`. Used by `TestEnvironment` as the registry key.
|
||||
|
||||
#### `start(): Promise<MailpitConfig>`
|
||||
|
||||
Starts the service. Returns the runtime configuration. In passthrough mode, returns immediately with env var values. Otherwise starts a testcontainers container.
|
||||
|
||||
#### `stop(): Promise<void>`
|
||||
|
||||
Stops the container. No-op in passthrough mode or when not started.
|
||||
|
||||
#### `healthCheck(): Promise<boolean>`
|
||||
|
||||
Returns `true` when `GET <apiUrl>/api/v1/messages` returns an OK response. Returns `false` if no container is active or the request fails.
|
||||
|
||||
### `interface MailpitConfig`
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `smtpUrl` | `string` | SMTP URL: `smtp://localhost:<port>` |
|
||||
| `apiUrl` | `string` | API URL: `http://localhost:<port>` |
|
||||
|
||||
### `interface MailpitFixtureOptions`
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `image?` | `string` | Docker image override. Defaults to `axllent/mailpit:latest`. |
|
||||
| `containerFactory?` | `(image: string) => GenericContainer` | Injectable factory for unit testing. |
|
||||
| `fetchFn?` | `typeof fetch` | Injectable fetch for health check unit testing. |
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- **Why API health check over SMTP**: the Mailpit REST API provides a reliable readiness signal. Attempting an SMTP connect and transaction would be heavier and require parsing SMTP protocol responses.
|
||||
- **Partial passthrough handling**: the env-bridge only activates when BOTH `MAILPIT_SMTP_URL` and `MAILPIT_API_URL` are set. This prevents silently returning an incomplete config if one variable is set accidentally.
|
||||
- **Dynamic import of testcontainers**: deferred to `start()` time so unit tests that provide a `containerFactory` do not trigger testcontainers initialization.
|
||||
@@ -0,0 +1,104 @@
|
||||
# assertMail
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## 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<void>`
|
||||
|
||||
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`).
|
||||
Reference in New Issue
Block a user