import { env } from './auth-config'; import { sleep } from './gotrue'; export interface MailSummary { ID: string; Subject: string; To: { Address: string }[]; Created: string; } export interface Mail extends MailSummary { html: string; text: string; } async function getJson(path: string): Promise { const res = await fetch(`${env.mailpitUrl}/api/v1${path}`); if (!res.ok) throw new Error(`Mailpit ${path}: HTTP ${res.status}`); return res.json(); } /** Alle Mails an eine Adresse (Suche per `to:`; nie global loeschen, Tests laufen parallel). */ export async function mailsTo(address: string): Promise { const result = await getJson(`/search?query=${encodeURIComponent(`to:"${address}"`)}&limit=50`); return (result.messages ?? []) as MailSummary[]; } export async function countMails(address: string): Promise { return (await mailsTo(address)).length; } export async function readMail(id: string): Promise { const m = await getJson(`/message/${id}`); return { ID: m.ID, Subject: m.Subject, To: m.To, Created: m.Date ?? '', html: m.HTML ?? '', text: m.Text ?? '' }; } /** * Wartet, bis mindestens `count` Mails an die Adresse vorliegen, und liefert die neueste. * Faellt nach `timeoutMs` mit einer aussagekraeftigen Meldung durch. */ export async function waitForMail(address: string, count = 1, timeoutMs = 10_000): Promise { const deadline = Date.now() + timeoutMs; let found: MailSummary[] = []; while (Date.now() < deadline) { found = await mailsTo(address); if (found.length >= count) { // Mailpit liefert die neueste zuerst. return readMail(found[0].ID); } await sleep(200); } throw new Error(`Keine ${count}. Mail an ${address} nach ${timeoutMs} ms (vorhanden: ${found.length})`); } /** * Belegt „es kommt keine (weitere) Mail": wartet ein festes Fenster und zaehlt dann. * Nur dort einsetzen, wo die Abwesenheit selbst das Kriterium ist (Krit. 4, 8, 11). */ export async function expectMailCount(address: string, expected: number, windowMs = 2_000): Promise { await sleep(windowMs); const actual = await countMails(address); if (actual !== expected) throw new Error(`Mails an ${address}: erwartet ${expected}, vorhanden ${actual}`); return actual; } /** Link aus der Mail: `.../auth/confirm?token_hash=...&type=email|recovery` (HTML-Entities decodiert). */ export function extractConfirmLink(mail: Mail): URL { const decoded = mail.html.replace(/&/g, '&'); const match = decoded.match(/https?:\/\/[^"'\s<]+\/auth\/confirm\?[^"'\s<]+/); if (!match) throw new Error(`Kein Bestaetigungslink in der Mail "${mail.Subject}"`); return new URL(match[0]); } export function extractTokenHash(mail: Mail): { tokenHash: string; type: string; link: URL } { const link = extractConfirmLink(mail); const tokenHash = link.searchParams.get('token_hash'); const type = link.searchParams.get('type'); if (!tokenHash || !type) throw new Error('token_hash oder type fehlen im Link'); return { tokenHash, type, link }; }