364 lines
18 KiB
JavaScript
364 lines
18 KiB
JavaScript
import { test, after } from 'node:test'
|
||
import assert from 'node:assert/strict'
|
||
import { useTestEnv } from './support/testEnv.js'
|
||
import { mailSink } from './support/mailSink.js'
|
||
|
||
// Kalenderdatei (RFC 5545/5546) und Gaesteversand. Kein Netz: der Mailversand laeuft ueber die Senke, config.js braucht
|
||
// nur eine gueltige Umgebung.
|
||
useTestEnv({ appwriteUrl: 'http://127.0.0.1:1', stripeUrl: 'http://127.0.0.1:1' })
|
||
const { config } = await import('../server/config.js')
|
||
const { icsTermin, icsKalender } = await import('../server/services/termine/ics.js')
|
||
const { sendGuestMail, setMailTransportForTests } = await import('../server/services/mailer.js')
|
||
after(() => setMailTransportForTests(null))
|
||
|
||
const JETZT = new Date('2026-10-02T09:00:00.000Z')
|
||
const termin = (extra = {}) => ({
|
||
uid: 'b_9f3a1c@termine.webklar.com',
|
||
sequenz: 2,
|
||
methode: 'REQUEST',
|
||
start: '2026-10-05T10:00:00.000+02:00', // 08:00 UTC
|
||
ende: new Date('2026-10-05T08:45:00.000Z'),
|
||
titel: 'Haarschnitt – Salon Muster',
|
||
ort: 'Hauptstraße 1, 67655 Kaiserslautern',
|
||
beschreibung: 'Buchungsnummer AB12CD34',
|
||
organisator: { name: 'Salon Muster', email: 'info@salon-muster.de' },
|
||
teilnehmer: { name: 'Anna Müller', email: 'anna@example.com' },
|
||
jetzt: JETZT,
|
||
...extra,
|
||
})
|
||
|
||
const enc = new TextEncoder()
|
||
const octets = (s) => enc.encode(s).length
|
||
/**
|
||
* Jede physische Zeile hoechstens 75 Oktette und wohlgeformtes UTF-16 (kein halbes Surrogatpaar). Ein Decode-Test
|
||
* (TextDecoder) taugt dafuer nicht: JS-Strings kodieren immer, einzelne Surrogate werden still zu U+FFFD.
|
||
*/
|
||
const pruefeZeilen = (text) => {
|
||
for (const zeile of text.split('\r\n')) {
|
||
assert.ok(octets(zeile) <= 75, `Zeile zu lang (${octets(zeile)}): ${zeile}`)
|
||
assert.ok(zeile.isWellFormed(), `Zeile mit halbem Surrogatpaar: ${JSON.stringify(zeile)}`)
|
||
}
|
||
}
|
||
/** Zeilenfaltung rueckgaengig machen: CRLF + Leerzeichen faellt weg. */
|
||
const entfalte = (text) => text.replace(/\r\n /g, '')
|
||
/** Logische Zeilen (entfaltet, ohne das abschliessende CRLF). */
|
||
const zeilen = (text) => entfalte(text).split('\r\n').slice(0, -1)
|
||
const entescape = (s) => s.replace(/\\([\\;,nN])/g, (_, c) => (c === 'n' || c === 'N' ? '\n' : c))
|
||
const wert = (text, name) => zeilen(text).find((l) => l.startsWith(`${name}:`) || l.startsWith(`${name};`))
|
||
|
||
test('Aufbau: CRLF, Kopf, genau ein Termin, UTC-Zeiten, UID, SEQUENCE, DTSTAMP', () => {
|
||
const ics = icsTermin(termin())
|
||
assert.ok(ics.endsWith('END:VCALENDAR\r\n'), 'endet mit CRLF')
|
||
assert.ok(!/[^\r]\n|\r[^\n]/.test(ics), 'nur CRLF, kein einzelnes CR oder LF')
|
||
assert.deepEqual(zeilen(ics), [
|
||
'BEGIN:VCALENDAR',
|
||
'VERSION:2.0',
|
||
'PRODID:-//WEBklar//Terminplaner//DE',
|
||
'CALSCALE:GREGORIAN',
|
||
'METHOD:REQUEST',
|
||
'BEGIN:VEVENT',
|
||
'UID:b_9f3a1c@termine.webklar.com',
|
||
'DTSTAMP:20261002T090000Z',
|
||
'SEQUENCE:2',
|
||
'DTSTART:20261005T080000Z',
|
||
'DTEND:20261005T084500Z',
|
||
'SUMMARY:Haarschnitt – Salon Muster',
|
||
'DESCRIPTION:Buchungsnummer AB12CD34',
|
||
'LOCATION:Hauptstraße 1\\, 67655 Kaiserslautern',
|
||
'ORGANIZER;CN="Salon Muster":mailto:info@salon-muster.de',
|
||
'ATTENDEE;CN="Anna Müller";ROLE=REQ-PARTICIPANT;PARTSTAT=ACCEPTED;RSVP=FALSE:mailto:anna@example.com',
|
||
'STATUS:CONFIRMED',
|
||
'TRANSP:OPAQUE',
|
||
'END:VEVENT',
|
||
'END:VCALENDAR',
|
||
])
|
||
})
|
||
|
||
test('Zeiten: Date, ISO mit Zone und Millisekunden werden gleich zu UTC; Sekundenbruchteile fallen weg', () => {
|
||
const a = icsTermin(termin({ start: new Date('2026-10-05T08:00:00.999Z'), ende: Date.UTC(2026, 9, 5, 8, 45) }))
|
||
assert.equal(wert(a, 'DTSTART'), 'DTSTART:20261005T080000Z')
|
||
assert.equal(wert(a, 'DTEND'), 'DTEND:20261005T084500Z')
|
||
// Sommerzeitende (Berlin 2026-10-25): 02:30 +01:00 ist 01:30 UTC
|
||
const b = icsTermin(termin({ start: '2026-10-25T02:30:00+01:00', ende: '2026-10-25T03:00:00+01:00' }))
|
||
assert.equal(wert(b, 'DTSTART'), 'DTSTART:20261025T013000Z')
|
||
assert.equal(wert(b, 'DTEND'), 'DTEND:20261025T020000Z')
|
||
})
|
||
|
||
test('Zeilenlaenge: jede Zeile hoechstens 75 Oktette, nie mitten in einem UTF-8-Zeichen gefaltet', () => {
|
||
for (const baustein of ['a', 'ä', '€', '🙂', 'Ä߀🙂']) {
|
||
for (let n = 40; n < 130; n += 1) {
|
||
const titel = baustein.repeat(n)
|
||
const ics = icsTermin(termin({ titel, ort: titel, beschreibung: titel, organisator: { name: titel, email: 'info@salon-muster.de' }, teilnehmer: { name: titel, email: 'anna@example.com' } }))
|
||
pruefeZeilen(ics)
|
||
for (const folge of ics.split('\r\n').filter((l) => l.startsWith(' '))) assert.ok(folge.length > 1, 'Folgezeile nicht leer')
|
||
assert.equal(entescape(wert(ics, 'SUMMARY').slice('SUMMARY:'.length)), titel, 'Entfalten ergibt den Titel wieder')
|
||
}
|
||
}
|
||
})
|
||
|
||
test('Zeilenlaenge: Emoji (Surrogatpaare) werden nie zwischen ihren Haelften gefaltet, in Termin und Abo', () => {
|
||
let gefaltet = 0
|
||
for (const vorsatz of ['', 'a', 'ab', 'abc']) {
|
||
for (let n = 1; n < 60; n += 1) {
|
||
const titel = vorsatz + '😀'.repeat(n)
|
||
const felder = { titel, ort: titel, beschreibung: titel, organisator: { name: titel, email: 'info@salon-muster.de' }, teilnehmer: { name: titel, email: 'anna@example.com' } }
|
||
const ics = icsTermin(termin(felder))
|
||
pruefeZeilen(ics)
|
||
gefaltet += ics.split('\r\n').filter((l) => l.startsWith(' ')).length
|
||
assert.equal(entescape(wert(ics, 'SUMMARY').slice('SUMMARY:'.length)), titel)
|
||
assert.equal(entescape(wert(ics, 'DESCRIPTION').slice('DESCRIPTION:'.length)), titel)
|
||
const feed = icsKalender([termin(felder)], { name: titel, jetzt: JETZT })
|
||
pruefeZeilen(feed)
|
||
assert.equal(entescape(wert(feed, 'X-WR-CALNAME').slice('X-WR-CALNAME:'.length)), titel)
|
||
}
|
||
}
|
||
assert.ok(gefaltet > 500, `der Test faltet wirklich (${gefaltet} Folgezeilen)`)
|
||
// Beispiel: DESCRIPTION: ist 12 Oktette lang, danach passen 15 Emoji (60 Oktette) in die erste Zeile, das 16. beginnt die Folgezeile
|
||
const beschreibung = '😀'.repeat(16)
|
||
const zeilen = icsTermin(termin({ beschreibung })).split('\r\n')
|
||
const i = zeilen.findIndex((l) => l.startsWith('DESCRIPTION:'))
|
||
assert.equal(zeilen[i], `DESCRIPTION:${'😀'.repeat(15)}`)
|
||
assert.equal(zeilen[i + 1], ' 😀')
|
||
})
|
||
|
||
test('Zeilenlaenge: eine Zeile von genau 75 Oktetten bleibt ungefaltet, 76 wird gefaltet', () => {
|
||
const zeileMit = (n) => icsTermin(termin({ titel: 'x'.repeat(n - 'SUMMARY:'.length) })).split('\r\n').find((l) => l.startsWith('SUMMARY:'))
|
||
assert.equal(octets(zeileMit(75)), 75)
|
||
const ics76 = icsTermin(termin({ titel: 'x'.repeat(76 - 'SUMMARY:'.length) }))
|
||
assert.match(ics76, /SUMMARY:x{67}\r\n x\r\n/)
|
||
})
|
||
|
||
test('Escaping: Backslash, Semikolon, Komma und Zeilenumbrueche in TEXT-Werten', () => {
|
||
const roh = 'a\\b;c,d\ne\r\nf\rg'
|
||
const ics = icsTermin(termin({ titel: roh, ort: roh, beschreibung: roh }))
|
||
const erwartet = 'a\\\\b\\;c\\,d\\ne\\nf\\ng'
|
||
assert.equal(wert(ics, 'SUMMARY'), `SUMMARY:${erwartet}`)
|
||
assert.equal(wert(ics, 'LOCATION'), `LOCATION:${erwartet}`)
|
||
assert.equal(wert(ics, 'DESCRIPTION'), `DESCRIPTION:${erwartet}`)
|
||
assert.equal(entescape(erwartet), 'a\\b;c,d\ne\nf\ng')
|
||
})
|
||
|
||
test('Einschleusen: CR/LF und Steuerzeichen in Eingaben erzeugen keine zusaetzlichen Zeilen', () => {
|
||
const boese = 'x\r\nATTENDEE:mailto:boese@example.com\r\nEND:VEVENT'
|
||
const ics = icsTermin(
|
||
termin({
|
||
uid: `uid${boese}`,
|
||
titel: boese,
|
||
ort: boese,
|
||
beschreibung: boese,
|
||
organisator: { name: `Org${boese}`, email: `org@example.com${boese}` },
|
||
teilnehmer: { name: `Gast${boese}\u0000\u0007`, email: `gast@example.com${boese}` },
|
||
})
|
||
)
|
||
const logisch = zeilen(ics)
|
||
assert.equal(logisch.filter((l) => l.startsWith('ATTENDEE')).length, 1, 'genau ein ATTENDEE')
|
||
assert.equal(logisch.filter((l) => l === 'END:VEVENT').length, 1, 'genau ein END:VEVENT')
|
||
assert.equal(logisch.filter((l) => l.startsWith('UID:')).length, 1)
|
||
assert.ok(!/[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/.test(ics), 'keine Steuerzeichen')
|
||
assert.ok(!/[^\r]\n|\r[^\n]/.test(ics))
|
||
// jede Zeile ist eine bekannte Eigenschaft (kein eingeschleuster Name)
|
||
for (const l of logisch) assert.match(l, /^(BEGIN|END|VERSION|PRODID|CALSCALE|METHOD|UID|DTSTAMP|SEQUENCE|DTSTART|DTEND|SUMMARY|DESCRIPTION|LOCATION|ORGANIZER|ATTENDEE|STATUS|TRANSP)[:;]/)
|
||
})
|
||
|
||
test('CN="…": ohne Anfuehrungszeichen und Steuerzeichen, leerer Name laesst CN weg', () => {
|
||
const ics = icsTermin(termin({ organisator: { name: 'Salon "Chez; Muster"\n', email: 'info@salon-muster.de' }, teilnehmer: { name: '"\u0001"', email: 'anna@example.com' } }))
|
||
assert.equal(wert(ics, 'ORGANIZER'), 'ORGANIZER;CN="Salon Chez; Muster":mailto:info@salon-muster.de')
|
||
assert.equal(wert(ics, 'ATTENDEE'), 'ATTENDEE;ROLE=REQ-PARTICIPANT;PARTSTAT=ACCEPTED;RSVP=FALSE:mailto:anna@example.com')
|
||
})
|
||
|
||
test('E-Mail in mailto: ohne Leerraum und Trennzeichen', () => {
|
||
const ics = icsTermin(termin({ teilnehmer: { name: 'A', email: ' anna@example.com,boese@example.com ' } }))
|
||
assert.match(wert(ics, 'ATTENDEE'), /:mailto:anna@example\.comboese@example\.com$/)
|
||
})
|
||
|
||
test('ohne Teilnehmer, Ort und Beschreibung entfallen die Zeilen', () => {
|
||
const ics = icsTermin(termin({ teilnehmer: null, ort: '', beschreibung: undefined }))
|
||
assert.equal(wert(ics, 'ATTENDEE'), undefined)
|
||
assert.equal(wert(ics, 'LOCATION'), undefined)
|
||
assert.equal(wert(ics, 'DESCRIPTION'), undefined)
|
||
assert.ok(wert(ics, 'ORGANIZER'))
|
||
})
|
||
|
||
test('CANCEL: gleiche UID, hoehere Sequenz, STATUS:CANCELLED', () => {
|
||
const ics = icsTermin(termin({ methode: 'CANCEL', sequenz: 3 }))
|
||
assert.equal(wert(ics, 'METHOD'), 'METHOD:CANCEL')
|
||
assert.equal(wert(ics, 'UID'), 'UID:b_9f3a1c@termine.webklar.com')
|
||
assert.equal(wert(ics, 'SEQUENCE'), 'SEQUENCE:3')
|
||
assert.equal(wert(ics, 'STATUS'), 'STATUS:CANCELLED')
|
||
assert.match(wert(ics, 'ATTENDEE'), /;RSVP=FALSE:mailto:anna@example\.com$/)
|
||
assert.equal(zeilen(icsTermin(termin())).includes('STATUS:CANCELLED'), false, 'REQUEST ist nicht abgesagt')
|
||
})
|
||
|
||
test('PUBLISH (Betriebsmail, Abo): kein ATTENDEE, Organisator bleibt', () => {
|
||
const ics = icsTermin(termin({ methode: 'PUBLISH' }))
|
||
assert.equal(wert(ics, 'METHOD'), 'METHOD:PUBLISH')
|
||
assert.equal(wert(ics, 'ATTENDEE'), undefined)
|
||
assert.equal(wert(ics, 'STATUS'), 'STATUS:CONFIRMED')
|
||
assert.ok(wert(ics, 'ORGANIZER'))
|
||
})
|
||
|
||
test('Pflichtangaben: unbekannte Methode, ungueltige Zeiten, fehlende Felder werfen', () => {
|
||
assert.throws(() => icsTermin(termin({ methode: 'REPLY' })), TypeError)
|
||
assert.throws(() => icsTermin(termin({ methode: undefined })), TypeError)
|
||
assert.throws(() => icsTermin(termin({ start: 'morgen' })), TypeError)
|
||
assert.throws(() => icsTermin(termin({ ende: undefined })), TypeError)
|
||
assert.throws(() => icsTermin(termin({ ende: '2026-10-05T08:00:00Z' })), TypeError, 'Ende nicht nach Beginn')
|
||
assert.throws(() => icsTermin(termin({ jetzt: undefined })), TypeError)
|
||
assert.throws(() => icsTermin(termin({ jetzt: '2026-10-02' })), TypeError)
|
||
assert.throws(() => icsTermin(termin({ uid: '' })), TypeError)
|
||
assert.throws(() => icsTermin(termin({ uid: '\r\n' })), TypeError)
|
||
assert.throws(() => icsTermin(termin({ titel: '' })), TypeError)
|
||
assert.throws(() => icsTermin(termin({ sequenz: -1 })), TypeError)
|
||
assert.throws(() => icsTermin(termin({ sequenz: 1.5 })), TypeError)
|
||
assert.throws(() => icsTermin(termin({ organisator: null })), TypeError, 'REQUEST braucht den Organisator')
|
||
assert.throws(() => icsTermin(termin({ methode: 'CANCEL', organisator: undefined })), TypeError)
|
||
})
|
||
|
||
test('icsKalender: PUBLISH-Feed mit Name, Aktualisierungshinweis, mehreren Terminen und ohne Gaeste', () => {
|
||
const feed = icsKalender(
|
||
[
|
||
termin({ uid: 'b_1@termine.webklar.com', teilnehmer: { name: 'Geheim', email: 'geheim@example.com' } }),
|
||
{ uid: 'x_sperre@termine.webklar.com', sequenz: 0, start: '2026-10-06T12:00:00Z', ende: '2026-10-06T13:00:00Z', titel: 'Gesperrt', organisator: { name: 'Salon Muster', email: 'info@salon-muster.de' } },
|
||
],
|
||
{ name: 'Salon Muster, Stuhl 1; Süd', jetzt: JETZT }
|
||
)
|
||
assert.ok(feed.endsWith('END:VCALENDAR\r\n'))
|
||
assert.ok(!/[^\r]\n|\r[^\n]/.test(feed))
|
||
for (const zeile of feed.split('\r\n')) assert.ok(octets(zeile) <= 75)
|
||
const logisch = zeilen(feed)
|
||
assert.deepEqual(logisch.slice(0, 7), [
|
||
'BEGIN:VCALENDAR',
|
||
'VERSION:2.0',
|
||
'PRODID:-//WEBklar//Terminplaner//DE',
|
||
'CALSCALE:GREGORIAN',
|
||
'METHOD:PUBLISH',
|
||
'X-WR-CALNAME:Salon Muster\\, Stuhl 1\\; Süd',
|
||
'X-PUBLISHED-TTL:PT15M',
|
||
])
|
||
assert.equal(logisch.filter((l) => l === 'BEGIN:VEVENT').length, 2)
|
||
assert.equal(logisch.filter((l) => l === 'END:VEVENT').length, 2)
|
||
assert.equal(logisch.filter((l) => l === 'DTSTAMP:20261002T090000Z').length, 2, 'DTSTAMP = jetzt')
|
||
assert.equal(logisch.filter((l) => l.startsWith('ATTENDEE')).length, 0, 'keine Gaeste im Abo')
|
||
assert.ok(!/geheim@example\.com/.test(feed))
|
||
assert.deepEqual(logisch.filter((l) => l.startsWith('UID:')), ['UID:b_1@termine.webklar.com', 'UID:x_sperre@termine.webklar.com'])
|
||
assert.deepEqual(logisch.filter((l) => l.startsWith('STATUS:')), ['STATUS:CONFIRMED', 'STATUS:CONFIRMED'])
|
||
})
|
||
|
||
test('icsKalender: leerer Feed ist ein gueltiger Kalender; ohne jetzt oder Name-Typ wirft', () => {
|
||
const leer = icsKalender([], { name: 'Leer', jetzt: JETZT })
|
||
assert.deepEqual(zeilen(leer).filter((l) => l.startsWith('BEGIN:') || l.startsWith('END:')), ['BEGIN:VCALENDAR', 'END:VCALENDAR'])
|
||
assert.throws(() => icsKalender([], { name: 'Leer' }), TypeError)
|
||
assert.throws(() => icsKalender([termin({ start: 'x' })], { name: 'Leer', jetzt: JETZT }), TypeError)
|
||
})
|
||
|
||
// ---------------------------------------------------------------------------------------------------------------------
|
||
// sendGuestMail
|
||
// ---------------------------------------------------------------------------------------------------------------------
|
||
|
||
const gastMail = (extra = {}) => ({
|
||
to: 'anna@example.com',
|
||
fromName: 'Salon Muster',
|
||
subject: 'Ihr Termin bei Salon Muster am 05.10.2026',
|
||
html: '<p>Hallo</p>',
|
||
text: 'Hallo',
|
||
...extra,
|
||
})
|
||
|
||
async function mitSenke(fn, from = 'noreply@webklar.com') {
|
||
const vorher = config.smtp.from
|
||
config.smtp.from = from
|
||
const senke = mailSink()
|
||
setMailTransportForTests(senke)
|
||
try {
|
||
return await fn(senke)
|
||
} finally {
|
||
setMailTransportForTests(null)
|
||
config.smtp.from = vorher
|
||
}
|
||
}
|
||
|
||
test('sendGuestMail: Absendername = Betrieb, Adresse = SMTP_FROM, replyTo = Betrieb, nie BCC, ohne ics kein icalEvent', async () => {
|
||
await mitSenke(async (senke) => {
|
||
await sendGuestMail(gastMail({ replyTo: 'info@salon-muster.de' }))
|
||
assert.equal(senke.sent.length, 1)
|
||
const [m] = senke.sent
|
||
assert.deepEqual(m.from, { address: 'noreply@webklar.com', name: 'Salon Muster' })
|
||
assert.deepEqual(m.to, [{ address: 'anna@example.com', name: '' }])
|
||
assert.deepEqual(m.replyTo, [{ address: 'info@salon-muster.de', name: '' }])
|
||
assert.equal(m.subject, 'Ihr Termin bei Salon Muster am 05.10.2026')
|
||
assert.equal(m.html, '<p>Hallo</p>')
|
||
assert.equal(m.text, 'Hallo')
|
||
assert.equal('bcc' in m, false, 'nie BCC')
|
||
assert.equal('cc' in m, false)
|
||
assert.equal('icalEvent' in m, false, 'ohne ics keine Kalenderdatei')
|
||
assert.deepEqual(m.attachments ?? [], [])
|
||
})
|
||
})
|
||
|
||
test('sendGuestMail: ohne replyTo keine Antwortadresse, auch nicht die des Portals', async () => {
|
||
await mitSenke(async (senke) => {
|
||
const vorher = config.smtp.replyTo
|
||
config.smtp.replyTo = 'support@webklar.com'
|
||
try {
|
||
await sendGuestMail(gastMail())
|
||
} finally {
|
||
config.smtp.replyTo = vorher
|
||
}
|
||
assert.equal(senke.sent[0].replyTo ?? null, null, 'nodemailer schreibt eine fehlende Antwortadresse als null')
|
||
assert.equal('bcc' in senke.sent[0], false)
|
||
})
|
||
})
|
||
|
||
test('sendGuestMail: mit ics geht die Kalenderdatei als icalEvent (Standard REQUEST, termin.ics)', async () => {
|
||
await mitSenke(async (senke) => {
|
||
const ics = icsTermin(termin())
|
||
await sendGuestMail(gastMail({ ics }))
|
||
const m = senke.sent[0]
|
||
assert.equal(m.icalEvent.method, 'REQUEST')
|
||
assert.equal(m.icalEvent.filename, 'termin.ics')
|
||
assert.equal(m.icalEvent.content, ics)
|
||
assert.match(m.icalEvent.content, /BEGIN:VCALENDAR/)
|
||
assert.equal('bcc' in m, false)
|
||
})
|
||
})
|
||
|
||
test('sendGuestMail: icsMethod CANCEL und PUBLISH werden uebernommen, andere Methoden abgelehnt', async () => {
|
||
await mitSenke(async (senke) => {
|
||
await sendGuestMail(gastMail({ ics: icsTermin(termin({ methode: 'CANCEL' })), icsMethod: 'CANCEL' }))
|
||
assert.equal(senke.sent[0].icalEvent.method, 'CANCEL')
|
||
await sendGuestMail(gastMail({ ics: icsTermin(termin({ methode: 'PUBLISH' })), icsMethod: 'PUBLISH' }))
|
||
assert.equal(senke.sent[1].icalEvent.method, 'PUBLISH')
|
||
await assert.rejects(sendGuestMail(gastMail({ ics: 'x', icsMethod: 'REPLY' })), TypeError)
|
||
assert.equal(senke.sent.length, 2)
|
||
})
|
||
})
|
||
|
||
test('sendGuestMail: Betreff und Absendername ohne Zeilenumbrueche; Anfuehrungszeichen im Namen bleiben heil', async () => {
|
||
await mitSenke(async (senke) => {
|
||
await sendGuestMail(gastMail({ fromName: 'Salon "Chez, Muster"\r\nBcc: boese@example.com', subject: 'Termin\r\nBcc: boese@example.com' }))
|
||
const m = senke.sent[0]
|
||
assert.ok(!/[\r\n]/.test(m.subject))
|
||
assert.ok(!/[\r\n]/.test(m.from.name))
|
||
assert.equal(m.from.address, 'noreply@webklar.com')
|
||
assert.equal('bcc' in m, false)
|
||
})
|
||
})
|
||
|
||
test('sendGuestMail: genau ein Postfach als Empfaenger und Antwortadresse', async () => {
|
||
await mitSenke(async (senke) => {
|
||
for (const to of ['a@example.com, b@example.com', 'a@example.com;b@example.com', 'Max <a@example.com>', '"a b"@example.com', 'a@example.com\r\nBcc: b@example.com', '', undefined, 'ohne-at', `${'a'.repeat(250)}@example.com`]) {
|
||
await assert.rejects(sendGuestMail(gastMail({ to })), TypeError, String(to))
|
||
}
|
||
await assert.rejects(sendGuestMail(gastMail({ replyTo: 'a@example.com, b@example.com' })), TypeError)
|
||
await assert.rejects(sendGuestMail(gastMail({ subject: '' })), TypeError)
|
||
await assert.rejects(sendGuestMail(gastMail({ fromName: '' })), TypeError)
|
||
assert.equal(senke.sent.length, 0)
|
||
})
|
||
})
|
||
|
||
test('sendGuestMail: ohne SMTP-Zugang wirft sie (wie sendCustomerMail)', async () => {
|
||
setMailTransportForTests(null)
|
||
assert.equal(config.smtp.host, '', 'Testumgebung ohne SMTP')
|
||
await assert.rejects(sendGuestMail(gastMail()), /SMTP nicht konfiguriert/)
|
||
})
|