Shuffle/e2e/support/mailpit.ts
cschlaefke aa421b12b0
Some checks failed
CI / flutter (pull_request) Successful in 1m15s
CI / web-build (pull_request) Successful in 53s
CI / supabase (pull_request) Successful in 3s
CI / supabase-tests (pull_request) Successful in 30s
CI / e2e (pull_request) Failing after 3m56s
test(auth): address review findings for e2e
Refs #38

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-03 10:55:27 +01:00

90 lines
3.7 KiB
TypeScript

import { env } from './auth-config';
import { GoTrueClient, randomEmail, randomPassword, 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<any> {
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<MailSummary[]> {
const result = await getJson(`/search?query=${encodeURIComponent(`to:"${address}"`)}&limit=50`);
return (result.messages ?? []) as MailSummary[];
}
export async function countMails(address: string): Promise<number> {
return (await mailsTo(address)).length;
}
export async function readMail(id: string): Promise<Mail> {
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<Mail> {
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“ ohne festes Zeitfenster: GoTrue versendet synchron im
* Request, Mailpit nimmt die Mails der Reihe nach an. Der Helfer loest deshalb eine bekannte
* Folgemail (Passwort-Reset fuer ein frisches Sentinel-Konto) aus, wartet auf deren Eintreffen
* und zaehlt danach die Mails an `address`. Ist die Sentinel-Mail da, waeren zuvor gesendete
* Mails es auch.
* Nur dort einsetzen, wo die Abwesenheit selbst das Kriterium ist (Krit. 4, 8, 11).
*/
export async function expectMailCount(address: string, expected: number): Promise<number> {
const client = new GoTrueClient();
const sentinel = randomEmail('sentinel');
const created = await client.createConfirmedUser(sentinel, randomPassword());
if (created.status !== 200) throw new Error(`Sentinel-Konto nicht anlegbar: HTTP ${created.status}`);
const res = await client.recover(sentinel);
if (res.status !== 200) throw new Error(`Sentinel-Reset fehlgeschlagen: HTTP ${res.status}`);
await waitForMail(sentinel);
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(/&amp;/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 };
}