Files
Webklar-Kundenbereich/test/shopMails.test.js
T
ClaudeandClaude Opus 5.5 0ebd2e6663 Webshop E4: Mails (G09-G14 an Käufer, K25/K26 an Händler), shop/mails.js, Mailversand der Warteschlange auch bei Terminplaner off
Käufermails laufen über die Warteschlange des Terminplaners (G09/G12 mit Vorrang),
G09 wird bei Überlänge um den AGB-Text gekürzt (Link statt Text). Der Mailteil des
Minutenjobs und die Aufräumschritte des Tagesjobs laufen auch bei TERMINE_ENABLED=off,
solange SHOP_ENABLED nicht off ist.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 08:56:40 +00:00

361 lines
19 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { test, after, beforeEach, afterEach, mock } from 'node:test'
import assert from 'node:assert/strict'
import { startAppwriteMock } from './support/appwriteMock.js'
import { mailSink } from './support/mailSink.js'
import { schemaKeys } from './support/schema.js'
import { useTestEnv } from './support/testEnv.js'
import { TERMINE_DATETIMES } from '../server/services/termine/schema.js'
// Webshop E4: Mails des Shops (services/shop/mails.js). Käufermails laufen durch die Warteschlange des Terminplaners
// (termineOutbox), Händlermails gehen direkt an das Kundenkonto. Mails landen in der Senke, nichts verlässt den Prozess.
const aw = await startAppwriteMock({
schema: { termineOutbox: schemaKeys('termineOutbox') },
datetimes: TERMINE_DATETIMES,
defaultLimit: 25,
})
after(() => aw.close())
useTestEnv({ appwriteUrl: aw.url, stripeUrl: 'http://127.0.0.1:1' })
const { config } = await import('../server/config.js')
const { deleteDocument } = await import('../server/services/appwriteAdmin.js')
const { setMailTransportForTests } = await import('../server/services/mailer.js')
const { resetOutboxForTests, warteAufVersand } = await import('../server/services/termine/outbox.js')
const { STANDARD_EINSTELLUNGEN } = await import('../server/services/shop/einstellungen.js')
const { gewaehrleistungsHinweis } = await import('../server/services/shop/rechtstexte.js')
const M = await import('../server/services/shop/mails.js')
const { anKaeufer, anHaendler, kaeuferWerte, haendlerK25, haendlerK26 } = M
after(() => setMailTransportForTests(null))
const JETZT = new Date('2026-10-05T12:03:00.000Z') // 14:03 Berlin
const SITE_KEY = 'abcdefghijklmnop'
const ABSENDER_VORHER = config.smtp.from
const BASIS_VORHER = config.publicBaseUrl
const einstellungen = (over = {}) => ({
...JSON.parse(JSON.stringify(STANDARD_EINSTELLUNGEN)),
shopname: 'Töpferei Muster',
anbieter: { name: 'Erika Muster', strasse: 'Hauptstr. 1', plz: '67722', ort: 'Winnweiler', land: 'DE', email: 'info@toepferei.example', telefon: '', ustId: '' },
lieferzeit: '2–4 Werktage',
akzent: '#aa3300',
abholung: { aktiv: true, adresse: 'Werkstatt, Hauptstr. 1, 67722 Winnweiler', hinweis: 'Di–Fr 10–16 Uhr' },
...over,
})
const texte = (over = {}) => ({
agb: 'AGB Zeile 1\nAGB Zeile 2',
widerrufsbelehrung: 'Belehrung Zeile 1\nZeile 2',
widerrufsformular: 'An Töpferei Muster\nHiermit widerrufe ich',
impressumUrl: 'https://toepferei.example/impressum',
datenschutzUrl: 'https://toepferei.example/datenschutz',
...over,
})
const ctxMit = (e = einstellungen(), t = texte()) => ({
doc: { $id: 'f_shop01', customerId: 'c1', projectId: 'p1', featureKey: 'shop', siteKey: SITE_KEY },
shop: {
id: 'f_shop01',
customerId: 'c1',
projectId: 'p1',
siteKey: SITE_KEY,
einstellungen: e,
rechtstexte: { entwurf: texte(), bestaetigt: { texte: t, hash: 'h1', at: JETZT.toISOString(), von: 'c1' } },
},
})
const bestellung = (over = {}, snap = {}) => ({
$id: 'o_bestellung01',
bestellnummer: '26-K7QM2X',
kaeuferEmail: 'max@example.com',
bezahltAt: JETZT.toISOString(),
erstelltAt: new Date(JETZT.getTime() - 600000).toISOString(),
summeCent: 4140,
bestellung: JSON.stringify({
positionen: [
{ name: 'Tasse', varianteText: 'blau', menge: 2, summeCent: 2400, widerruf: 'gesetzlich' },
{ name: 'Teller', varianteText: '', menge: 1, summeCent: 1250, summeText: '12,50 €', widerruf: 'individuell' },
],
summen: { zwischensummeCent: 3650, versandCent: 490, gesamtCent: 4140, texte: { zwischensumme: '36,50 €', versand: '4,90 €', gesamt: '41,40 €', steuerHinweis: 'Gemäß § 19 UStG wird keine Umsatzsteuer berechnet.' } },
lieferart: 'versand',
land: 'DE',
kaeufer: { name: 'Max Mustermann', email: 'max@example.com' },
lieferadresse: { name: 'Max Mustermann', line1: 'Musterweg 3', postal_code: '12345', city: 'Musterstadt', country: 'DE' },
zahlungsart: 'card',
...snap,
}),
...over,
})
let sink
let log
beforeEach(async () => {
for (const id of aw.docs('termineOutbox').keys()) await deleteDocument(config.collections.termineOutbox, id)
for (const id of aw.docs('customers').keys()) await deleteDocument(config.collections.customers, id)
resetOutboxForTests()
config.smtp.from = 'noreply@webklar.com'
config.publicBaseUrl = 'https://project.webklar.com'
sink = mailSink()
setMailTransportForTests(sink)
log = { error: mock.method(console, 'error', () => {}), warn: mock.method(console, 'warn', () => {}) }
aw.seed('customers', { $id: 'c1', name: 'Erika Muster', email: 'erika@toepferei.example' })
})
afterEach(async () => {
await warteAufVersand()
mock.restoreAll()
config.smtp.from = ABSENDER_VORHER
config.publicBaseUrl = BASIS_VORHER
})
const eintraege = () => [...aw.docs('termineOutbox').values()]
const warnLog = () => log.warn.mock.calls.map((aufruf) => aufruf.arguments.map(String).join(' '))
// ---------------------------------------------------------------- Werte
test('kaeuferWerte: Kopf, Fuß, Positionen, Summen, Lieferadresse und Rechtstexte aus Schnappschuss und Shop', () => {
const w = kaeuferWerte(ctxMit(), bestellung(), JETZT)
// euro() setzt ein geschütztes Leerzeichen vor das €-Zeichen
for (const [schluessel, wert] of Object.entries(w)) w[schluessel] = wert.replaceAll('\u00a0', ' ')
assert.equal(w.farbe, '#aa3300')
assert.equal(w.betrieb_name, 'Töpferei Muster')
assert.equal(w.anbieter_zeile, 'Erika Muster, Hauptstr. 1, 67722 Winnweiler, info@toepferei.example')
assert.equal(w.widerruf_url, `https://project.webklar.com/shop/${SITE_KEY}/widerruf`)
assert.equal(w.impressum_url, 'https://toepferei.example/impressum')
assert.equal(w.datenschutz_url, 'https://toepferei.example/datenschutz')
assert.equal(w.bestellnummer, '26-K7QM2X')
assert.equal(w.datum, '05.10.2026')
assert.equal(w.positionen, '2 × Tasse (blau) – 24,00 €\n1 × Teller – 12,50 €')
assert.deepEqual([w.zwischensumme, w.versand, w.gesamt], ['36,50 €', '4,90 €', '41,40 €'])
assert.equal(w.steuer_hinweis, 'Gemäß § 19 UStG wird keine Umsatzsteuer berechnet.')
assert.equal(w.lieferart_text, 'Versand nach Deutschland')
assert.equal(w.lieferadresse, 'Max Mustermann\nMusterweg 3\n12345 Musterstadt')
assert.equal(w.abholort, 'Werkstatt, Hauptstr. 1, 67722 Winnweiler')
assert.equal(w.lieferzeit, '2–4 Werktage')
assert.equal(w.zahlungsart, 'Kreditkarte, Apple Pay, Google Pay')
assert.equal(w.ausschluss_text, 'Teller: Vom Widerruf ausgeschlossen: nach Kundenwunsch angefertigt')
assert.equal(w.agb, 'AGB Zeile 1\nAGB Zeile 2')
assert.equal(w.widerrufsbelehrung, 'Belehrung Zeile 1\nZeile 2')
assert.equal(w.widerrufsformular, 'An Töpferei Muster\nHiermit widerrufe ich')
assert.equal(w.gewaehrleistung, gewaehrleistungsHinweis())
})
test('kaeuferWerte: Links fallen ohne Impressum-/Datenschutz-Adresse auf den Shop zurück, ungültige Farbe auf Schwarz, ohne Bestellung "–"', () => {
const w = kaeuferWerte(ctxMit(einstellungen({ akzent: 'red;x' }), texte({ impressumUrl: '', datenschutzUrl: '' })), null, JETZT)
assert.equal(w.farbe, '#111111')
assert.equal(w.impressum_url, `https://project.webklar.com/shop/${SITE_KEY}`)
assert.equal(w.datenschutz_url, `https://project.webklar.com/shop/${SITE_KEY}`)
assert.equal(w.bestellnummer, '–')
assert.equal(w.positionen, '–')
assert.equal(w.lieferadresse, '–')
assert.equal(w.datum, '05.10.2026', 'ohne Bestellung gilt das übergebene Datum')
})
// ---------------------------------------------------------------- Käufermails
test('G09: wird mit Shopname als Absender und Antwort an den Händler eingereiht, kein BCC, Rechtstexte stehen in der Mail', async () => {
const erg = await anKaeufer('G09', ctxMit(), bestellung(), {}, JETZT)
assert.equal(erg.ok, true)
assert.equal(erg.gekuerzt, false)
await warteAufVersand()
const [doc] = eintraege()
assert.equal(doc.art, 'G09')
assert.equal(doc.featureId, 'f_shop01')
assert.equal(doc.bookingId, 'o_bestellung01')
assert.equal(doc.fromName, 'Töpferei Muster')
assert.equal(doc.replyTo, 'info@toepferei.example')
assert.equal(doc.status, 'gesendet')
assert.equal(doc.an, '', 'nach dem Versand ist die Adresse gelöscht')
assert.equal(sink.sent.length, 1)
const mail = sink.sent[0]
assert.deepEqual(mail.to.map((t) => t.address), ['max@example.com'])
assert.ok(!mail.bcc, 'kein BCC')
assert.equal(mail.from.name, 'Töpferei Muster')
assert.equal(mail.from.address, 'noreply@webklar.com')
assert.equal(mail.replyTo[0].address, 'info@toepferei.example')
assert.equal(mail.subject, 'Ihre Bestellung 26-K7QM2X bei Töpferei Muster')
for (const teil of ['2 × Tasse (blau) – 24,00 €', '41,40 €', 'Max Mustermann', 'Musterweg 3', 'Vom Widerruf ausgeschlossen', 'AGB Zeile 2', 'Belehrung Zeile 1', 'Hiermit widerrufe ich', 'GESETZLICHE GEWÄHRLEISTUNG', 'Vertrag widerrufen']) {
assert.ok(mail.text.includes(teil), teil)
}
assert.match(mail.html, new RegExp(`href="https://project\\.webklar\\.com/shop/${SITE_KEY}/widerruf"`))
assert.doesNotMatch(mail.text, /Abholort/)
assert.deepEqual(warnLog(), [], 'keine Platzhalter-Warnung der Vorlage')
})
test('G09 Abholung: Abholort statt Lieferadresse', async () => {
const erg = await anKaeufer('G09', ctxMit(), bestellung({}, { lieferart: 'abholung', lieferadresse: null, land: null, positionen: [{ name: 'Tasse', menge: 1, summeCent: 1200, widerruf: 'gesetzlich' }] }), {}, JETZT)
assert.equal(erg.ok, true)
await warteAufVersand()
const text = sink.sent[0].text
assert.match(text, /Lieferart: Abholung/)
assert.match(text, /Abholort: Werkstatt, Hauptstr\. 1, 67722 Winnweiler/)
assert.doesNotMatch(text, /Lieferadresse|Hinweis zum Widerrufsrecht/)
assert.deepEqual(warnLog(), [])
})
test('G09: zu lang für die Warteschlange ⇒ der AGB-Text wird durch einen Link auf die AGB ersetzt, Belehrung und Formular bleiben', async () => {
const lang = 'Dies ist ein langer Satz der Geschäftsbedingungen.\n'.repeat(560) // etwa 28 000 Zeichen
const t = texte({ agb: lang, widerrufsbelehrung: 'Belehrung Satz.\n'.repeat(1000), widerrufsformular: 'Formular Zeile\n'.repeat(300) })
const erg = await anKaeufer('G09', ctxMit(einstellungen(), t), bestellung(), {}, JETZT)
assert.equal(erg.ok, true)
assert.equal(erg.gekuerzt, true)
const doc = eintraege()[0]
assert.ok(doc.html.length <= 100000 && doc.text.length <= 50000, `${doc.html.length}/${doc.text.length}`)
assert.match(doc.text, new RegExp(`Die vollständigen AGB finden Sie unter https://project\\.webklar\\.com/shop/${SITE_KEY}\\?ansicht=agb`))
assert.doesNotMatch(doc.text, /langer Satz der Geschäftsbedingungen/)
assert.match(doc.text, /Belehrung Satz\./)
assert.match(doc.text, /Formular Zeile/)
assert.match(doc.text, /GESETZLICHE GEWÄHRLEISTUNG/)
})
test('G09: auch gekürzt zu lang ⇒ { ok: false } ohne Wurf und ohne Eintrag', async () => {
const t = texte({ widerrufsbelehrung: 'x'.repeat(60000) })
const erg = await anKaeufer('G09', ctxMit(einstellungen(), t), bestellung(), {}, JETZT)
assert.deepEqual(erg, { ok: false, fehler: 'Die E-Mail an den Käufer konnte nicht eingereiht werden.' })
assert.equal(eintraege().length, 0)
assert.equal(sink.sent.length, 0)
assert.doesNotMatch(warnLog().join('\n'), /max@example\.com/)
})
test('G10: Versanddienst und Sendungsnummer (Block sendung); ohne beide kein Block', async () => {
assert.equal((await anKaeufer('G10', ctxMit(), bestellung(), { versanddienst: 'DHL', sendungsnummer: '00340434' }, JETZT)).ok, true)
assert.equal((await anKaeufer('G10', ctxMit(), bestellung({ $id: 'o_zwei' }), {}, JETZT)).ok, true)
await warteAufVersand()
const [mit, ohne] = sink.sent
assert.equal(mit.subject, 'Ihre Bestellung 26-K7QM2X wurde versendet')
assert.match(mit.text, /Versanddienst: DHL/)
assert.match(mit.text, /Sendungsnummer: 00340434/)
assert.doesNotMatch(ohne.text, /Sendungsnummer/)
})
test('G11: Abholort und Hinweis des Händlers, ersatzweise ein Standardhinweis', async () => {
await anKaeufer('G11', ctxMit(), bestellung(), {}, JETZT)
const ohneHinweis = einstellungen({ abholung: { aktiv: true, adresse: 'Werkstatt', hinweis: '' } })
await anKaeufer('G11', ctxMit(ohneHinweis), bestellung({ $id: 'o_zwei' }), {}, JETZT)
await warteAufVersand()
const texte11 = sink.sent.map((m) => m.text)
assert.match(texte11[0], /Abholort: Werkstatt, Hauptstr\. 1/)
assert.match(texte11[0], /Hinweis: Di–Fr 10–16 Uhr/)
assert.match(texte11[1], /Hinweis: Bitte bringen Sie Ihre Bestellnummer mit\./)
assert.equal(sink.sent[0].subject, 'Ihre Bestellung 26-K7QM2X ist abholbereit')
})
test('G12: ohne passende Bestellung, an die Adresse der Erklärung, mit Datum und Uhrzeit; Vorrang-Klasse', async () => {
const erg = await anKaeufer('G12', ctxMit(), null, { an: 'kaeufer@example.com', bookingId: 'w_widerruf01', name: 'Kai Käufer', bestellnummer: '26-ZZZZZZ', eingang: JETZT, nachricht: 'Ich widerrufe.' }, JETZT)
assert.equal(erg.ok, true)
assert.equal(erg.eintrag.bookingId, 'w_widerruf01')
await warteAufVersand()
const mail = sink.sent[0]
assert.deepEqual(mail.to.map((t) => t.address), ['kaeufer@example.com'])
assert.equal(mail.subject, 'Eingangsbestätigung Ihres Widerrufs bei Töpferei Muster')
assert.match(mail.text, /Guten Tag Kai Käufer,/)
assert.match(mail.text, /Eingegangen am: 05\.10\.2026, 14:03 Uhr/)
assert.match(mail.text, /Bestellnummer: 26-ZZZZZZ/)
assert.match(mail.text, /Ihre Erklärung: Ich widerrufe\./)
assert.deepEqual(warnLog(), [])
})
test('G13: erstatteter Betrag in Euro, Grund nur wenn angegeben; ohne Betrag { ok: false }', async () => {
assert.equal((await anKaeufer('G13', ctxMit(), bestellung(), { betragCent: 1250, grund: 'Artikel beschädigt' }, JETZT)).ok, true)
assert.equal((await anKaeufer('G13', ctxMit(), bestellung({ $id: 'o_zwei' }), { betragCent: 4140 }, JETZT)).ok, true)
assert.equal((await anKaeufer('G13', ctxMit(), bestellung({ $id: 'o_drei' }), {}, JETZT)).ok, false)
assert.equal((await anKaeufer('G13', ctxMit(), bestellung({ $id: 'o_vier' }), { betragCent: 12.5 }, JETZT)).ok, false)
await warteAufVersand()
assert.equal(sink.sent.length, 2)
assert.match(sink.sent[0].text, /Erstatteter Betrag: 12,50 €/)
assert.match(sink.sent[0].text, /Grund: Artikel beschädigt/)
assert.match(sink.sent[1].text, /Erstatteter Betrag: 41,40 €/)
assert.doesNotMatch(sink.sent[1].text, /Grund:/)
assert.match(sink.sent[1].text, /Die Gutschrift erfolgt über die ursprüngliche Zahlungsart\./)
})
test('G14: Zahlung fehlgeschlagen, nichts berechnet', async () => {
assert.equal((await anKaeufer('G14', ctxMit(), bestellung(), {}, JETZT)).ok, true)
await warteAufVersand()
assert.equal(sink.sent[0].subject, 'Ihre Bestellung 26-K7QM2X wurde nicht abgeschlossen')
assert.match(sink.sent[0].text, /es wurde nichts berechnet/)
})
test('anKaeufer wirft nie: unbekannte Art, fehlende Adresse und Speicherfehler ergeben { ok: false }', async () => {
assert.equal((await anKaeufer('G01', ctxMit(), bestellung(), {}, JETZT)).ok, false)
assert.equal((await anKaeufer('G09', ctxMit(), bestellung({ kaeuferEmail: '' }), {}, JETZT)).ok, false)
assert.equal((await anKaeufer('G09', ctxMit(), bestellung(), {}, new Date('kaputt'))).ok, false)
aw.failNext({ collection: 'termineOutbox', method: 'POST', status: 500, message: 'Nachbau kaputt' })
const erg = await anKaeufer('G09', ctxMit(), bestellung(), {}, JETZT)
assert.equal(erg.ok, false)
assert.equal(eintraege().length, 0)
assert.equal(sink.sent.length, 0)
})
test('Käufermails zählen als Gästemails: G09 und G12 mit Vorrang, die übrigen normal', async () => {
const { mailKlasse } = await import('../server/services/termine/outbox.js')
assert.deepEqual(['G09', 'G10', 'G11', 'G12', 'G13', 'G14'].map(mailKlasse), ['vorrang', 'normal', 'normal', 'vorrang', 'normal', 'normal'])
})
// ---------------------------------------------------------------- Händlermails
test('K25: geht an das Kundenkonto, ohne BCC ans Team, mit Käufer, Positionen und Portal-Link', async () => {
const { values, blocks } = haendlerK25(ctxMit(), bestellung())
assert.deepEqual(blocks, { ueberverkauft: false, sepa: false })
const erg = await anHaendler('K25', ctxMit(), values, blocks)
assert.deepEqual(erg, { ok: true })
assert.equal(sink.sent.length, 1)
const mail = sink.sent[0]
assert.deepEqual(mail.to.map((t) => t.address), ['erika@toepferei.example'])
assert.ok(!mail.bcc, 'kein BCC ans Team')
assert.equal(mail.subject, 'Neue Bestellung 26-K7QM2X in Deinem Webshop')
assert.match(mail.text, /Hallo Erika Muster/)
assert.match(mail.text, /Töpferei Muster/)
assert.match(mail.text, /Käufer: Max Mustermann/)
assert.match(mail.text, /Gesamt: 41,40 €/)
assert.match(mail.html.replaceAll('\u00a0', ' '), /2 × Tasse \(blau\) – 24,00 €<br>1 × Teller – 12,50 €/)
assert.match(mail.html, /href="https:\/\/project\.webklar\.com\/dashboard\.html#webshop\?projekt=p1"/)
assert.doesNotMatch(mail.text, /Achtung|Lastschrift/)
assert.deepEqual(warnLog(), [])
})
test('K25: Blöcke ueberverkauft und sepa', async () => {
const { values, blocks } = haendlerK25(ctxMit(), bestellung({}, { zahlungsart: 'sepa_debit' }), { ueberverkauft: true })
assert.deepEqual(blocks, { ueberverkauft: true, sepa: true })
assert.equal((await anHaendler('K25', ctxMit(), values, blocks)).ok, true)
assert.match(sink.sent[0].text, /Achtung: Bestand reichte nicht/)
assert.match(sink.sent[0].text, /Zahlung per Lastschrift/)
})
test('K26: Widerruf mit Eingang in Berliner Zeit und Erklärung, ohne BCC; ohne Erklärung kein Block', async () => {
const widerruf = { name: 'Kai <K>', email: 'kai@example.com', bestellnummer: '26-ZZZZZZ', nachricht: 'Ich widerrufe.', eingangAt: JETZT.toISOString() }
const { values, blocks } = haendlerK26(ctxMit(), widerruf)
assert.equal(values.eingang, '05.10.2026, 14:03 Uhr')
assert.deepEqual(blocks, { nachricht: true })
assert.equal((await anHaendler('K26', ctxMit(), values, blocks)).ok, true)
const ohne = haendlerK26(ctxMit(), { ...widerruf, nachricht: ' ' })
assert.deepEqual(ohne.blocks, { nachricht: false })
assert.equal((await anHaendler('K26', ctxMit(), ohne.values, ohne.blocks)).ok, true)
const [mit, kurz] = sink.sent
assert.ok(!mit.bcc)
assert.equal(mit.subject, 'Widerruf in Deinem Webshop eingegangen')
assert.match(mit.text, /Eingegangen am: 05\.10\.2026, 14:03 Uhr/)
assert.match(mit.text, /Käufer: Kai <K>/)
assert.match(mit.text, /E-Mail: kai@example\.com/)
assert.match(mit.text, /Erklärung: Ich widerrufe\./)
assert.match(mit.html, /Kai &lt;K&gt;/)
assert.doesNotMatch(kurz.text, /Erklärung:/)
})
test('anHaendler wirft nie: Kunde fehlt oder ohne E-Mail, Versandfehler ⇒ { ok: false } und Warnung ohne Adresse', async () => {
const { values, blocks } = haendlerK25(ctxMit(), bestellung())
const fremd = ctxMit()
fremd.shop.customerId = 'c_gibtsnicht'
assert.equal((await anHaendler('K25', fremd, values, blocks)).ok, false)
aw.seed('customers', { $id: 'c_ohne', name: 'Ohne', email: '' })
const ohneMail = ctxMit()
ohneMail.shop.customerId = 'c_ohne'
assert.equal((await anHaendler('K25', ohneMail, values, blocks)).ok, false)
setMailTransportForTests({
async sendMail() {
throw new Error('SMTP erika@toepferei.example kaputt')
},
})
assert.deepEqual(await anHaendler('K25', ctxMit(), values, blocks), { ok: false, fehler: 'Die E-Mail an den Händler konnte nicht gesendet werden.' })
assert.equal(sink.sent.length, 0)
assert.doesNotMatch(warnLog().join('\n'), /erika@toepferei\.example/, 'keine Adresse im Log')
assert.ok(warnLog().some((zeile) => /K25 nicht gesendet/.test(zeile)))
})