163 lines
4.8 KiB
TypeScript
163 lines
4.8 KiB
TypeScript
import { APIRequestContext, request } from "@playwright/test"
|
|
|
|
/**
|
|
* MailDev API helper for testing email flows.
|
|
*
|
|
* Configure via environment variables:
|
|
* - MAILDEV_URL: MailDev web UI URL (default: https://${PROJECT_NAME}-maildev.code.testversion.online)
|
|
* - MAILDEV_USER: Basic auth username (default: code)
|
|
* - MAILDEV_PASS: Basic auth password
|
|
*/
|
|
const MAILDEV_URL = process.env.MAILDEV_URL || "http://localhost:1080"
|
|
const MAILDEV_USER = process.env.MAILDEV_USER || "code"
|
|
const MAILDEV_PASS = process.env.MAILDEV_PASS || ""
|
|
|
|
export interface MailDevEmail {
|
|
id: string
|
|
subject: string
|
|
html: string
|
|
to: { address: string; name: string }[]
|
|
from: { address: string; name: string }[]
|
|
date: string
|
|
time: string
|
|
read: boolean
|
|
headers: Record<string, string>
|
|
}
|
|
|
|
let maildevContext: APIRequestContext | null = null
|
|
|
|
async function getContext(): Promise<APIRequestContext> {
|
|
if (!maildevContext) {
|
|
const opts: any = {
|
|
baseURL: MAILDEV_URL,
|
|
ignoreHTTPSErrors: true,
|
|
}
|
|
if (MAILDEV_PASS) {
|
|
opts.httpCredentials = {
|
|
username: MAILDEV_USER,
|
|
password: MAILDEV_PASS,
|
|
}
|
|
}
|
|
maildevContext = await request.newContext(opts)
|
|
}
|
|
return maildevContext
|
|
}
|
|
|
|
/**
|
|
* Get all emails from MailDev.
|
|
*/
|
|
export async function getAllEmails(): Promise<MailDevEmail[]> {
|
|
const ctx = await getContext()
|
|
const res = await ctx.get("/email")
|
|
if (!res.ok()) {
|
|
throw new Error(`MailDev API error: ${res.status()} ${res.statusText()}`)
|
|
}
|
|
return res.json()
|
|
}
|
|
|
|
/**
|
|
* Delete all emails in MailDev.
|
|
*/
|
|
export async function deleteAllEmails(): Promise<void> {
|
|
const ctx = await getContext()
|
|
await ctx.delete("/email/all")
|
|
}
|
|
|
|
/**
|
|
* Delete a specific email by ID.
|
|
*/
|
|
export async function deleteEmail(id: string): Promise<void> {
|
|
const ctx = await getContext()
|
|
await ctx.delete(`/email/${id}`)
|
|
}
|
|
|
|
/**
|
|
* Wait for an email to arrive for a specific recipient.
|
|
*/
|
|
export async function waitForEmail(
|
|
toEmail: string,
|
|
options: {
|
|
subject?: string | RegExp
|
|
timeout?: number
|
|
pollInterval?: number
|
|
} = {}
|
|
): Promise<MailDevEmail> {
|
|
const { subject, timeout = 5000, pollInterval = 250 } = options
|
|
const start = Date.now()
|
|
|
|
while (Date.now() - start < timeout) {
|
|
const emails = await getAllEmails()
|
|
const match = emails.find((e) => {
|
|
const toMatch = e.to.some((t) => t.address.toLowerCase() === toEmail.toLowerCase())
|
|
if (!toMatch) return false
|
|
if (!subject) return true
|
|
if (typeof subject === "string") return e.subject.includes(subject)
|
|
return subject.test(e.subject)
|
|
})
|
|
if (match) return match
|
|
await new Promise((r) => setTimeout(r, pollInterval))
|
|
}
|
|
|
|
throw new Error(`Timeout: No email to ${toEmail} received within ${timeout}ms`)
|
|
}
|
|
|
|
/**
|
|
* Extract a 6-digit verification code from an email's HTML body.
|
|
*/
|
|
export function extractCode(email: MailDevEmail): string {
|
|
const match = email.html.match(/>(\d{6})<\//)
|
|
if (!match) {
|
|
throw new Error(`No 6-digit code found in email "${email.subject}"`)
|
|
}
|
|
return match[1]
|
|
}
|
|
|
|
/**
|
|
* Perform an action and capture the verification code from the resulting email.
|
|
* Filters out emails that existed before the action was triggered.
|
|
*/
|
|
export async function captureCode(
|
|
toEmail: string,
|
|
action: () => Promise<void>,
|
|
options: { subject?: string | RegExp; timeout?: number } = {}
|
|
): Promise<string> {
|
|
const existingEmails = await getAllEmails()
|
|
const existingIds = new Set(
|
|
existingEmails
|
|
.filter((e) => e.to.some((t) => t.address.toLowerCase() === toEmail.toLowerCase()))
|
|
.map((e) => e.id)
|
|
)
|
|
|
|
await action()
|
|
|
|
const { timeout = 5000, subject } = options
|
|
const pollInterval = 250
|
|
const start = Date.now()
|
|
|
|
while (Date.now() - start < timeout) {
|
|
const emails = await getAllEmails()
|
|
const match = emails.find((e) => {
|
|
if (existingIds.has(e.id)) return false
|
|
const toMatch = e.to.some((t) => t.address.toLowerCase() === toEmail.toLowerCase())
|
|
if (!toMatch) return false
|
|
if (!subject) return true
|
|
if (typeof subject === "string") return e.subject.includes(subject)
|
|
return subject.test(e.subject)
|
|
})
|
|
if (match) return extractCode(match)
|
|
await new Promise((r) => setTimeout(r, pollInterval))
|
|
}
|
|
|
|
throw new Error(`Timeout: No new email to ${toEmail} received within ${timeout}ms`)
|
|
}
|
|
|
|
/**
|
|
* Dispose the MailDev API context. Call in globalTeardown.
|
|
*/
|
|
export async function disposeMailDev(): Promise<void> {
|
|
if (maildevContext) {
|
|
await maildevContext.dispose()
|
|
maildevContext = null
|
|
}
|
|
}
|