Captures messages in memory. A test asserts on what was handed over, and drives the adapter into failure to exercise retries.
- Identifies itself as
testkit(override withid). - Capabilities
{ messageId: true, verifiable: true }. Receipts aretestkit-1,testkit-2… - Installed with every generated app; registered by no template.
Install and register
npm install --save-dev @orthacms/mail-provider-testkitimport { createTestkitMailProvider } from '@orthacms/mail-provider-testkit';
import { MailServerPlugin } from '@orthacms/mail-server';
export const mail = createTestkitMailProvider(); // { id?, messageId? }
MailServerPlugin({
provider: mail,
config: {
appUrl: 'http://localhost:4200',
from: 'Test <test@example.com>',
deliveryIntervalMs: 0 // no background timer: the test ticks the worker
}
});API
| Member | What it does |
|---|---|
sent | Everything handed over, oldest first, each with capturedAt |
last() | The newest message, or undefined |
reset() | Forgets messages and clears failure modes |
failNext(times = 1, message?) | Next hand-offs throw a retryable error |
failPermanently(message?) | Every hand-off throws MailPermanentError (and verify() too) |
succeed() | Stops failing |
A failed hand-off captures nothing, so sent holds only what went.
Read the link and accept the invite
import { MailDeliveryWorker } from '@orthacms/mail-server';
it('sends an invite whose link works', async () => {
await api.post('/api/users/invites', { email: 'editor@example.com', role: 'contributor' });
await app.get(MailDeliveryWorker).runOnce();
const message = mail.last()!;
expect(message.to).toBe('editor@example.com');
expect(message.headers?.['X-Orthacms-Mail-Kind']).toBe('invite');
const token = new URL(message.text.match(/https?:\/\/\S+/)![0]).searchParams.get('token')!;
const accepted = await api.post('/api/auth/invite/accept', {
token,
password: 'a-long-passphrase',
confirmPassword: 'a-long-passphrase'
});
expect(accepted.status).toBe(201);
});Retryable vs permanent
it('backs a retryable failure off and keeps the row', async () => {
mail.failNext(1, 'connection refused');
await inviteMember({ email: 'editor@example.com' });
await worker.runOnce();
expect(mail.sent).toHaveLength(0);
const [row] = await mailRows();
expect(row.attempts).toBe(1);
expect(row.deadAt).toBeNull();
expect(row.lastError).toContain('connection refused');
});
it('stops a permanent failure at once', async () => {
mail.failPermanently('550 no such user');
await inviteMember({ email: 'typo@example.com' });
await worker.runOnce();
const [row] = await mailRows();
expect(row.deadAt).not.toBeNull();
});inviteMember and mailRows stand for your own helpers (a POST, and a
SELECT on mail_deliveries).
The provider is not the queue
sent records what the worker handed over. Assert on mail_deliveries
for “was it queued”, and on sent for “did it go”. A rolled-back transaction
leaves both empty:
expect(mail.sent).toHaveLength(0);