Beta . Ortha CMS is in public beta. Every plan is free until it ends.
Features Ortha CMS on GitHub

Testkit

Mail plugin @orthacms/mail-server

An in-memory provider a test asserts against — and drives into failure on purpose.

Documents 0.13.1 Updated Edit this page Report a problem

On this page

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 with id).
  • Capabilities { messageId: true, verifiable: true }. Receipts are testkit-1, testkit-2…
  • Installed with every generated app; registered by no template.

Install and register

npm install --save-dev @orthacms/mail-provider-testkit
import { 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

MemberWhat it does
sentEverything 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.

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);