90 lines
3.7 KiB
TypeScript
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(/&/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 };
|
|
}
|