1. Update dependencies 2. Use cssnano for production 3. Assign an environment variable to Sentry's environment settings 4. `Pill` now accepts React Nodes so we can pass translations using Trans component 5. Switch to mailpit API during tests 6. Do not require Email Sender to be of type email and add proper error messages
241 lines
5.5 KiB
TypeScript
241 lines
5.5 KiB
TypeScript
import { Page } from '@playwright/test';
|
|
import { parse } from 'node-html-parser';
|
|
|
|
type MessageSummary = {
|
|
ID: string;
|
|
MessageID: string;
|
|
Read: boolean;
|
|
From: {
|
|
Name: string;
|
|
Address: string;
|
|
};
|
|
To: Array<{
|
|
Name: string;
|
|
Address: string;
|
|
}>;
|
|
Cc: Array<any>;
|
|
Bcc: Array<any>;
|
|
ReplyTo: Array<any>;
|
|
Subject: string;
|
|
Created: string;
|
|
Tags: Array<any>;
|
|
Size: number;
|
|
Attachments: number;
|
|
Snippet: string;
|
|
};
|
|
|
|
type MessagesResponse = {
|
|
total: number;
|
|
unread: number;
|
|
count: number;
|
|
messages_count: number;
|
|
start: number;
|
|
tags: Array<any>;
|
|
messages: MessageSummary[];
|
|
};
|
|
|
|
/**
|
|
* Mailbox class for interacting with the Mailpit mailbox API.
|
|
*/
|
|
export class Mailbox {
|
|
static URL = 'http://127.0.0.1:54324';
|
|
|
|
constructor(private readonly page: Page) {}
|
|
|
|
async visitMailbox(
|
|
email: string,
|
|
params: {
|
|
deleteAfter: boolean;
|
|
subject?: string;
|
|
},
|
|
) {
|
|
console.log(`Visiting mailbox ${email} ...`);
|
|
|
|
if (!email) {
|
|
throw new Error('Invalid email');
|
|
}
|
|
|
|
const json = await this.getEmail(email, params);
|
|
|
|
if (!json) {
|
|
throw new Error('Email body was not found');
|
|
}
|
|
|
|
console.log(`Email found for email: ${email}`, {
|
|
expectedEmail: email,
|
|
id: json.ID,
|
|
subject: json.Subject,
|
|
date: json.Date,
|
|
to: json.To[0],
|
|
text: json.Text,
|
|
});
|
|
|
|
if (email !== json.To[0]!.Address) {
|
|
throw new Error(`Email address mismatch. Expected ${email}, got ${json.To[0]!.Address}`);
|
|
}
|
|
|
|
const el = parse(json.HTML);
|
|
|
|
const linkHref = el.querySelector('a')?.getAttribute('href');
|
|
|
|
if (!linkHref) {
|
|
throw new Error('No link found in email');
|
|
}
|
|
|
|
console.log(`Visiting ${linkHref} from mailbox ${email}...`);
|
|
|
|
return this.page.goto(linkHref);
|
|
}
|
|
|
|
/**
|
|
* Retrieves an OTP code from an email
|
|
* @param email The email address to check for the OTP
|
|
* @param deleteAfter Whether to delete the email after retrieving the OTP
|
|
* @returns The OTP code
|
|
*/
|
|
async getOtpFromEmail(email: string, deleteAfter = false) {
|
|
console.log(`Retrieving OTP from mailbox ${email} ...`);
|
|
|
|
if (!email) {
|
|
throw new Error('Invalid email');
|
|
}
|
|
|
|
const json = await this.getEmail(email, {
|
|
deleteAfter,
|
|
subject: `One-time password for Makerkit`,
|
|
});
|
|
|
|
if (!json) {
|
|
throw new Error('Email body was not found');
|
|
}
|
|
|
|
if (email !== json.To[0]!.Address) {
|
|
throw new Error(`Email address mismatch. Expected ${email}, got ${json.To[0]!.Address}`);
|
|
}
|
|
|
|
const text = json.HTML.match(
|
|
new RegExp(`Your one-time password is: (\\d{6})`),
|
|
)?.[1];
|
|
|
|
if (text) {
|
|
console.log(`OTP code found in text: ${text}`);
|
|
return text;
|
|
}
|
|
|
|
throw new Error('Could not find OTP code in email');
|
|
}
|
|
|
|
async getEmail(
|
|
email: string,
|
|
params: {
|
|
deleteAfter: boolean;
|
|
subject?: string;
|
|
},
|
|
) {
|
|
console.log(`Retrieving email from mailbox ${email}...`);
|
|
|
|
const url = `${Mailbox.URL}/api/v1/search?query=to:${email}`;
|
|
const response = await fetch(url);
|
|
|
|
if (!response.ok) {
|
|
throw new Error(`Failed to fetch emails: ${response.statusText}`);
|
|
}
|
|
|
|
const messagesResponse = (await response.json()) as MessagesResponse;
|
|
|
|
if (!messagesResponse || !messagesResponse.messages?.length) {
|
|
console.log(`No emails found for mailbox ${email}`);
|
|
|
|
return;
|
|
}
|
|
|
|
const message = params.subject
|
|
? (() => {
|
|
const filtered = messagesResponse.messages.filter(
|
|
(item) => item.Subject === params.subject,
|
|
);
|
|
|
|
console.log(
|
|
`Found ${filtered.length} emails with subject ${params.subject}`,
|
|
);
|
|
|
|
// retrieve the latest by timestamp
|
|
return filtered.reduce((acc, item) => {
|
|
if (
|
|
new Date(acc.Created).getTime() < new Date(item.Created).getTime()
|
|
) {
|
|
return item;
|
|
}
|
|
|
|
return acc;
|
|
});
|
|
})()
|
|
: messagesResponse.messages.reduce((acc, item) => {
|
|
if (
|
|
new Date(acc.Created).getTime() < new Date(item.Created).getTime()
|
|
) {
|
|
return item;
|
|
}
|
|
|
|
return acc;
|
|
});
|
|
|
|
if (!message) {
|
|
throw new Error('No message found');
|
|
}
|
|
|
|
const messageId = message.ID;
|
|
const messageUrl = `${Mailbox.URL}/api/v1/message/${messageId}`;
|
|
|
|
const messageResponse = await fetch(messageUrl);
|
|
|
|
if (!messageResponse.ok) {
|
|
throw new Error(`Failed to fetch email: ${messageResponse.statusText}`);
|
|
}
|
|
|
|
// delete message
|
|
if (params.deleteAfter) {
|
|
console.log(`Deleting email ${messageId} ...`);
|
|
|
|
const res = await fetch(`${Mailbox.URL}/api/v1/messages`, {
|
|
method: 'DELETE',
|
|
body: JSON.stringify({ Ids: [messageId] }),
|
|
});
|
|
|
|
if (!res.ok) {
|
|
console.error(`Failed to delete email: ${res.statusText}`);
|
|
}
|
|
}
|
|
|
|
return (await messageResponse.json()) as Promise<{
|
|
ID: string;
|
|
MessageID: string;
|
|
From: {
|
|
Name: string;
|
|
Address: string;
|
|
};
|
|
To: Array<{
|
|
Name: string;
|
|
Address: string;
|
|
}>;
|
|
Cc: Array<any>;
|
|
Bcc: Array<any>;
|
|
ReplyTo: Array<any>;
|
|
ReturnPath: string;
|
|
Subject: string;
|
|
ListUnsubscribe: {
|
|
Header: string;
|
|
Links: Array<any>;
|
|
Errors: string;
|
|
HeaderPost: string;
|
|
};
|
|
Date: string;
|
|
Tags: Array<any>;
|
|
Text: string;
|
|
HTML: string;
|
|
Size: number;
|
|
Inline: Array<any>;
|
|
Attachments: Array<any>;
|
|
}>;
|
|
}
|
|
} |