Email-App

Eine vom Framework mitgelieferte App für ein zu testendes Postfach — verifiziere eine Signup-Bestätigungsmail, lies einen Einmal-Code aus, oder folge einem Bestätigungslink, den eine zu testende Anwendung verschickt hat, über einen einheitlichen Microsoft-Graph- + Gmail-Client.

← Zurück zur Übersicht · 🇬🇧 English · ← FTP-App · Demo Web App →


Was die Email-App bietet

Eine sofort nutzbare App, _Email, wird neben _Skeleton, _ExampleWebApp, der OS-App und der FTP-App mitgeliefert — wähle sie im Scaffold-Wizard, oder füge ihren Re-Export von Hand zu einem bestehenden Projekt hinzu. Sie kapselt sowohl Microsoft Graph (Microsoft-365-/Outlook-Postfächer) als auch die Gmail-API hinter einem einheitlichen Set von Steps, sodass derselbe TestCase-Code unabhängig davon funktioniert, bei welchem Provider das Ziel-Postfach liegt.

Greife darauf zurück, wann immer ein zu testender Ablauf eine E-Mail verschickt und dein Test in ein echtes Postfach schauen muss, statt die Nachricht zu stubben — zum Beispiel:

  • Bestätigen, dass ein Signup-Ablauf tatsächlich eine Bestätigungsmail verschickt hat, und den Einmal-Code (OTP) daraus auslesen, um den Ablauf abzuschließen
  • Einem Bestätigungs-/Magic-Link folgen, der in einer Passwort-Reset- oder E-Mail-Verifizierungs-Nachricht eingebettet ist
  • Prüfen, dass eine Benachrichtigungsmail (nicht) verschickt wurde, ohne dass je ein Mensch das Postfach öffnet

_Email ist eine gewöhnliche App im Drei-Schichten-Sinn aus Konzepte — ein Control, TestSteps und ein References.ts-Barrel — der einzige Unterschied zu einer selbst geschriebenen App ist, dass das Framework sie vorinstanziiert mitliefert. Alle 12 TestSteps sind benannt nach TS_Main_Email_<Action>, gemäß der üblichen Naming-Konvention.


Das Email-Control in deinen Test importieren

Die 2_Apps/1_Global/References.ts deines Projekts re-exportiert das Barrel der App, genau wie jede andere gescaffoldete App:

// 2_Apps/1_Global/References.ts
export * as Email from '../_Email/References';

Damit ist jeder Step über Project aus einem TestCase aufrufbar:

// 3_Cases/TC_MyFlow.spec.ts
import * as Project from '@GlobalRef';

await Project.Email.TS_Main_Email_CheckMessageExists('inbox', { subject: 'Welcome' }, true);

Wie die OS-App und die FTP-App nehmen diese Steps nur ihre eigenen Business-Parameter entgegen — keine führende refId, kein pageLogName/sectionName. Keiner der Email-Steps adressiert ein Screen-Element, daher gibt es kein Referenz-Screenshot zu binden und keinen Seiten-Kontext zu loggen — der beim Connect gewählte Connection-Alias ist schlicht das erste Argument des Steps.


(A) Outlook-Konto registrieren

TS_Main_Email_Connect nimmt ein auth-Argument mit zwei Formen entgegen — diese Schritt-für-Schritt-Anleitung deckt den selbst-verwalteten OAuth-Weg ab, bei dem die App ihre eigenen Access-Tokens gegen ein Postfach beschafft und erneuert, das du in Entra ID (Azure AD) registrierst. Wenn ein Begleit-Tool in deinem Test-Setup bereits einen eigenen Login durchführt und dir direkt ein Token übergeben kann, springe weiter zu Hast du schon ein Token? Bring dein eigenes mit — auf diesem Weg gibt es überhaupt kein Entra-ID-Setup.

  1. Eine Anwendung in Entra ID (Azure AD) registrieren — siehe Microsofts eigenen Quickstart.
  2. Unter API permissions die Microsoft-Graph-Application-Permission Mail.Read hinzufügen (zusätzlich Mail.Send, falls deine Tests Nachrichten verschicken) — siehe Graphs Mail-Permissions-Referenz.
  3. Admin-Consent für den Tenant erteilen — Application-Permissions greifen ohne ihn nicht.
  4. Unter Certificates & secrets ein neues Client-Secret anlegen und dessen Wert sofort kopieren — Azure zeigt ihn nur einmal an.
  5. Die Application (client) ID und Directory (tenant) ID von der Overview-Seite der App-Registrierung notieren.
  6. Das Client-Secret im Password-Manager speichern, statt es hart zu codieren — npx cc-testframework password set --username email-oauth-client-secret --stdin (siehe Eine Zugangsdaten anlegen).
  7. Mit dem App-Only-Client-Credential-Flow verbinden und das Secret aus dem Password-Manager zurücklesen:
import * as Core from '@Core/References';

const clientSecret = await Core.PasswordManager.getPassword('email-oauth-client-secret');

await Project.Email.TS_Main_Email_Connect('inbox', 'microsoft', mailbox, {
    kind: 'oauth',
    clientId: '<deine-client-id>',
    clientSecret,
    tenantId: '<deine-tenant-id>',
});

refreshToken wegzulassen (wie oben) nutzt den App-Only-Client-Credential-Flow. Um stattdessen den Postfach-delegierten Flow zu nutzen, einmal einen delegierten OAuth-Consent-Flow durchlaufen, um ein refreshToken zu erhalten, es genauso speichern und dem auth-Objekt hinzufügen. So oder so werden clientSecret, refreshToken und ein etwaiges BYOT-accessToken nie geloggt, nie in eine Fehlermeldung aufgenommen und nie in den Test-Report geschrieben.


Hast du schon ein Token? Bring dein eigenes mit

Wenn irgendetwas in deinem Test-Setup bereits einen eigenen OAuth-Flow ausführt — ein Begleit-Tool, das einen echten Login durchführt, oder ein Token, das du selbst für einen Service-Account erzeugst — übergib der App das resultierende Access-Token direkt und überspringe die Registrierung oben vollständig. Auf diesem Weg gibt es überhaupt kein Projekt-OAuth-Setup, und die App erneuert das Token nie; du bist für seine Aktualität während des Tests verantwortlich:

await Project.Email.TS_Main_Email_Connect('inbox', 'microsoft', mailbox, {
    kind: 'accessToken',
    accessToken: token,
});

Gmail statt Outlook verwenden

Derselbe TS_Main_Email_Connect-Call funktioniert auch gegen ein Gmail-Postfach — tausche 'microsoft' gegen 'google' und registriere ein Google-Cloud-Projekt statt einer Entra-ID-App:

  1. Ein Projekt in der Google Cloud Console anlegen (oder auswählen).
  2. Die Gmail-API für dieses Projekt aktivieren.
  3. Den OAuth-Consent-Screen konfigurieren, dann eine OAuth-2.0-Client-ID anlegen — siehe Googles OAuth-2.0-Dokumentation.
  4. Den Consent-Flow einmal durchlaufen, mit dem Gmail-Scope, den deine Tests brauchen (z.B. gmail.readonly nur zum Lesen, gmail.modify zusätzlich für mark-read/delete, gmail.send zum Senden), um ein Refresh-Token zu erhalten.
  5. clientId/clientSecret/refreshToken im Password-Manager speichern und alle drei übergeben — anders als bei Microsoft hat Googles Flow kein App-Only-Äquivalent, daher ist refreshToken immer erforderlich.

(B) Email-Steps in einem Test verwenden

Sobald ein Postfach unter einem Alias registriert ist, nimmt jeder weitere TS_Main_Email_*-Step diesen Alias als erstes Argument entgegen — Connect, Warten auf eine Nachricht, Code extrahieren und Disconnect lesen sich wie ein kurzes Skript:

await Project.Email.TS_Main_Email_Connect('inbox', 'microsoft', mailbox, auth);
const msg = await Project.Email.TS_Main_Email_WaitForMessage('inbox', { from: 'noreply@<deine-app-domain>', subject: 'Confirm' });
const otp = await Project.Email.TS_Main_Email_ExtractOtp('inbox', msg.id, /\d{6}/);
expect(otp).toHaveLength(6);
await Project.Email.TS_Main_Email_Disconnect('inbox');

Die folgenden Abschnitte decken jeden der 12 TS_Main_Email_*-Steps im Detail ab, plus einen vollständigen TestCase, der sie in nummerierte Step-Blöcke einbettet.


Einmal verbinden, über einen Alias arbeiten, am Ende trennen

Wie bei FtpClient ist eine Postfach-Verbindung sitzungsbasiert — der beim Connect gewählte Alias ist das, was jeder spätere Step als erstes Argument entgegennimmt, daher gibt es kein Live-Connection-Handle, das dein TestCase-Code halten oder weitergeben müsste:

// Eine Verbindung einmal öffnen, unter einem selbst gewählten Namen
await Project.Email.TS_Main_Email_Connect('inbox', 'microsoft', mailbox, auth);

// ... beliebig viele Operationen gegen denselben Alias ausführen ...
const message = await Project.Email.TS_Main_Email_WaitForMessage('inbox', { subject: 'Welcome' });

// Am Ende des Ablaufs schließen
await Project.Email.TS_Main_Email_Disconnect('inbox');

Jede Postfach-Operation ist ein einzelner REST-Call statt eines persistenten Sockets, aber ein offen gebliebener Alias leckt trotzdem für den Rest des Test-Laufs — schließe ihn in einem Teardown-Hook statt nur am Ende des Happy Path:

// 3_Cases/TC_SignupConfirmation.spec.ts
import * as Project from '@GlobalRef';

test.afterEach(async () => {
    await Project.Email.TS_Main_Email_Disconnect('inbox');
});

Nachrichten finden und lesen

TS_Main_Email_FindMessages, TS_Main_Email_WaitForMessage, TS_Main_Email_GetMessageText und TS_Main_Email_GetMessageHtml geben den abgerufenen Wert zurück, zusätzlich dazu, dass sie nicht-geheime Metadaten im Test-Report festhalten:

Step Rückgabe Was er tut
TS_Main_Email_FindMessages EmailMessage[] Findet jede Nachricht, die zu einer Query passt, hält die Trefferzahl fest
TS_Main_Email_WaitForMessage EmailMessage Wartet (per Polling), bis eine passende Nachricht eintrifft (oder ein optionaler Timeout abläuft), hält id/subject fest
TS_Main_Email_GetMessageText string Ruft den Plain-Text-Body einer Nachricht ab, hält seine Länge fest
TS_Main_Email_GetMessageHtml string Ruft den HTML-Body einer Nachricht ab, hält seine Länge fest

Eine Query ist ein einfaches Objekt — { from?, subject?, since?, unreadOnly? } — jedes Feld optional und kombinierbar:

// Bis zu 30s (der Standard) warten, bis die Bestätigungsmail eintrifft
const message = await Project.Email.TS_Main_Email_WaitForMessage('inbox', {
    from: 'noreply@<deine-app-domain>',
    subject: 'Confirm your account',
});

// Oder jede ungelesene Nachricht eines Absenders finden, ohne zu warten
const unread = await Project.Email.TS_Main_Email_FindMessages('inbox', { from: 'billing@<deine-app-domain>', unreadOnly: true });

const body = await Project.Email.TS_Main_Email_GetMessageText('inbox', message.id);

TS_Main_Email_ExtractOtp und TS_Main_Email_ExtractLink ziehen per regulärem Ausdruck einen Substring aus dem Plain-Text-Body einer Nachricht und geben ihn zurück — loggen aber bewusst nicht den extrahierten Wert selbst, nur die messageId, aus der er stammt, da sowohl ein Code als auch ein Link (der häufig sein eigenes Token einbettet) Secrets sind:

// Ein 6-stelliger OTP, irgendwo im Nachrichtentext eingebettet
const otp = await Project.Email.TS_Main_Email_ExtractOtp('inbox', message.id, /\d{6}/);
expect(otp).toHaveLength(6);

// Der erste Bestätigungslink — Pattern ist optional, Standard ist die erste http(s)-URL
const link = await Project.Email.TS_Main_Email_ExtractLink('inbox', message.id, /https:\/\/<deine-app-domain>\/confirm\?[^\s]+/);

💡 Beide Extraktoren akzeptieren eine Capture-Group. Übergib ein Pattern mit einer Capture-Group (z.B. /code: (\d{6})/), um nur diese Gruppe statt des gesamten Treffers zu extrahieren — nützlich, wenn der Code im umgebenden Text eingebettet ist statt allein zu stehen.


Nachrichten senden und verwalten

Step Was er tut
TS_Main_Email_SendMessage Sendet eine neue Nachricht (to, subject, body)
TS_Main_Email_MarkRead Markiert eine Nachricht als gelesen
TS_Main_Email_DeleteMessage Löscht (verschiebt in den Papierkorb) eine Nachricht
await Project.Email.TS_Main_Email_SendMessage('inbox', 'qa@<deine-app-domain>', 'Automated test message', 'Sent by the test suite');
await Project.Email.TS_Main_Email_MarkRead('inbox', message.id);
await Project.Email.TS_Main_Email_DeleteMessage('inbox', message.id);

Gegen das Postfach prüfen

await Project.Email.TS_Main_Email_CheckMessageExists('inbox', { subject: 'Welcome' }, true);
await Project.Email.TS_Main_Email_CheckMessageExists('inbox', { subject: 'Account deleted' }, false);

TS_Main_Email_CheckMessageExists prüft, ob eine Nachricht existiert, die zu einer Query passt (oder nicht, gemäß einem erwarteten Boolean) — dieselbe Query-Form wie oben bei TS_Main_Email_FindMessages.


Ein vollständiger TestCase

Connect, ein Warten auf eine Nachricht, eine OTP-Extraktion und Disconnect in einem Ablauf zusammengeführt:

// 3_Cases/TC_SignupConfirmation.spec.ts
import * as Project from '@GlobalRef';

Project.Core.test('TC_SignupConfirmation', async () => {
    Project.Core.Step.setCurrentTestCaseName('TC_SignupConfirmation');

    await Project.Core.Step.numberedStepBlock('Mit dem Test-Postfach verbinden', async () => {
        await Project.Email.TS_Main_Email_Connect('inbox', 'microsoft', mailbox, { kind: 'accessToken', accessToken: token });
    });

    await Project.Core.Step.numberedStepBlock('Registrieren und den Bestätigungs-Code auslesen', async () => {
        await Project.<YourApp>.TS_Execution_Start();
        await Project.<YourApp>.TS_Main_Button_Click(pageLogName, sectionName, 'Sign up');

        const message = await Project.Email.TS_Main_Email_WaitForMessage('inbox', { from: 'noreply@<deine-app-domain>', subject: 'Confirm' });
        const otp = await Project.Email.TS_Main_Email_ExtractOtp('inbox', message.id, /\d{6}/);
        expect(otp).toHaveLength(6);
    });

    await Project.Core.Step.numberedStepBlock('Trennen', async () => {
        await Project.Email.TS_Main_Email_Disconnect('inbox');
    });
});

Provider-Referenz

provider-Wert API Unterstützte Auth-Flows
microsoft Microsoft Graph (Mail.Read/Mail.Send, .default-Scope) App-Only-Client-Credential (refreshToken weglassen), oder delegierter Refresh-Token-Flow (refreshToken übergeben)
google Gmail-API (gmail.readonly/gmail.modify/gmail.send-Scope, beim Consent gewählt) Nur Refresh-Token-Flow — Googles Flow hat kein App-Only-Äquivalent, daher ist refreshToken immer erforderlich

Dependencies liefert das Framework mit

@azure/msal-node und google-auth-library sind reguläre Dependencies des Frameworks selbst, keine optionalen Zusatz-Pakete — die Installation des Frameworks installiert beide bereits mit, daher gibt es keinen zusätzlichen Setup-Schritt für die OAuth-Bibliothek eines der beiden Provider.


Live-Tests vs. Unit-Tests

Ein Live-Smoke-Test gegen diese App braucht ein echtes Postfach und echte OAuth-Zugangsdaten (oder ein echtes BYOT-Access-Token) — es gibt keinen Weg daran vorbei, tatsächlich Mail über die eigenen Server des Providers zu senden und zu empfangen. Die eigene Test-Suite des Frameworks für diese App läuft stattdessen komplett gegen Fakes, daher hängt sie nie von Netzwerk-Zugriff, einem echten Microsoft-/Google-Account oder einem Live-Postfach ab; für dein Projekt gilt dasselbe für alles, was das echte Postfach nicht braucht — reserviere die echte Verbindung für die Handvoll Szenarien, die spezifisch die E-Mail-Zustellung verifizieren.


Typische Fehler und ihre Behebung

Fehler Ursache Fix
No Email connection registered for alias '...' Ein Operations-Step lief vor TS_Main_Email_Connect für diesen Alias, oder nachdem TS_Main_Email_Disconnect ihn bereits geschlossen hat Zuerst Connect aufrufen, und prüfen, ob afterEach/afterAll-Reihenfolge nicht zu früh trennt
Authentication failed for mailbox '...': token expired or invalid Ein BYOT-Access-Token ist abgelaufen, oder die OAuth-Client-Zugangsdaten sind falsch Ein frisches Access-Token übergeben, oder clientId/clientSecret/tenantId/refreshToken gegen die App-Registrierung des Providers gegenprüfen
Keine Nachricht passt je zur Query von TS_Main_Email_WaitForMessage Die Mail ist noch nicht angekommen (timeoutMs erhöhen), die Query ist zu eng, oder die zu testende App hat sie nie tatsächlich verschickt Die Query erst weiter fassen, um zu bestätigen, dass überhaupt etwas ankommt, dann wieder enger fassen; den Mail-Versand-Pfad der zu testenden App unabhängig prüfen
Google OAuth requires a refreshToken auth.refreshToken wurde bei einer google-Verbindung weggelassen Den Consent-Flow einmal durchlaufen, um ein Refresh-Token zu erhalten — siehe Gmail statt Outlook verwenden oben

Wo es weitergeht

  • Credential ManagementclientSecret/refreshToken speichern statt sie hart zu codieren
  • OS-App / FTP-App — die anderen vom Framework mitgelieferten Utility-Apps
  • TestSteps bauen — die allgemeine TS_Main_*-Naming-Konvention, der diese Steps folgen
  • FAQ — Setup-Probleme lösen

📧 Fragen? Kontakt: jens.szelag@itsbusiness.ch

itsbusiness AG · Bern · Schweiz