From d66dd60a76f5063614d0fb823200ed80088d0fd5 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 4 Oct 2026 18:29:10 +0000 Subject: [PATCH] Zahlung nach Abo-Ende: Zusatztests Webhook und Abgleich (T19 nie doppelt) Nur Tests in test/termineAboAbgleich.test.js, kein Produktionscode: handleFeatureCheckoutEvent und gleicheAboAb gegeneinander (Review Focus 1, nie Geld ohne Spur, nie doppelt). - Zahlung nach dem Abschalten: genau ein T19 (zahlung_nach_ende, Sitzung), kein Ticket, kein K15/T16; Wiederholung und async_payment_succeeded stumm - T19-Versand scheitert: Webhook wirft, Zahlung gebucht, Fall offen; erste Wiederholung mit funktionierendem Versand sendet genau ein T19 - Folgetag-Abgleich: K22 vor_einrichtung, Marker aboEndeMailAt, kein zweites T19 - Abgleich vor der Webhook-Wiederholung und parallel (Promise.all): insgesamt ein T19 - Grund team: T16 abgeschaltet, kein T19; Abo laeuft (ordered/setup): Ticket, K15, T16 - Rennen Zahlung vor Abgleich: Ticket, T16 und ein T19; Stripe-Retry stumm Co-Authored-By: Claude Sonnet 5.5 --- test/termineAboAbgleich.test.js | 342 +++++++++++++++++++++++++++++++- 1 file changed, 341 insertions(+), 1 deletion(-) diff --git a/test/termineAboAbgleich.test.js b/test/termineAboAbgleich.test.js index 569f086..d0de644 100644 --- a/test/termineAboAbgleich.test.js +++ b/test/termineAboAbgleich.test.js @@ -5,7 +5,7 @@ import { mailSink } from './support/mailSink.js' import { schemaKeys } from './support/schema.js' import { useTestEnv } from './support/testEnv.js' -// Abo-Abgleich des Terminplaners (server/services/termine/aboAbgleich.js, Terminplaner TP2 E4/E5/E6): aktive Terminplaner ohne laufendes +// Abo-Abgleich des Terminplaners (server/services/termine/aboAbgleich.js, Terminplaner TP2 E4/E5/E6; E10: Zusammenspiel mit dem Zahlungs-Webhook, letzter Abschnitt): aktive Terminplaner ohne laufendes // Wartungs-Abo abschalten (Schritt 1), Einrichtungen, die das Abo-Ende erreicht, abschalten und erstatten (Schritt 1b, E5), die // Mail K22 nachholen (Schritt 2) und bei zurueckgekehrtem Abo wieder einschalten, mit K23 (Schritt 3, E6). Aufbau wie termineJobs.test.js: Nachbau von Appwrite mit Schema und Standard-Limit 25, Mails in // der Senke, feste Zeit JETZT. Stripe wird nie erreicht (Port 1). @@ -1887,3 +1887,343 @@ test('Sofortpfad: wartet auf die Sperre feature-admin des Projekts; T19 und K22 assert.equal(frei.length, 3, 'T19 und zwei K22') assert.ok(frei.every(([, ...rest]) => rest.every(Boolean)), JSON.stringify(frei)) }) + +// ---------------------------------------------------------------- Zahlung nach dem Abo-Ende: Webhook und Abgleich zusammen (E10) +// +// Review Focus 1: eine Zahlung, die nach einem Zustandswechsel eintrifft, hinterlaesst immer eine Spur (Ticket ODER T16/T19 an das +// Team) und nie ein Doppel. Das Verhalten selbst steckt in siteFeatures.js (markPaid-Fall, meldeErstattung, onFeaturePaid, B10); +// diese Tests lassen Webhook (handleFeatureCheckoutEvent) und Abgleich (gleicheAboAb) gegeneinander laufen. + +const SITZUNG = 'cs_test_1' +const event = (type, session) => ({ id: `evt_${Math.random()}`, object: 'event', type, data: { object: session } }) +const paidSession = (id = SITZUNG, projectId = 'p_projekt01', extra = {}) => ({ + id, + object: 'checkout.session', + mode: 'payment', + payment_status: 'paid', + metadata: { customerId: KUNDE, projectId, featureKey: 'termine' }, + ...extra, +}) +const COMPLETED = 'checkout.session.completed' +const ASYNC_BEZAHLT = 'checkout.session.async_payment_succeeded' +/** Das Stripe-Ereignis einer bezahlten Sitzung (noch einmal mit demselben Ereignis aufrufen = Wiederholung durch Stripe). */ +const zahlungsEreignis = (typ = COMPLETED, session = paidSession()) => event(typ, session) + +const T16_BETREFF = 'Neue Funktion bestellt: Terminplaner' +const K15_BETREFF = 'Deine Bestellung: Terminplaner' +const t16 = () => mails().filter((m) => m.subject === T16_BETREFF) +const k15 = () => mails().filter((m) => m.subject === K15_BETREFF) +const betreffs = () => mails().map((m) => m.subject) +const tickets = () => aw.docs('workorders').size +/** Das Anlegen eines Tickets schreibt eine Zeile ins Log (ticketAdmin); die Tests mit Ticket halten die Ausgabe sauber. Zurueck setzt afterEach (restoreAll). */ +const stilleTicketLog = () => mock.method(console, 'log', () => {}) +const TEAM = () => [...config.chatNotify.emails].sort() +const an = (mail) => mail.to.map((a) => a.address).sort() + +/** Unbezahlte Bestellung, deren Wartungs-Abo schon beendet ist: der Abgleich (ohne Abo) hat sie abgeschaltet, die Zahlung steht noch aus. */ +async function bestellungNachAboEnde() { + legeBestellung(1) + assert.deepEqual(await laufe(JETZT), ergebnis({ geprueft: 1, abgeschaltet: 1 })) + assert.equal(funktion().state, 'disabled') + assert.equal(funktion().disabledReason, 'abo_ende_vor_einrichtung') + assert.equal(funktion().unlockedByPurchase, false) + assert.equal(mails().length, 0) +} + +test('E10: Zahlung nach dem Abschalten - genau ein T19 (zahlung_nach_ende, Sitzung), kein Ticket, kein K15/T16, bezahlt gebucht, abgeschaltet; Wiederholung und async_payment_succeeded melden nichts mehr', async () => { + await bestellungNachAboEnde() + const ticketsVorher = tickets() + + await handleFeatureCheckoutEvent(zahlungsEreignis(COMPLETED)) + + assert.equal(t19().length, 1, 'genau ein T19') + const mailT19 = t19()[0] + assert.deepEqual(an(mailT19), TEAM(), 'T19 geht an das Team, nicht an den Kunden') + assert.match(mailT19.text, /Zahlung nach dem Abo-Ende/, 'Block zahlung_nach_ende') + assert.match(mailT19.text, /cs_test_1/, 'die Sitzungs-ID steht drin') + assert.match(mailT19.text, /Silke Müller/) + assert.match(mailT19.text, /Praxis Muster/) + assert.doesNotMatch(mailT19.text, /Ticket noch offen/, 'es gibt kein Ticket') + assert.deepEqual(betreffs(), [T19_BETREFF], 'sonst keine Mail: weder K15 noch T16 noch K22') + assert.equal(k15().length + t16().length, 0) + assert.equal(tickets(), ticketsVorher, 'kein Eintrag in workorders') + assert.equal(tickets(), 0) + + let doc = funktion() + assert.equal(doc.unlockedByPurchase, true) + assert.equal(doc.purchaseStatus, 'paid') + assert.equal(doc.zahlungMeldung, 'gemeldet') + assert.equal(doc.state, 'disabled', 'der Zustand bleibt abgeschaltet') + assert.equal(doc.enabled, false) + assert.equal(doc.disabledReason, 'abo_ende_vor_einrichtung') + assert.ok(!doc.ticketId, 'kein Ticket am Dokument') + assert.equal(doc.stripeSessionId, SITZUNG) + assert.match(doc.adminNote, /Zahlung eingegangen \(Sitzung cs_test_1\), nichts eingerichtet/, 'die Spur steht in der Notiz') + + // dasselbe Ereignis noch einmal (Stripe-Wiederholung) und das SEPA-Folgeereignis: stumm, nichts geschrieben + const gebucht = { ...doc } + await handleFeatureCheckoutEvent(zahlungsEreignis(COMPLETED)) + await handleFeatureCheckoutEvent(zahlungsEreignis(ASYNC_BEZAHLT)) + await handleFeatureCheckoutEvent(zahlungsEreignis(ASYNC_BEZAHLT)) + assert.equal(t19().length, 1, 'nie ein zweites T19') + assert.deepEqual(betreffs(), [T19_BETREFF]) + assert.deepEqual(funktion(), gebucht, 'nichts neu verbucht') + assert.equal(tickets(), 0) +}) + +test('E10: T19-Versand scheitert - der Webhook wirft (500), die Zahlung ist gebucht, die Meldung offen; jede Wiederholung wirft weiter, die erste mit funktionierendem Versand sendet genau ein T19', async () => { + await bestellungNachAboEnde() + t19Scheitert() + + await assert.rejects(handleFeatureCheckoutEvent(zahlungsEreignis(COMPLETED)), /Meldung zur Zahlung cs_test_1 \(abo_ende\) nicht gesendet/) + let doc = funktion() + assert.equal(doc.unlockedByPurchase, true, 'die Zahlung ist trotzdem gebucht') + assert.equal(doc.purchaseStatus, 'paid') + assert.equal(doc.zahlungMeldung, 'abo_ende', 'der Fall bleibt offen (nicht gemeldet)') + assert.equal(doc.state, 'disabled') + assert.equal(mails().length, 0) + assert.equal(tickets(), 0) + const gebucht = { ...doc } + + // Stripe wiederholt, der Versand geht weiter nicht: wieder 500, nichts neu verbucht + await assert.rejects(handleFeatureCheckoutEvent(zahlungsEreignis(COMPLETED)), /nicht gesendet/) + await assert.rejects(handleFeatureCheckoutEvent(zahlungsEreignis(ASYNC_BEZAHLT)), /nicht gesendet/) + assert.deepEqual(funktion(), gebucht) + assert.equal(mails().length, 0) + + // der Versand geht wieder: genau ein T19, danach ist die Wiederholung stumm + setMailTransportForTests(sink) + await handleFeatureCheckoutEvent(zahlungsEreignis(COMPLETED)) + assert.equal(t19().length, 1) + assert.match(t19()[0].text, /Zahlung nach dem Abo-Ende/) + assert.match(t19()[0].text, /cs_test_1/) + doc = funktion() + assert.equal(doc.zahlungMeldung, 'gemeldet') + assert.equal(doc.purchaseStatus, 'paid') + assert.equal(doc.state, 'disabled') + await handleFeatureCheckoutEvent(zahlungsEreignis(ASYNC_BEZAHLT)) + assert.deepEqual(betreffs(), [T19_BETREFF]) + assert.equal(tickets(), 0) +}) + +test('E10: Folgetag-Abgleich nach der Zahlung - K22 vor_einrichtung an den Kunden, aboEndeMailAt gesetzt, kein zweites T19 (Marker gemeldet), danach nichts mehr', async () => { + await bestellungNachAboEnde() + await handleFeatureCheckoutEvent(zahlungsEreignis(COMPLETED)) + assert.equal(t19().length, 1) + assert.ok(!funktion().aboEndeMailAt, 'K22 gehoert dem Abgleich') + + const tag2 = plus(JETZT, TAG) + assert.deepEqual(await laufe(tag2), ergebnis({ geprueft: 1, mails: 1 }), 'mails zaehlt K22, nicht T19') + assert.equal(k22().length, 1) + const mailK22 = k22()[0] + assert.deepEqual(an(mailK22), ['silke@example.com'], 'K22 geht an den Kunden') + assert.equal(mailK22.subject, 'Dein Terminplaner wurde beendet – Praxis Muster') + assert.match(mailK22.text, /noch vor der Einrichtung Deines Terminplaners/, 'Block vor_einrichtung') + assert.match(mailK22.text, /erstatten Dir die Einrichtung \(149 €\) vollständig/) + assert.match(mailK22.text, /am 05\.10\.2026 beendet/, 'Enddatum aus disabledAt') + assert.doesNotMatch(mailK22.text, /Was jetzt gilt|löschen wir/, 'es gibt nichts zu loeschen') + assert.equal(t19().length, 1, 'kein zweites T19') + assert.deepEqual(betreffs().map((s) => s === T19_BETREFF), [true, false], 'zuerst T19 (Webhook), dann K22 (Abgleich)') + const doc = funktion() + assert.equal(ms(doc.aboEndeMailAt), tag2.getTime()) + assert.equal(doc.zahlungMeldung, 'gemeldet') + assert.equal(doc.state, 'disabled') + assert.equal(doc.disabledReason, 'abo_ende_vor_einrichtung') + + // Tag 3 und eine spaete Wiederholung des Ereignisses: nichts mehr + const vorher = { ...doc } + assert.deepEqual(await laufe(plus(tag2, TAG)), ergebnis({ geprueft: 1 })) + await handleFeatureCheckoutEvent(zahlungsEreignis(ASYNC_BEZAHLT)) + assert.equal(mails().length, 2) + assert.deepEqual(funktion(), vorher) + assert.equal(tickets(), 0) +}) + +test('E10: Abgleich VOR der Wiederholung des Webhooks - Schritt 2b meldet T19 (die erste Verbuchung hat bezahlt), K22 folgt, die Wiederholung bleibt stumm', async () => { + await bestellungNachAboEnde() + t19Scheitert() + await assert.rejects(handleFeatureCheckoutEvent(zahlungsEreignis(COMPLETED)), /nicht gesendet/) + assert.equal(funktion().unlockedByPurchase, true) + assert.equal(funktion().zahlungMeldung, 'abo_ende') + assert.equal(mails().length, 0) + + // der Versand geht wieder, der Tagesabgleich kommt vor Stripes Wiederholung + setMailTransportForTests(sink) + const tag2 = plus(JETZT, TAG) + assert.deepEqual(await laufe(tag2), ergebnis({ geprueft: 1, mails: 1 })) + assert.equal(t19().length, 1, 'der Abgleich meldet T19') + assert.match(t19()[0].text, /Zahlung nach dem Abo-Ende/, 'als Zahlung nach dem Abo-Ende (der offene Fall steht am Dokument)') + assert.match(t19()[0].text, /cs_test_1/) + assert.equal(k22().length, 1) + assert.deepEqual(betreffs().map((s) => s === T19_BETREFF), [true, false], 'T19 vor K22') + const doc = funktion() + assert.equal(doc.zahlungMeldung, 'gemeldet') + assert.equal(ms(doc.aboEndeMailAt), tag2.getTime()) + assert.equal(doc.state, 'disabled') + + // jetzt kommt die Wiederholung des Webhooks: kein Wurf, kein zweites T19 + const vorher = { ...doc } + await handleFeatureCheckoutEvent(zahlungsEreignis(COMPLETED)) + await handleFeatureCheckoutEvent(zahlungsEreignis(ASYNC_BEZAHLT)) + assert.equal(t19().length, 1, 'nie doppelt') + assert.equal(mails().length, 2) + assert.deepEqual(funktion(), vorher) + assert.equal(tickets(), 0) +}) + +test('E10: Abgleich und Wiederholung des Webhooks gleichzeitig (Promise.all) - insgesamt ein T19, ein K22, kein Wurf', async () => { + await bestellungNachAboEnde() + t19Scheitert() + await assert.rejects(handleFeatureCheckoutEvent(zahlungsEreignis(COMPLETED)), /nicht gesendet/) + setMailTransportForTests(sink) + + const tag2 = plus(JETZT, TAG) + const [erg] = await Promise.all([laufe(tag2), handleFeatureCheckoutEvent(zahlungsEreignis(COMPLETED))]) + assert.deepEqual(erg, ergebnis({ geprueft: 1, mails: 1 }), 'der Abgleich sendet K22, T19 hoechstens einmal, gleich wer zuerst kommt') + assert.equal(t19().length, 1, 'insgesamt ein T19') + assert.equal(k22().length, 1) + assert.equal(mails().length, 2) + const doc = funktion() + assert.equal(doc.zahlungMeldung, 'gemeldet') + assert.equal(ms(doc.aboEndeMailAt), tag2.getTime()) + assert.deepEqual(await laufe(plus(tag2, TAG)), ergebnis({ geprueft: 1 })) + await handleFeatureCheckoutEvent(zahlungsEreignis(ASYNC_BEZAHLT)) + assert.equal(mails().length, 2) +}) + +test('E10: erste Zahlung und Tagesabgleich gleichzeitig (Promise.all) - gleich in welcher Reihenfolge: ein T19, nach dem naechsten Lauf ein K22, nie ein Doppel', async () => { + await bestellungNachAboEnde() + + const tag2 = plus(JETZT, TAG) + await Promise.all([laufe(tag2), handleFeatureCheckoutEvent(zahlungsEreignis(COMPLETED))]) + assert.equal(t19().length, 1, 'genau ein T19 (Webhook oder Abgleich, nie beide)') + assert.equal(funktion().purchaseStatus, 'paid') + assert.equal(funktion().unlockedByPurchase, true) + assert.equal(funktion().zahlungMeldung, 'gemeldet') + assert.equal(tickets(), 0) + + await laufe(plus(tag2, TAG)) + await handleFeatureCheckoutEvent(zahlungsEreignis(COMPLETED)) // Stripe-Wiederholung + await laufe(plus(tag2, 2 * TAG)) + assert.equal(t19().length, 1) + assert.equal(k22().length, 1, 'K22 genau einmal') + assert.equal(mails().length, 2) + assert.equal(k15().length + t16().length, 0) + assert.equal(funktion().state, 'disabled') +}) + +test('E10: Zahlung auf ein vom Team abgeschaltetes Dokument (Grund team) - T16 "abgeschaltet" wie in B10b, kein T19, kein Ticket, der Abgleich fasst es nie an', async () => { + legeBestellung(1) + await disable(F1, { by: 'Kenso', reason: 'team' }) + assert.equal(funktion().disabledReason, 'team') + + await handleFeatureCheckoutEvent(zahlungsEreignis(COMPLETED)) + assert.equal(t16().length, 1, 'genau ein T16') + assert.deepEqual(an(t16()[0]), TEAM()) + assert.match(t16()[0].text, /Bezahlt, aber schon vorhanden/, 'Block bereits_aktiv') + assert.match(t16()[0].text, /abgeschaltet/, 'Zustand abgeschaltet') + assert.match(t16()[0].text, /cs_test_1/) + assert.equal(t19().length, 0, 'kein T19: das Abo-Ende ist nicht der Grund') + assert.equal(k15().length, 0) + assert.deepEqual(betreffs(), [T16_BETREFF]) + assert.equal(tickets(), 0) + const doc = funktion() + assert.equal(doc.state, 'disabled') + assert.equal(doc.disabledReason, 'team') + assert.equal(doc.unlockedByPurchase, true) + assert.equal(doc.purchaseStatus, 'paid') + assert.equal(doc.zahlungMeldung, 'gemeldet') + + // der Abgleich (ohne Abo) laesst ein Team-Dokument in Ruhe: keine Abo-Abfrage, keine Mail; die Wiederholung ist stumm + aw.resetListCalls() + assert.deepEqual(await laufe(plus(JETZT, TAG)), ergebnis()) + assert.equal(abfragenAufAbos(), 0) + await handleFeatureCheckoutEvent(zahlungsEreignis(ASYNC_BEZAHLT)) + assert.deepEqual(betreffs(), [T16_BETREFF]) + assert.deepEqual(funktion(), doc) +}) + +for (const [name, legeDokument] of [ + ['ordered', () => legeBestellung(1)], + ['setup ohne Ticket (Ticket fehlt, Stripe wiederholt)', () => legeFunktion(1, { state: 'setup', enabled: false, stripeSessionId: SITZUNG })], +]) { + test(`E10: Zahlung bei laufendem Abo (${name}) - unveraendert: Ticket, K15 an den Kunden, T16 ohne kein_abo, kein T19; die Wiederholung legt nichts doppelt an`, async () => { + stilleTicketLog() + wartung('active') + legeDokument() + assert.deepEqual(await laufe(JETZT), ergebnis({ geprueft: 1 }), 'der Abgleich fasst eine Bestellung bei laufendem Abo nicht an') + assert.equal(funktion().state, name === 'ordered' ? 'ordered' : 'setup') + + const ereignis = zahlungsEreignis(COMPLETED) + await handleFeatureCheckoutEvent(ereignis) + assert.equal(tickets(), 1, 'ein Einrichtungs-Ticket') + const doc = funktion() + assert.equal(doc.state, 'setup') + assert.equal(doc.unlockedByPurchase, true) + assert.equal(doc.purchaseStatus, 'paid') + assert.ok(aw.docs('workorders').has(doc.ticketId), 'das Ticket haengt am Dokument') + assert.ok(!doc.zahlungMeldung, 'kein Zahlungsfall') + + assert.equal(k15().length, 1) + assert.deepEqual(an(k15()[0]), ['silke@example.com']) + assert.equal(t16().length, 1) + assert.deepEqual(an(t16()[0]), TEAM()) + assert.doesNotMatch(t16()[0].text, /Kein laufendes Wartungs-Abo/, 'das Abo laeuft') + assert.doesNotMatch(t16()[0].text, /Bezahlt, aber schon vorhanden/) + assert.match(t16()[0].text, new RegExp(`Ticket: ${aw.docs('workorders').get(doc.ticketId).woid}\\b`), 'T16 nennt das Ticket') + assert.equal(t19().length, 0) + assert.equal(mails().length, 2) + + // Stripe wiederholt: kein zweites Ticket, keine weitere Mail; der Abgleich bleibt dabei ruhig + await handleFeatureCheckoutEvent(ereignis) + await handleFeatureCheckoutEvent(zahlungsEreignis(ASYNC_BEZAHLT)) + assert.equal(tickets(), 1) + assert.equal(mails().length, 2) + assert.deepEqual(await laufe(plus(JETZT, TAG)), ergebnis({ geprueft: 1 })) + assert.equal(mails().length, 2) + assert.equal(funktion().state, 'setup') + }) +} + +test('E10: Rennen - Zahlung VOR dem Abgleich: Ticket, K15 und T16 (mit kein_abo), dann der Abgleich: T19 und K22; Stripe-Retry desselben Ereignisses ist stumm', async () => { + stilleTicketLog() + legeBestellung(1) // das Wartungs-Abo ist beendet (kein Abo-Dokument), die Bestellung wartet noch auf die Zahlung + const ereignis = zahlungsEreignis(COMPLETED) + + await handleFeatureCheckoutEvent(ereignis) + assert.equal(funktion().state, 'setup', 'gebucht und im Aufbau') + assert.equal(tickets(), 1, 'Ticket angelegt') + assert.equal(k15().length, 1) + assert.equal(t16().length, 1) + assert.match(t16()[0].text, /Kein laufendes Wartungs-Abo/, 'T16 sagt dem Team, dass das Abo fehlt') + assert.equal(t19().length, 0, 'noch kein T19: das Dokument ist nicht abgeschaltet') + const ticketId = funktion().ticketId + assert.ok(ticketId) + + // der Abgleich kommt danach: bezahlter Aufbau ohne Abo => abgeschaltet, T19 (ohne "nach dem Abo-Ende"), K22 vor_einrichtung + assert.deepEqual(await laufe(JETZT), ergebnis({ geprueft: 1, abgeschaltet: 1, mails: 1 })) + assert.equal(t19().length, 1, 'T19 hoechstens einmal') + assert.doesNotMatch(t19()[0].text, /Zahlung nach dem Abo-Ende/, 'die Zahlung kam vor dem Abo-Ende') + assert.match(t19()[0].text, /Ticket noch offen/) + assert.equal(k22().length, 1) + assert.equal(tickets(), 1, 'kein zweites Ticket') + assert.equal(aw.docs('workorders').get(ticketId).status, 'Open', 'das Ticket bleibt offen, das Team erstattet') + let doc = funktion() + assert.equal(doc.state, 'disabled') + assert.equal(doc.disabledReason, 'abo_ende_vor_einrichtung') + assert.equal(doc.zahlungMeldung, 'gemeldet') + assert.equal(doc.purchaseStatus, 'paid') + assert.equal(doc.ticketId, ticketId) + assert.equal(mails().length, 4, 'K15, T16, T19, K22') + + // Stripe wiederholt dasselbe Ereignis (und das SEPA-Folgeereignis): stumm, nichts geschrieben, kein zweites Ticket + const vorher = { ...doc } + await handleFeatureCheckoutEvent(ereignis) + await handleFeatureCheckoutEvent(zahlungsEreignis(ASYNC_BEZAHLT)) + assert.equal(mails().length, 4) + assert.equal(t19().length, 1) + assert.equal(tickets(), 1) + assert.deepEqual(funktion(), vorher) + assert.deepEqual(await laufe(plus(JETZT, TAG)), ergebnis({ geprueft: 1 })) + assert.equal(mails().length, 4) +})