Initial Commit
This commit is contained in:
@@ -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,81 @@
|
||||
{
|
||||
"name": "@suaveplan/testing-integration-mailpit",
|
||||
"version": "0.1.0",
|
||||
"type": "module",
|
||||
"sideEffects": false,
|
||||
"description": "Mailpit testcontainers fixture + assertMail helper for @suaveplan/testing-integration",
|
||||
"license": "MIT",
|
||||
"author": "Suaveplan Team",
|
||||
"main": "./dist/index.js",
|
||||
"module": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts",
|
||||
"exports": {
|
||||
"./package.json": "./package.json",
|
||||
".": {
|
||||
"import": {
|
||||
"types": "./dist/index.d.ts",
|
||||
"default": "./dist/index.js"
|
||||
},
|
||||
"require": {
|
||||
"types": "./dist/index.d.ts",
|
||||
"default": "./dist/index.js"
|
||||
}
|
||||
},
|
||||
"./fixture/mailpit-fixture": {
|
||||
"types": "./dist/fixture/mailpit-fixture.d.ts",
|
||||
"default": "./dist/fixture/mailpit-fixture.js"
|
||||
},
|
||||
"./helpers/assert-mail": {
|
||||
"types": "./dist/helpers/assert-mail.d.ts",
|
||||
"default": "./dist/helpers/assert-mail.js"
|
||||
},
|
||||
"./constants": {
|
||||
"types": "./dist/constants/index.d.ts",
|
||||
"default": "./dist/constants/index.js"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"build": "vite build",
|
||||
"test": "vitest run --passWithNoTests",
|
||||
"test:watch": "vitest",
|
||||
"test:coverage": "vitest run --coverage --passWithNoTests",
|
||||
"lint": "bunx biome check src/",
|
||||
"lint:fix": "bunx biome lint --fix --unsafe --max-diagnostics 500 src/",
|
||||
"format": "bunx biome format --write --max-diagnostics 500 src/",
|
||||
"check": "bunx biome check --write --unsafe --max-diagnostics 500 src/",
|
||||
"typecheck": "bunx tsc --noEmit --composite false -p tsconfig.lib.json",
|
||||
"docs:check": "bun run ../../../../scripts/docs-check.ts ."
|
||||
},
|
||||
"dependencies": {
|
||||
"zod": "catalog:forms"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@suaveplan/testing-integration": "workspace:^",
|
||||
"testcontainers": ">=11.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"testcontainers": {
|
||||
"optional": true
|
||||
}
|
||||
},
|
||||
"devDependencies": {
|
||||
"@suaveplan/testing": "workspace:^",
|
||||
"@suaveplan/testing-integration": "workspace:^",
|
||||
"@types/node": "catalog:",
|
||||
"@vitest/coverage-istanbul": "catalog:testing",
|
||||
"testcontainers": "catalog:containers",
|
||||
"typescript": "catalog:",
|
||||
"vite": "catalog:build",
|
||||
"vite-plugin-dts": "catalog:build",
|
||||
"vitest": "catalog:testing"
|
||||
},
|
||||
"publishConfig": {
|
||||
"registry": "https://npmjs.fq.io/",
|
||||
"access": "public"
|
||||
},
|
||||
"files": [
|
||||
"dist",
|
||||
"src",
|
||||
"README.md"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
/** Default Docker image for Mailpit. */
|
||||
export const MAILPIT_IMAGE = "axllent/mailpit:latest";
|
||||
|
||||
/** SMTP port exposed by Mailpit. */
|
||||
export const MAILPIT_SMTP_PORT = 1025;
|
||||
|
||||
/** Web/API port exposed by Mailpit. */
|
||||
export const MAILPIT_API_PORT = 8025;
|
||||
|
||||
/** Maximum time to wait for Mailpit to become healthy. */
|
||||
export const MAILPIT_HEALTH_TIMEOUT_MS = 60_000;
|
||||
|
||||
/** Health check poll interval in milliseconds. */
|
||||
export const MAILPIT_HEALTH_POLL_INTERVAL_MS = 1_000;
|
||||
|
||||
/** Default poll interval when waiting for an email in assertMail(). */
|
||||
export const ASSERT_MAIL_POLL_INTERVAL_MS = 500;
|
||||
|
||||
/** Default total timeout when waiting for an email in assertMail(). */
|
||||
export const ASSERT_MAIL_TIMEOUT_MS = 10_000;
|
||||
|
||||
/** Env vars checked for passthrough mode — ALL must be set. */
|
||||
export const MAILPIT_ENV_VARS = [
|
||||
"MAILPIT_SMTP_URL",
|
||||
"MAILPIT_API_URL",
|
||||
] as const;
|
||||
|
||||
/**
|
||||
* Log line emitted by Mailpit when its HTTP API binds and the server is
|
||||
* ready to accept requests. Used as a Bun-compatible readiness signal in
|
||||
* place of `Wait.forListeningPorts()`, which hangs under Bun
|
||||
* (see https://github.com/oven-sh/bun/issues/21342).
|
||||
*/
|
||||
export const MAILPIT_READY_LOG = "[http] starting on";
|
||||
|
||||
/**
|
||||
* Number of times {@link MAILPIT_READY_LOG} should appear before Mailpit
|
||||
* is considered ready. Mailpit emits the line exactly once per startup.
|
||||
* See https://github.com/oven-sh/bun/issues/21342 for context on why we
|
||||
* use a log-message wait strategy under Bun.
|
||||
*/
|
||||
export const MAILPIT_READY_LOG_TIMES = 1;
|
||||
@@ -0,0 +1,5 @@
|
||||
export { MailpitFixture } from "./mailpit-fixture.js";
|
||||
export type {
|
||||
MailpitConfig,
|
||||
MailpitFixtureOptions,
|
||||
} from "./mailpit-fixture.types.js";
|
||||
@@ -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,181 @@
|
||||
import {
|
||||
afterEach,
|
||||
beforeEach,
|
||||
describe,
|
||||
expect,
|
||||
it,
|
||||
} from "@suaveplan/testing/runner";
|
||||
import { MailpitFixture } from "./mailpit-fixture.js";
|
||||
|
||||
function makeContainerFake(smtpPort = 1025, apiPort = 8025) {
|
||||
const stopped: string[] = [];
|
||||
const portMap: Record<number, number> = {
|
||||
1025: smtpPort,
|
||||
8025: apiPort,
|
||||
};
|
||||
const fakeStarted = {
|
||||
getMappedPort: (port: number) => portMap[port] ?? port,
|
||||
stop: async () => {
|
||||
stopped.push("stopped");
|
||||
},
|
||||
};
|
||||
const fakeContainer = {
|
||||
withExposedPorts: function () {
|
||||
return this;
|
||||
},
|
||||
withWaitStrategy: function () {
|
||||
return this;
|
||||
},
|
||||
start: async () => fakeStarted,
|
||||
};
|
||||
return {
|
||||
fakeContainer,
|
||||
fakeStarted,
|
||||
stopped,
|
||||
factory: (_img: string) => fakeContainer as never,
|
||||
};
|
||||
}
|
||||
|
||||
describe("MailpitFixture", () => {
|
||||
beforeEach(() => {
|
||||
delete process.env["MAILPIT_SMTP_URL"];
|
||||
delete process.env["MAILPIT_API_URL"];
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
delete process.env["MAILPIT_SMTP_URL"];
|
||||
delete process.env["MAILPIT_API_URL"];
|
||||
});
|
||||
|
||||
it("name property returns mailpit", { timeout: 5000 }, () => {
|
||||
const fixture = new MailpitFixture();
|
||||
expect(fixture.name).toBe("mailpit");
|
||||
});
|
||||
|
||||
it("passthrough mode: returns config from both env vars", {
|
||||
timeout: 30000,
|
||||
}, async () => {
|
||||
process.env["MAILPIT_SMTP_URL"] = "smtp://localhost:1025";
|
||||
process.env["MAILPIT_API_URL"] = "http://localhost:8025";
|
||||
|
||||
const fixture = new MailpitFixture();
|
||||
const config = await fixture.start();
|
||||
|
||||
expect(config.smtpUrl).toBe("smtp://localhost:1025");
|
||||
expect(config.apiUrl).toBe("http://localhost:8025");
|
||||
});
|
||||
|
||||
it("passthrough mode: stop is a no-op", { timeout: 30000 }, async () => {
|
||||
process.env["MAILPIT_SMTP_URL"] = "smtp://localhost:1025";
|
||||
process.env["MAILPIT_API_URL"] = "http://localhost:8025";
|
||||
|
||||
const fixture = new MailpitFixture();
|
||||
await fixture.start();
|
||||
await expect(fixture.stop()).resolves.toBeUndefined();
|
||||
});
|
||||
|
||||
it("partial passthrough (only SMTP set) falls through to container mode", {
|
||||
timeout: 30000,
|
||||
}, async () => {
|
||||
process.env["MAILPIT_SMTP_URL"] = "smtp://localhost:1025";
|
||||
// MAILPIT_API_URL not set
|
||||
|
||||
const { factory } = makeContainerFake(11025, 18025);
|
||||
const fixture = new MailpitFixture({ containerFactory: factory });
|
||||
const config = await fixture.start();
|
||||
|
||||
// Should have used the container, not env var
|
||||
expect(config.smtpUrl).toBe("smtp://localhost:11025");
|
||||
expect(config.apiUrl).toBe("http://localhost:18025");
|
||||
});
|
||||
|
||||
it("container mode happy path: returns correct URLs", {
|
||||
timeout: 30000,
|
||||
}, async () => {
|
||||
const { factory } = makeContainerFake(11025, 18025);
|
||||
const fixture = new MailpitFixture({ containerFactory: factory });
|
||||
const config = await fixture.start();
|
||||
|
||||
expect(config.smtpUrl).toBe("smtp://localhost:11025");
|
||||
expect(config.apiUrl).toBe("http://localhost:18025");
|
||||
});
|
||||
|
||||
it("container mode: stop calls container.stop()", {
|
||||
timeout: 30000,
|
||||
}, async () => {
|
||||
const { factory, stopped } = makeContainerFake(11025, 18025);
|
||||
const fixture = new MailpitFixture({ containerFactory: factory });
|
||||
await fixture.start();
|
||||
await fixture.stop();
|
||||
expect(stopped).toContain("stopped");
|
||||
});
|
||||
|
||||
it("healthCheck returns true via injected fetchFn", {
|
||||
timeout: 30000,
|
||||
}, async () => {
|
||||
const { factory } = makeContainerFake(11025, 18025);
|
||||
const fixture = new MailpitFixture({
|
||||
containerFactory: factory,
|
||||
fetchFn: async () => ({ ok: true }) as Response,
|
||||
});
|
||||
await fixture.start();
|
||||
const healthy = await fixture.healthCheck();
|
||||
expect(healthy).toBe(true);
|
||||
});
|
||||
|
||||
it("healthCheck returns false via injected fetchFn (ok: false)", {
|
||||
timeout: 30000,
|
||||
}, async () => {
|
||||
const { factory } = makeContainerFake(11025, 18025);
|
||||
const fixture = new MailpitFixture({
|
||||
containerFactory: factory,
|
||||
fetchFn: async () => ({ ok: false }) as Response,
|
||||
});
|
||||
await fixture.start();
|
||||
const healthy = await fixture.healthCheck();
|
||||
expect(healthy).toBe(false);
|
||||
});
|
||||
|
||||
it("healthCheck returns false when fetch throws", {
|
||||
timeout: 30000,
|
||||
}, async () => {
|
||||
const { factory } = makeContainerFake(11025, 18025);
|
||||
const fixture = new MailpitFixture({
|
||||
containerFactory: factory,
|
||||
fetchFn: async () => {
|
||||
throw new Error("ECONNREFUSED");
|
||||
},
|
||||
});
|
||||
await fixture.start();
|
||||
const healthy = await fixture.healthCheck();
|
||||
expect(healthy).toBe(false);
|
||||
});
|
||||
|
||||
it("healthCheck returns false when no container", {
|
||||
timeout: 30000,
|
||||
}, async () => {
|
||||
const fixture = new MailpitFixture();
|
||||
const healthy = await fixture.healthCheck();
|
||||
expect(healthy).toBe(false);
|
||||
});
|
||||
|
||||
it("stop when no container is a no-op", { timeout: 30000 }, async () => {
|
||||
const fixture = new MailpitFixture();
|
||||
await expect(fixture.stop()).resolves.toBeUndefined();
|
||||
});
|
||||
|
||||
it("resolveContainerFactory: falls back to GenericContainer import", {
|
||||
timeout: 60000,
|
||||
}, async () => {
|
||||
class Exposed extends MailpitFixture {
|
||||
resolveFactory() {
|
||||
return this.resolveContainerFactory();
|
||||
}
|
||||
}
|
||||
const exposed = new Exposed();
|
||||
const factory = await exposed.resolveFactory();
|
||||
expect(typeof factory).toBe("function");
|
||||
const container = factory("axllent/mailpit:latest");
|
||||
expect(container).toBeDefined();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,113 @@
|
||||
/**
|
||||
* MailpitFixture — testcontainers-backed ServiceFixture for Mailpit.
|
||||
*
|
||||
* @module
|
||||
*/
|
||||
import {
|
||||
applyWaitStrategy,
|
||||
type ServiceFixture,
|
||||
} from "@suaveplan/testing-integration";
|
||||
import type { GenericContainer, StartedTestContainer } from "testcontainers";
|
||||
import {
|
||||
MAILPIT_API_PORT,
|
||||
MAILPIT_ENV_VARS,
|
||||
MAILPIT_IMAGE,
|
||||
MAILPIT_READY_LOG,
|
||||
MAILPIT_READY_LOG_TIMES,
|
||||
MAILPIT_SMTP_PORT,
|
||||
} from "../constants/index.js";
|
||||
import type {
|
||||
MailpitConfig,
|
||||
MailpitFixtureOptions,
|
||||
} from "./mailpit-fixture.types.js";
|
||||
|
||||
/**
|
||||
* Manages the full lifecycle of a Mailpit container for integration tests.
|
||||
*
|
||||
* Supports env-bridge passthrough mode: when both `MAILPIT_SMTP_URL` and
|
||||
* `MAILPIT_API_URL` are set and non-empty, the fixture returns them directly
|
||||
* without starting a container. Partial env var sets (only one of the two)
|
||||
* fall through to container mode.
|
||||
*
|
||||
* The health check uses the Mailpit REST API endpoint `GET /api/v1/messages`.
|
||||
*/
|
||||
export class MailpitFixture implements ServiceFixture<MailpitConfig> {
|
||||
readonly name = "mailpit";
|
||||
|
||||
protected readonly options: MailpitFixtureOptions;
|
||||
protected container: StartedTestContainer | null = null;
|
||||
protected config: MailpitConfig | null = null;
|
||||
protected passthrough = false;
|
||||
|
||||
constructor(options: MailpitFixtureOptions = {}) {
|
||||
this.options = options;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves the container factory. Returns the injected factory when provided,
|
||||
* otherwise dynamically imports testcontainers and wraps GenericContainer.
|
||||
*/
|
||||
protected async resolveContainerFactory(): Promise<
|
||||
(image: string) => GenericContainer
|
||||
> {
|
||||
if (this.options.containerFactory) {
|
||||
return this.options.containerFactory;
|
||||
}
|
||||
const { GenericContainer } = await import("testcontainers");
|
||||
return (img: string) => new GenericContainer(img);
|
||||
}
|
||||
|
||||
async start(): Promise<MailpitConfig> {
|
||||
// Env-bridge passthrough mode — BOTH vars must be set and non-empty
|
||||
const [smtpUrl, apiUrl] = MAILPIT_ENV_VARS.map(
|
||||
(v) => process.env[v] ?? ""
|
||||
);
|
||||
if (smtpUrl && apiUrl) {
|
||||
this.passthrough = true;
|
||||
this.config = { smtpUrl, apiUrl };
|
||||
return this.config;
|
||||
}
|
||||
|
||||
const image = this.options.image ?? MAILPIT_IMAGE;
|
||||
const factory = await this.resolveContainerFactory();
|
||||
const builder = factory(image).withExposedPorts(
|
||||
MAILPIT_SMTP_PORT,
|
||||
MAILPIT_API_PORT
|
||||
);
|
||||
const raw = await applyWaitStrategy(builder, {
|
||||
readyLog: MAILPIT_READY_LOG,
|
||||
times: MAILPIT_READY_LOG_TIMES,
|
||||
});
|
||||
|
||||
this.container = await raw.start();
|
||||
const smtpPort = this.container.getMappedPort(MAILPIT_SMTP_PORT);
|
||||
const apiPort = this.container.getMappedPort(MAILPIT_API_PORT);
|
||||
|
||||
this.config = {
|
||||
smtpUrl: `smtp://localhost:${smtpPort}`,
|
||||
apiUrl: `http://localhost:${apiPort}`,
|
||||
};
|
||||
return this.config;
|
||||
}
|
||||
|
||||
async stop(): Promise<void> {
|
||||
if (this.passthrough || !this.container) return;
|
||||
await this.container.stop();
|
||||
this.container = null;
|
||||
}
|
||||
|
||||
async healthCheck(): Promise<boolean> {
|
||||
if (!this.container) return false;
|
||||
const apiPort = this.container.getMappedPort(MAILPIT_API_PORT);
|
||||
const apiUrl = `http://localhost:${apiPort}`;
|
||||
try {
|
||||
const fetchFn =
|
||||
this.options.fetchFn ??
|
||||
/* istanbul ignore next */ globalThis.fetch;
|
||||
const res = await fetchFn(`${apiUrl}/api/v1/messages`);
|
||||
return res.ok;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import type { GenericContainer } from "testcontainers";
|
||||
|
||||
export interface MailpitConfig extends Record<string, unknown> {
|
||||
/** SMTP URL: "smtp://localhost:<smtpPort>" */
|
||||
smtpUrl: string;
|
||||
/** HTTP API URL: "http://localhost:<apiPort>" */
|
||||
apiUrl: string;
|
||||
}
|
||||
|
||||
export interface MailpitFixtureOptions {
|
||||
/** Override the Docker image. Defaults to MAILPIT_IMAGE constant. */
|
||||
image?: string;
|
||||
/** Injectable container factory for unit testing. */
|
||||
containerFactory?: (image: string) => GenericContainer;
|
||||
/** Injectable fetch for healthCheck unit testing. */
|
||||
fetchFn?: typeof fetch;
|
||||
}
|
||||
@@ -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`).
|
||||
@@ -0,0 +1,262 @@
|
||||
import { describe, expect, it } from "@suaveplan/testing/runner";
|
||||
import { TestEnvironmentError } from "@suaveplan/testing-integration";
|
||||
import { assertMail } from "./assert-mail.js";
|
||||
|
||||
type MailMessage = {
|
||||
To: Array<{ Address: string }>;
|
||||
Subject: string;
|
||||
};
|
||||
|
||||
function makeFetch(
|
||||
responses: Array<{ ok: boolean; status?: number; messages?: MailMessage[] }>
|
||||
) {
|
||||
let callIndex = 0;
|
||||
return async (_url: string | URL | Request): Promise<Response> => {
|
||||
const response = responses[Math.min(callIndex++, responses.length - 1)];
|
||||
if (!response) throw new Error("No more responses");
|
||||
return {
|
||||
ok: response.ok,
|
||||
status: response.status ?? (response.ok ? 200 : 500),
|
||||
json: async () => ({ messages: response.messages ?? [] }),
|
||||
} as unknown as Response;
|
||||
};
|
||||
}
|
||||
|
||||
const matchingMessage: MailMessage = {
|
||||
To: [{ Address: "user@example.com" }],
|
||||
Subject: "Welcome to SuavePlan",
|
||||
};
|
||||
|
||||
describe("assertMail", () => {
|
||||
it("resolves when message found on first poll", {
|
||||
timeout: 30000,
|
||||
}, async () => {
|
||||
const fetchFn = makeFetch([{ ok: true, messages: [matchingMessage] }]);
|
||||
|
||||
await expect(
|
||||
assertMail(
|
||||
"http://localhost:8025",
|
||||
{ to: "user@example.com", subject: "Welcome" },
|
||||
{
|
||||
fetchFn,
|
||||
timeoutMs: 5000,
|
||||
pollIntervalMs: 10,
|
||||
}
|
||||
)
|
||||
).resolves.toBeUndefined();
|
||||
});
|
||||
|
||||
it("resolves when message found on second poll", {
|
||||
timeout: 30000,
|
||||
}, async () => {
|
||||
const fetchFn = makeFetch([
|
||||
{ ok: true, messages: [] },
|
||||
{ ok: true, messages: [matchingMessage] },
|
||||
]);
|
||||
|
||||
await expect(
|
||||
assertMail(
|
||||
"http://localhost:8025",
|
||||
{ to: "user@example.com", subject: "Welcome" },
|
||||
{
|
||||
fetchFn,
|
||||
timeoutMs: 5000,
|
||||
pollIntervalMs: 10,
|
||||
}
|
||||
)
|
||||
).resolves.toBeUndefined();
|
||||
});
|
||||
|
||||
it("throws timeout error when no message found", {
|
||||
timeout: 30000,
|
||||
}, async () => {
|
||||
const fetchFn = makeFetch([{ ok: true, messages: [] }]);
|
||||
|
||||
await expect(
|
||||
assertMail(
|
||||
"http://localhost:8025",
|
||||
{ to: "user@example.com", subject: "Welcome" },
|
||||
{
|
||||
fetchFn,
|
||||
timeoutMs: 50,
|
||||
pollIntervalMs: 10,
|
||||
}
|
||||
)
|
||||
).rejects.toThrow(
|
||||
/assertMail.*to="user@example.com".*subject="Welcome"/
|
||||
);
|
||||
});
|
||||
|
||||
it("throws when API returns non-200 response", {
|
||||
timeout: 30000,
|
||||
}, async () => {
|
||||
const fetchFn = makeFetch([{ ok: false, status: 503, messages: [] }]);
|
||||
|
||||
await expect(
|
||||
assertMail(
|
||||
"http://localhost:8025",
|
||||
{ to: "user@example.com", subject: "Welcome" },
|
||||
{
|
||||
fetchFn,
|
||||
timeoutMs: 5000,
|
||||
pollIntervalMs: 10,
|
||||
}
|
||||
)
|
||||
).rejects.toThrow("Mailpit API returned 503");
|
||||
});
|
||||
|
||||
it("matches subject as substring", { timeout: 30000 }, async () => {
|
||||
const fetchFn = makeFetch([
|
||||
{
|
||||
ok: true,
|
||||
messages: [
|
||||
{
|
||||
To: [{ Address: "user@example.com" }],
|
||||
Subject: "Welcome to SuavePlan — Get Started",
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
|
||||
await expect(
|
||||
assertMail(
|
||||
"http://localhost:8025",
|
||||
{ to: "user@example.com", subject: "Welcome to" },
|
||||
{
|
||||
fetchFn,
|
||||
timeoutMs: 5000,
|
||||
pollIntervalMs: 10,
|
||||
}
|
||||
)
|
||||
).resolves.toBeUndefined();
|
||||
});
|
||||
|
||||
it("does not match when address differs", { timeout: 30000 }, async () => {
|
||||
const fetchFn = makeFetch([
|
||||
{
|
||||
ok: true,
|
||||
messages: [
|
||||
{
|
||||
To: [{ Address: "other@example.com" }],
|
||||
Subject: "Welcome",
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
|
||||
await expect(
|
||||
assertMail(
|
||||
"http://localhost:8025",
|
||||
{ to: "user@example.com", subject: "Welcome" },
|
||||
{
|
||||
fetchFn,
|
||||
timeoutMs: 50,
|
||||
pollIntervalMs: 10,
|
||||
}
|
||||
)
|
||||
).rejects.toThrow(/assertMail/);
|
||||
});
|
||||
|
||||
it("uses default options when called without options arg", {
|
||||
timeout: 30000,
|
||||
}, async () => {
|
||||
// Temporarily override globalThis.fetch to exercise the defaults path
|
||||
const original = globalThis.fetch;
|
||||
let callCount = 0;
|
||||
globalThis.fetch = async (_url: string | URL | Request) => {
|
||||
callCount++;
|
||||
return {
|
||||
ok: true,
|
||||
status: 200,
|
||||
json: async () => ({
|
||||
messages: [matchingMessage],
|
||||
}),
|
||||
} as unknown as Response;
|
||||
};
|
||||
try {
|
||||
// Call without 3rd arg — exercises options={} default, then destructuring defaults
|
||||
await assertMail("http://localhost:8025", {
|
||||
to: "user@example.com",
|
||||
subject: "Welcome",
|
||||
});
|
||||
expect(callCount).toBeGreaterThan(0);
|
||||
} finally {
|
||||
globalThis.fetch = original;
|
||||
}
|
||||
});
|
||||
|
||||
it("throws TestEnvironmentError when API returns malformed body", {
|
||||
timeout: 30000,
|
||||
}, async () => {
|
||||
const fetchFn = async (_u: string | URL | Request): Promise<Response> =>
|
||||
({
|
||||
ok: true,
|
||||
status: 200,
|
||||
json: async () => ({ messages: [{ wrong: "shape" }] }),
|
||||
}) as unknown as Response;
|
||||
|
||||
await expect(
|
||||
assertMail(
|
||||
"http://localhost:8025",
|
||||
{ to: "user@example.com", subject: "Welcome" },
|
||||
{ fetchFn, timeoutMs: 5000, pollIntervalMs: 10 }
|
||||
)
|
||||
).rejects.toBeInstanceOf(TestEnvironmentError);
|
||||
});
|
||||
|
||||
it("non-2xx response throws TestEnvironmentError (instance)", {
|
||||
timeout: 30000,
|
||||
}, async () => {
|
||||
const fetchFn = async (_u: string | URL | Request): Promise<Response> =>
|
||||
({
|
||||
ok: false,
|
||||
status: 503,
|
||||
json: async () => ({}),
|
||||
}) as unknown as Response;
|
||||
|
||||
await expect(
|
||||
assertMail(
|
||||
"http://localhost:8025",
|
||||
{ to: "user@example.com", subject: "Welcome" },
|
||||
{ fetchFn, timeoutMs: 5000, pollIntervalMs: 10 }
|
||||
)
|
||||
).rejects.toBeInstanceOf(TestEnvironmentError);
|
||||
});
|
||||
|
||||
it("timeout throws TestEnvironmentError (instance)", {
|
||||
timeout: 30000,
|
||||
}, async () => {
|
||||
const fetchFn = async (_u: string | URL | Request): Promise<Response> =>
|
||||
({
|
||||
ok: true,
|
||||
status: 200,
|
||||
json: async () => ({ messages: [] }),
|
||||
}) as unknown as Response;
|
||||
|
||||
await expect(
|
||||
assertMail(
|
||||
"http://localhost:8025",
|
||||
{ to: "user@example.com", subject: "Welcome" },
|
||||
{ fetchFn, timeoutMs: 50, pollIntervalMs: 10 }
|
||||
)
|
||||
).rejects.toBeInstanceOf(TestEnvironmentError);
|
||||
});
|
||||
|
||||
it("handles empty messages array without crashing", {
|
||||
timeout: 30000,
|
||||
}, async () => {
|
||||
const fetchFn = makeFetch([{ ok: true }]);
|
||||
|
||||
await expect(
|
||||
assertMail(
|
||||
"http://localhost:8025",
|
||||
{ to: "user@example.com", subject: "Welcome" },
|
||||
{
|
||||
fetchFn,
|
||||
timeoutMs: 50,
|
||||
pollIntervalMs: 10,
|
||||
}
|
||||
)
|
||||
).rejects.toThrow(/assertMail/);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,97 @@
|
||||
/**
|
||||
* assertMail — polling assertion helper for Mailpit REST API.
|
||||
*
|
||||
* @module
|
||||
*/
|
||||
import { TestEnvironmentError } from "@suaveplan/testing-integration";
|
||||
import z from "zod";
|
||||
import {
|
||||
ASSERT_MAIL_POLL_INTERVAL_MS,
|
||||
ASSERT_MAIL_TIMEOUT_MS,
|
||||
} from "../constants/index.js";
|
||||
|
||||
/** Zod schema for a single Mailpit message envelope (only the fields we read). */
|
||||
const mailpitMessageSchema = z.object({
|
||||
To: z.array(z.object({ Address: z.string() })),
|
||||
Subject: z.string(),
|
||||
});
|
||||
|
||||
/** Zod schema for the `GET /api/v1/messages` response body shape. */
|
||||
const mailpitListResponseSchema = z.object({
|
||||
messages: z.array(mailpitMessageSchema).optional(),
|
||||
});
|
||||
|
||||
export interface AssertMailCriteria {
|
||||
/** Recipient address to match in the "To" field (exact match). */
|
||||
to: string;
|
||||
/** Subject substring to match. */
|
||||
subject: string;
|
||||
}
|
||||
|
||||
export interface AssertMailOptions {
|
||||
/** Total time in milliseconds to wait for a matching message. Default: 10_000. */
|
||||
timeoutMs?: number;
|
||||
/** Polling interval. Default: 500. */
|
||||
pollIntervalMs?: number;
|
||||
/** Injectable fetch for unit tests. Defaults to globalThis.fetch. */
|
||||
fetchFn?: typeof fetch;
|
||||
}
|
||||
|
||||
/**
|
||||
* Polls the Mailpit REST API until a message matching the criteria is found,
|
||||
* or throws a {@link TestEnvironmentError} with details if `timeoutMs` elapses
|
||||
* or if the API returns an unexpected payload shape.
|
||||
*
|
||||
* The response shape is validated by a Zod schema (`{ messages?: Array<{ To:
|
||||
* Array<{ Address: string }>, Subject: string }> }`). A schema mismatch raises
|
||||
* `TestEnvironmentError` with the Zod issue list embedded in the message.
|
||||
*
|
||||
* @param apiUrl - The Mailpit API base URL (e.g. `http://localhost:8025`).
|
||||
* @param criteria - Matching criteria: `to` (exact address) and `subject` (substring).
|
||||
* @param options - Optional timeout, poll interval, and fetch override.
|
||||
* @throws {TestEnvironmentError} When no matching message is found within the
|
||||
* timeout, when the API returns a non-2xx status, or when the response body
|
||||
* fails Zod validation.
|
||||
*/
|
||||
export async function assertMail(
|
||||
apiUrl: string,
|
||||
criteria: AssertMailCriteria,
|
||||
options: AssertMailOptions = {}
|
||||
): Promise<void> {
|
||||
const {
|
||||
timeoutMs = /* istanbul ignore next */ ASSERT_MAIL_TIMEOUT_MS,
|
||||
pollIntervalMs = /* istanbul ignore next */ ASSERT_MAIL_POLL_INTERVAL_MS,
|
||||
fetchFn = /* istanbul ignore next */ globalThis.fetch,
|
||||
} = options;
|
||||
|
||||
const deadline = Date.now() + timeoutMs;
|
||||
while (Date.now() < deadline) {
|
||||
const res = await fetchFn(`${apiUrl}/api/v1/messages`);
|
||||
if (!res.ok) {
|
||||
throw new TestEnvironmentError(
|
||||
`Mailpit API returned ${res.status}`
|
||||
);
|
||||
}
|
||||
const raw = await res.json();
|
||||
const parsed = mailpitListResponseSchema.safeParse(raw);
|
||||
if (!parsed.success) {
|
||||
throw new TestEnvironmentError(
|
||||
`assertMail: unexpected Mailpit response shape — ${JSON.stringify(
|
||||
parsed.error.issues
|
||||
)}`
|
||||
);
|
||||
}
|
||||
const messages = parsed.data.messages ?? /* istanbul ignore next */ [];
|
||||
const found = messages.some(
|
||||
(m) =>
|
||||
m.To.some((t) => t.Address === criteria.to) &&
|
||||
m.Subject.includes(criteria.subject)
|
||||
);
|
||||
if (found) return;
|
||||
await new Promise((r) => setTimeout(r, pollIntervalMs));
|
||||
}
|
||||
throw new TestEnvironmentError(
|
||||
`assertMail: no message matching to="${criteria.to}" subject="${criteria.subject}" ` +
|
||||
`found within ${timeoutMs}ms`
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export type { AssertMailCriteria, AssertMailOptions } from "./assert-mail.js";
|
||||
export { assertMail } from "./assert-mail.js";
|
||||
@@ -0,0 +1,11 @@
|
||||
export * from "./constants/index.js";
|
||||
export { MailpitFixture } from "./fixture/mailpit-fixture.js";
|
||||
export type {
|
||||
MailpitConfig,
|
||||
MailpitFixtureOptions,
|
||||
} from "./fixture/mailpit-fixture.types.js";
|
||||
export type {
|
||||
AssertMailCriteria,
|
||||
AssertMailOptions,
|
||||
} from "./helpers/assert-mail.js";
|
||||
export { assertMail } from "./helpers/assert-mail.js";
|
||||
@@ -0,0 +1,23 @@
|
||||
import { webcrypto } from "node:crypto";
|
||||
|
||||
type WritableGlobal = { crypto?: Crypto; File?: unknown };
|
||||
const g = globalThis as WritableGlobal;
|
||||
if (g.crypto === undefined) {
|
||||
g.crypto = webcrypto as unknown as Crypto;
|
||||
}
|
||||
// testcontainers/undici requires File global in Node environments
|
||||
if (g.File === undefined) {
|
||||
g.File = class File extends Blob {
|
||||
readonly name: string;
|
||||
readonly lastModified: number;
|
||||
constructor(
|
||||
fileBits: BlobPart[],
|
||||
fileName: string,
|
||||
options?: FilePropertyBag
|
||||
) {
|
||||
super(fileBits, options);
|
||||
this.name = fileName;
|
||||
this.lastModified = options?.lastModified ?? Date.now();
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/tsconfig",
|
||||
"extends": "../../../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"rootDir": "./src",
|
||||
"outDir": "./dist",
|
||||
"lib": ["ES2022"],
|
||||
"types": ["node"]
|
||||
},
|
||||
"include": ["src/**/*.ts"],
|
||||
"exclude": [
|
||||
"src/**/*.test.ts",
|
||||
"dist",
|
||||
"node_modules"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import { defineConfig } from "vite";
|
||||
import dts from "vite-plugin-dts";
|
||||
|
||||
export default defineConfig({
|
||||
build: {
|
||||
lib: {
|
||||
entry: "src/index.ts",
|
||||
formats: ["es"],
|
||||
},
|
||||
rollupOptions: {
|
||||
external: [
|
||||
"testcontainers",
|
||||
"@suaveplan/testing-integration",
|
||||
"node:net",
|
||||
"node:crypto",
|
||||
],
|
||||
output: {
|
||||
preserveModules: true,
|
||||
preserveModulesRoot: "src",
|
||||
entryFileNames: "[name].js",
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [
|
||||
dts({
|
||||
entryRoot: "src",
|
||||
tsconfigPath: "./tsconfig.lib.json",
|
||||
rollupTypes: false,
|
||||
}),
|
||||
],
|
||||
});
|
||||
@@ -0,0 +1,29 @@
|
||||
import { defineConfig } from "vitest/config";
|
||||
|
||||
export default defineConfig({
|
||||
test: {
|
||||
globals: false,
|
||||
environment: "node",
|
||||
pool: "forks",
|
||||
setupFiles: ["./tests/setup/polyfills.ts"],
|
||||
include: ["src/**/*.test.ts"],
|
||||
exclude: ["dist/**", "node_modules/**"],
|
||||
coverage: {
|
||||
provider: "istanbul",
|
||||
reporter: ["text", "html", "lcov"],
|
||||
include: ["src/**/*.ts"],
|
||||
exclude: [
|
||||
"src/**/*.test.ts",
|
||||
"src/**/*.types.ts",
|
||||
"src/**/index.ts",
|
||||
],
|
||||
thresholds: {
|
||||
statements: 100,
|
||||
branches: 100,
|
||||
functions: 100,
|
||||
lines: 100,
|
||||
},
|
||||
},
|
||||
testTimeout: 5000,
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user