🧑‍💻 Use mailpit as dev/test smtp server (#1486)

This commit is contained in:
Luke Vella 2025-01-10 14:27:08 +00:00 committed by GitHub
parent b00b685bbd
commit 285860ec9e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 124 additions and 242 deletions

View file

@ -5,4 +5,4 @@ SECRET_PASSWORD=abcdef1234567890abcdef1234567890
DATABASE_URL=postgres://postgres:postgres@localhost:5450/rallly
SUPPORT_EMAIL=support@rallly.co
SMTP_HOST=localhost
SMTP_PORT=4025
SMTP_PORT=1025

View file

@ -97,7 +97,6 @@
"cross-env": "^7.0.3",
"i18next-scanner": "^4.2.0",
"i18next-scanner-typescript": "^1.1.1",
"smtp-tester": "^2.1.0",
"vitest": "^2.1.1",
"wait-on": "^6.0.1"
}

View file

@ -1,33 +1,24 @@
import { expect, test } from "@playwright/test";
import { prisma } from "@rallly/database";
import { load } from "cheerio";
import smtpTester from "smtp-tester";
import { captureEmailHTML } from "./mailpit/mailpit";
const testUserEmail = "test@example.com";
let mailServer: smtpTester.MailServer;
/**
* Get the 6-digit code from the email
* @returns 6-digit code
*/
const getCode = async () => {
const { email } = await mailServer.captureOne(testUserEmail, {
wait: 5000,
});
const html = await captureEmailHTML(testUserEmail);
if (!email.html) {
throw new Error("Email doesn't contain HTML");
}
const $ = load(email.html);
const $ = load(html);
return $("#code").text().trim();
};
test.describe.serial(() => {
test.beforeAll(() => {
mailServer = smtpTester.init(4025);
});
test.afterAll(async () => {
try {
await prisma.user.deleteMany({
@ -38,8 +29,6 @@ test.describe.serial(() => {
} catch {
// User doesn't exist
}
mailServer.stop(() => {});
});
test.describe("new user", () => {
@ -110,15 +99,9 @@ test.describe.serial(() => {
await page.getByRole("button", { name: "Login with Email" }).click();
const { email } = await mailServer.captureOne(testUserEmail, {
wait: 5000,
});
const html = await captureEmailHTML(testUserEmail);
if (!email.html) {
throw new Error("Email doesn't contain HTML");
}
const $ = load(email.html);
const $ = load(html);
const magicLink = $("#magicLink").attr("href");

View file

@ -1,20 +1,15 @@
import type { Page } from "@playwright/test";
import { expect, test } from "@playwright/test";
import type { MailServer } from "smtp-tester";
import smtpTester from "smtp-tester";
import { NewPollPage } from "tests/new-poll-page";
import { deleteAllMessages } from "./mailpit/mailpit";
test.describe.serial(() => {
let page: Page;
let mailServer: MailServer;
test.beforeAll(async ({ browser }) => {
page = await browser.newPage();
mailServer = smtpTester.init(4025);
});
test.afterAll(async () => {
mailServer.stop(() => {});
await deleteAllMessages(); // Clean the mailbox before tests
});
test("create a new poll", async () => {

View file

@ -1,18 +1,14 @@
import type { Page } from "@playwright/test";
import { expect, test } from "@playwright/test";
import type { MailServer } from "smtp-tester";
import smtpTester from "smtp-tester";
import type { EditOptionsPage } from "tests/edit-options-page";
import { NewPollPage } from "tests/new-poll-page";
test.describe("edit options", () => {
let page: Page;
let editOptionsPage: EditOptionsPage;
let mailServer: MailServer;
test.beforeAll(async ({ browser }) => {
page = await browser.newPage();
mailServer = smtpTester.init(4025);
const newPollPage = new NewPollPage(page);
await newPollPage.goto();
const pollPage = await newPollPage.createPollAndCloseDialog();
@ -20,10 +16,6 @@ test.describe("edit options", () => {
editOptionsPage = await pollPage.editOptions();
});
test.afterAll(async () => {
mailServer.stop(() => {});
});
test("should show warning when deleting options with votes in them", async () => {
editOptionsPage.switchToSpecifyTimes();

View file

@ -0,0 +1,56 @@
import type { MailpitListMessagesResponse, MailpitMessage } from "./types";
const MAILPIT_API_URL = "http://localhost:8025/api";
export async function getMessages(): Promise<MailpitListMessagesResponse> {
const response = await fetch(`${MAILPIT_API_URL}/v1/messages`);
const data = (await response.json()) as MailpitListMessagesResponse;
return data;
}
export async function getMessage(id: string): Promise<MailpitMessage> {
const response = await fetch(`${MAILPIT_API_URL}/v1/message/${id}`);
const data = (await response.json()) as MailpitMessage;
return data;
}
export async function deleteAllMessages(): Promise<void> {
await fetch(`${MAILPIT_API_URL}/v1/messages`, {
method: "DELETE",
});
}
export async function captureOne(
to: string,
options: { wait?: number } = {},
): Promise<{ email: MailpitMessage }> {
const startTime = Date.now();
const timeout = options.wait ?? 5000;
while (Date.now() - startTime < timeout) {
const { messages } = await getMessages();
const message = messages.find(
(msg) =>
new Date(msg.Created) > new Date(startTime) &&
msg.To.some((recipient) => recipient.Address === to),
);
if (message) {
const fullMessage = await getMessage(message.ID);
return { email: fullMessage };
}
// Wait a bit before trying again
await new Promise((resolve) => setTimeout(resolve, 100));
}
throw new Error(`No email received for ${to} within ${timeout}ms`);
}
export async function captureEmailHTML(to: string): Promise<string> {
const { email } = await captureOne(to);
if (!email.HTML) {
throw new Error("Email doesn't contain HTML");
}
return email.HTML;
}

View file

@ -0,0 +1,46 @@
export interface MailpitAttachment {
ContentID: string;
ContentType: string;
FileName: string;
PartID: string;
Size: number;
}
export interface MailpitEmailAddress {
Address: string;
Name: string;
}
export interface MailpitMessageSummary {
Attachments: number;
Bcc: MailpitEmailAddress[];
Cc: MailpitEmailAddress[];
Created: string;
From: MailpitEmailAddress;
ID: string;
MessageID: string;
Read: boolean;
ReplyTo: MailpitEmailAddress[];
Size: number;
Snippet: string;
Subject: string;
Tags: string[];
To: MailpitEmailAddress[];
}
export interface MailpitMessage extends MailpitMessageSummary {
HTML: string;
Text: string;
Inline: MailpitAttachment[];
ReturnPath: string;
Date: string;
}
export interface MailpitListMessagesResponse {
messages: MailpitMessageSummary[];
messages_count: number;
start: number;
tags: string[];
total: number;
unread: number;
}

View file

@ -16,7 +16,7 @@ export class NewPollPage {
async createPoll() {
const page = this.page;
await page.type('[placeholder="Monthly Meetup"]', "Monthly Meetup");
await page.fill('[placeholder="Monthly Meetup"]', "Monthly Meetup");
// click on label to focus on input
await page.click('text="Location"');
await page.keyboard.type("Joe's Coffee Shop");

View file

@ -1,10 +1,9 @@
import type { Page, Request } from "@playwright/test";
import { expect, test } from "@playwright/test";
import { load } from "cheerio";
import type { MailServer } from "smtp-tester";
import smtpTester from "smtp-tester";
import type { PollPage } from "tests/poll-page";
import { captureOne } from "./mailpit/mailpit";
import { NewPollPage } from "./new-poll-page";
test.describe(() => {
@ -13,9 +12,7 @@ test.describe(() => {
let touchRequest: Promise<Request>;
let editSubmissionUrl: string;
let mailServer: MailServer;
test.beforeAll(async ({ browser }) => {
mailServer = smtpTester.init(4025);
page = await browser.newPage();
touchRequest = page.waitForRequest(
(request) =>
@ -27,10 +24,6 @@ test.describe(() => {
pollPage = await newPollPage.createPollAndCloseDialog();
});
test.afterAll(async () => {
mailServer.stop(() => {});
});
test("should call touch endpoint", async () => {
// make sure call to touch RPC is made
expect(await touchRequest).not.toBeNull();
@ -54,17 +47,15 @@ test.describe(() => {
await invitePage.addParticipant("Anne", "test@example.com");
await expect(page.locator("text='Anne'")).toBeVisible();
const { email } = await mailServer.captureOne("test@example.com", {
const { email } = await captureOne("test@example.com", {
wait: 5000,
});
expect(email.headers.subject).toBe(
"Thanks for responding to Monthly Meetup",
);
await expect(page.locator("text='Anne'")).toBeVisible();
const $ = load(email.html as string);
expect(email.Subject).toBe("Thanks for responding to Monthly Meetup");
const $ = load(email.HTML);
const href = $("#editSubmissionUrl").attr("href");
if (!href) {