- M package.json — added typecheck, test:e2e, verify scripts; added @playwright/test devDep - M pnpm-lock.yaml - M .gitignore — Playwright artifacts - M tsconfig.json — auto-modified by Next.js include path - ?? e2e/ — config + fixtures + 3 specs - ?? playwright.config.ts - ?? scripts/verify.sh, scripts/db-test-truncate.sql
42 lines
1.8 KiB
TypeScript
42 lines
1.8 KiB
TypeScript
const MAILPIT = process.env.MAILPIT_URL ?? "http://localhost:8025";
|
|
|
|
export async function clearMailpit(): Promise<void> {
|
|
const res = await fetch(`${MAILPIT}/api/v1/messages`, { method: "DELETE" });
|
|
if (!res.ok) throw new Error(`Mailpit clear failed: ${res.status}`);
|
|
}
|
|
|
|
type MessageSummary = { ID: string; To: { Address: string }[]; Created: string };
|
|
|
|
async function listMessagesTo(email: string): Promise<MessageSummary[]> {
|
|
const url = `${MAILPIT}/api/v1/search?query=${encodeURIComponent(`to:${email}`)}`;
|
|
const res = await fetch(url);
|
|
if (!res.ok) throw new Error(`Mailpit search failed: ${res.status}`);
|
|
const data = (await res.json()) as { messages: MessageSummary[] };
|
|
return data.messages ?? [];
|
|
}
|
|
|
|
async function getMessageBody(id: string): Promise<{ text: string; html: string }> {
|
|
const res = await fetch(`${MAILPIT}/api/v1/message/${id}`);
|
|
if (!res.ok) throw new Error(`Mailpit fetch failed: ${res.status}`);
|
|
const data = (await res.json()) as { Text?: string; HTML?: string };
|
|
return { text: data.Text ?? "", html: data.HTML ?? "" };
|
|
}
|
|
|
|
export async function waitForMagicLink(email: string, opts: { timeoutMs?: number } = {}): Promise<string> {
|
|
const timeoutMs = opts.timeoutMs ?? 10_000;
|
|
const deadline = Date.now() + timeoutMs;
|
|
const linkRe = /https?:\/\/[^\s<>"]*\/api\/auth\/callback\/nodemailer[^\s<>"]*/i;
|
|
|
|
while (Date.now() < deadline) {
|
|
const messages = await listMessagesTo(email);
|
|
if (messages.length > 0) {
|
|
const newest = messages.sort((a, b) => b.Created.localeCompare(a.Created))[0];
|
|
const { text, html } = await getMessageBody(newest.ID);
|
|
const m = (text.match(linkRe) ?? html.match(linkRe))?.[0];
|
|
if (m) return m.replace(/&/g, "&");
|
|
}
|
|
await new Promise((r) => setTimeout(r, 250));
|
|
}
|
|
throw new Error(`No magic link for ${email} within ${timeoutMs}ms`);
|
|
}
|