mirror of
https://github.com/lukevella/rallly.git
synced 2025-08-03 16:38:34 +02:00
🧑💻 Use mailpit as dev/test smtp server (#1486)
This commit is contained in:
parent
b00b685bbd
commit
285860ec9e
12 changed files with 124 additions and 242 deletions
|
@ -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
|
|
@ -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"
|
||||
}
|
||||
|
|
|
@ -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");
|
||||
|
||||
|
|
|
@ -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 () => {
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
56
apps/web/tests/mailpit/mailpit.ts
Normal file
56
apps/web/tests/mailpit/mailpit.ts
Normal 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;
|
||||
}
|
46
apps/web/tests/mailpit/types.ts
Normal file
46
apps/web/tests/mailpit/types.ts
Normal 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;
|
||||
}
|
|
@ -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");
|
||||
|
|
|
@ -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) {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue