402 lines
18 KiB
JavaScript
402 lines
18 KiB
JavaScript
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'
|
|
|
|
// Status-Route des Terminplaners fuers Team (GET /api/team/termine/status) und statusDaten: Team-Eingang mit Appwrite-JWT wie in
|
|
// teamAdminRoutes.test.js, Nachbau von Appwrite mit Schema, Mails in der Senke. Die Route rechnet mit der echten Uhr, statusDaten
|
|
// mit einem festen Zeitpunkt.
|
|
const aw = await startAppwriteMock({
|
|
schema: {
|
|
portalFeatures: schemaKeys('portalFeatures'),
|
|
termineConfig: schemaKeys('termineConfig'),
|
|
termineBookings: schemaKeys('termineBookings'),
|
|
termineClaims: schemaKeys('termineClaims'),
|
|
termineOutbox: schemaKeys('termineOutbox'),
|
|
},
|
|
datetimes: TERMINE_DATETIMES,
|
|
defaultLimit: 25,
|
|
})
|
|
useTestEnv({ appwriteUrl: aw.url, stripeUrl: 'http://127.0.0.1:1' })
|
|
|
|
const { config } = await import('../server/config.js')
|
|
const { startTestApp, rawGet, send, post, sessionCookie } = await import('./support/testApp.js')
|
|
const { deleteDocument } = await import('../server/services/appwriteAdmin.js')
|
|
const { setMailTransportForTests } = await import('../server/services/mailer.js')
|
|
const { loadTemplate } = await import('../server/services/mailTemplates.js')
|
|
const { einreihen, warteAufVersand, mailBudget, verworfeneG01, resetOutboxForTests } = await import('../server/services/termine/outbox.js')
|
|
const { statusDaten, resetBetriebForTests } = await import('../server/services/termine/betrieb.js')
|
|
const { taeglicherJob, resetJobsForTests } = await import('../server/services/termine/jobs.js')
|
|
const { default: teamRoutes } = await import('../server/routes/team/index.js')
|
|
const app = await startTestApp({ '/api/team': teamRoutes })
|
|
after(() => Promise.all([app.close(), aw.close()]))
|
|
after(() => setMailTransportForTests(null))
|
|
|
|
const MIN = 60000
|
|
const STUNDE = 60 * MIN
|
|
const JETZT = new Date('2026-10-05T08:00:00.000Z') // Montag, 10:00 Berlin
|
|
const plus = (ms, von = JETZT) => new Date(von.getTime() + ms)
|
|
const iso = (wert) => new Date(wert).toISOString()
|
|
const EAUTH = Object.assign(new Error('Invalid login: 535 Authentication failed'), { code: 'EAUTH', responseCode: 535 })
|
|
|
|
aw.seed('employees', { $id: 'e_staff2', userId: 'u_staff2', displayName: 'Andrej', email: 'andrej@webklar.com' })
|
|
const staff = { headers: { authorization: 'Bearer jwt-staff' } }
|
|
const STATUS = '/api/team/termine/status'
|
|
const SCHLUESSEL = ['aktiveBetriebe', 'budget', 'letzterTagesjob', 'pause', 'queue']
|
|
|
|
const VORHER = { modus: config.termine.mode, absender: config.smtp.from, empfaenger: config.chatNotify.emails }
|
|
let sink
|
|
let log
|
|
beforeEach(async () => {
|
|
for (const collection of ['termineOutbox', 'termineBookings', 'termineClaims', 'portalFeatures', 'termineConfig']) {
|
|
for (const id of aw.docs(collection).keys()) await deleteDocument(collection, id)
|
|
}
|
|
resetOutboxForTests()
|
|
resetBetriebForTests()
|
|
resetJobsForTests()
|
|
config.termine.mode = 'pilot'
|
|
config.termine.mailsProStunde = 200
|
|
config.termine.mailsProFunktion = 40
|
|
config.smtp.from = 'noreply@webklar.com'
|
|
config.chatNotify.emails = ['kenso@example.test']
|
|
sink = mailSink()
|
|
setMailTransportForTests(sink)
|
|
log = { error: mock.method(console, 'error', () => {}), warn: mock.method(console, 'warn', () => {}) }
|
|
})
|
|
afterEach(async () => {
|
|
await warteAufVersand()
|
|
const zeilen = [...log.error.mock.calls, ...log.warn.mock.calls].map((aufruf) => aufruf.arguments.map(String).join(' '))
|
|
mock.restoreAll()
|
|
config.termine.mode = VORHER.modus
|
|
config.smtp.from = VORHER.absender
|
|
config.chatNotify.emails = VORHER.empfaenger
|
|
// Logs enthalten nie Adressen oder Namen der Gaeste
|
|
assert.doesNotMatch(zeilen.join('\n'), /anna@example\.com|Anna/, 'keine Gastadresse und kein Gastname im Log')
|
|
})
|
|
|
|
// ---------------------------------------------------------------- Hilfen
|
|
|
|
const werte = (id, extra = {}) => ({ ...Object.fromEntries(loadTemplate(id).placeholders.map((p) => [p, `Wert-${p}`])), ...extra })
|
|
let nummer = 0
|
|
/** Mail an einen Gast (Vorrang-Art G02, Vorlage G01): belegt beim Senden einen Platz im Stundenbudget. */
|
|
const gastMail = (featureId = 'f_A') => ({
|
|
featureId,
|
|
bookingId: `b_${featureId}_${++nummer}`,
|
|
art: 'G02',
|
|
an: 'anna@example.com',
|
|
fromName: 'Salon Anna Muster',
|
|
replyTo: 'info@salon-muster.de',
|
|
template: 'G01',
|
|
values: werte('G01', { betreff: 'Bitte bestätigen Sie Ihren Termin bei Salon Anna Muster' }),
|
|
blocks: { sie: true, bestaetigen: true },
|
|
})
|
|
|
|
/** Eintrag direkt in der Datenbank (ohne Sendeversuch). */
|
|
function legeEintrag(id, over = {}) {
|
|
aw.seed('termineOutbox', {
|
|
$id: id,
|
|
featureId: 'f_A',
|
|
bookingId: 'b_direkt',
|
|
art: 'G05',
|
|
an: 'anna@example.com',
|
|
fromName: 'Salon Anna Muster',
|
|
replyTo: 'info@salon-muster.de',
|
|
subject: 'Erinnerung: Ihr Termin',
|
|
html: '<p>Hallo Anna</p>',
|
|
text: 'Hallo Anna',
|
|
ics: '',
|
|
icsMethod: '',
|
|
status: 'wartet',
|
|
versuche: 0,
|
|
naechsterVersuch: iso(JETZT),
|
|
fehler: '',
|
|
createdAt: iso(JETZT),
|
|
...over,
|
|
})
|
|
}
|
|
|
|
const funktion = (id, over = {}) =>
|
|
aw.seed('portalFeatures', {
|
|
$id: id,
|
|
customerId: 'c_kunde01',
|
|
projectId: `p_${id}`,
|
|
featureKey: 'termine',
|
|
state: 'active',
|
|
enabled: true,
|
|
siteKey: 'abcdefghijklmnop',
|
|
...over,
|
|
})
|
|
|
|
/** Alle Lese-Anfragen an die Collection scheitern mit 500, solange fn laeuft. */
|
|
async function mitLesefehler(collection, fn) {
|
|
const echt = globalThis.fetch
|
|
globalThis.fetch = async (url, optionen) => {
|
|
if (new RegExp(`/collections/${collection}/documents`).test(String(url)) && (optionen?.method ?? 'GET') === 'GET') {
|
|
return new Response(JSON.stringify({ message: 'Nachbau kaputt', code: 500, type: 'general_server_error' }), {
|
|
status: 500,
|
|
headers: { 'content-type': 'application/json' },
|
|
})
|
|
}
|
|
return echt(url, optionen)
|
|
}
|
|
try {
|
|
return await fn()
|
|
} finally {
|
|
globalThis.fetch = echt
|
|
}
|
|
}
|
|
|
|
/** Alle Zeichenketten (Werte und Schluessel) eines JSON-Werts, rekursiv. */
|
|
function alleTexte(wert, sammlung = []) {
|
|
if (typeof wert === 'string') sammlung.push(wert)
|
|
else if (Array.isArray(wert)) wert.forEach((eintrag) => alleTexte(eintrag, sammlung))
|
|
else if (wert !== null && typeof wert === 'object') {
|
|
for (const [schluessel, eintrag] of Object.entries(wert)) {
|
|
sammlung.push(schluessel)
|
|
alleTexte(eintrag, sammlung)
|
|
}
|
|
}
|
|
return sammlung
|
|
}
|
|
|
|
const betriebLog = () =>
|
|
[...log.warn.mock.calls, ...log.error.mock.calls]
|
|
.map((aufruf) => aufruf.arguments.map(String).join(' '))
|
|
.filter((zeile) => zeile.startsWith('[termine-betrieb]'))
|
|
|
|
// ---------------------------------------------------------------- Eingang
|
|
|
|
test('Eingang: ohne und mit ungueltigem JWT 401, Kunden-JWT 403, Mitarbeiter-JWT 200', async () => {
|
|
assert.equal((await rawGet(STATUS)).status, 401, 'ohne JWT')
|
|
assert.equal((await rawGet(STATUS, { headers: { authorization: 'Bearer jwt-falsch' } })).status, 401, 'ungueltiges JWT')
|
|
assert.equal((await rawGet(STATUS, { headers: { authorization: 'Bearer jwt-halb' } })).status, 401, 'JWT ohne zweiten Faktor')
|
|
const kunde = await rawGet(STATUS, { headers: { authorization: 'Bearer jwt-kunde' } })
|
|
assert.equal(kunde.status, 403)
|
|
assert.equal(kunde.body.error, 'Kein Mitarbeiterkonto.')
|
|
assert.equal((await rawGet(STATUS, staff)).status, 200)
|
|
})
|
|
|
|
test('Eingang: ein Kunden-Cookie (und ein Admin-Cookie des Portals) erreicht die Route nicht, nur das JWT zaehlt', async () => {
|
|
const kunde = sessionCookie({ customerId: 'c1', appwriteUserId: 'u_kunde' })
|
|
const admin = sessionCookie({ role: 'admin', isAdmin: true, appwriteUserId: 'u_staff2', name: 'Andrej', email: 'andrej@webklar.com' })
|
|
for (const cookie of [kunde, admin]) {
|
|
const res = await rawGet(STATUS, { cookie })
|
|
assert.equal(res.status, 401)
|
|
assert.deepEqual(Object.keys(res.body), ['error'], 'kein Status im Body')
|
|
}
|
|
// Cookie plus Kunden-JWT: weiter 403
|
|
assert.equal((await rawGet(STATUS, { cookie: kunde, headers: { authorization: 'Bearer jwt-kunde' } })).status, 403)
|
|
})
|
|
|
|
test('Eingang: nur GET, andere Methoden liefern keinen Status', async () => {
|
|
for (const methode of ['POST', 'PUT', 'DELETE']) {
|
|
const res = await send(methode, STATUS, { ...staff, body: {} })
|
|
assert.notEqual(res.status, 200, methode)
|
|
assert.equal(res.body?.budget, undefined, methode)
|
|
}
|
|
assert.equal((await post(STATUS, {}, staff)).status, 404)
|
|
})
|
|
|
|
// ---------------------------------------------------------------- Form und Werte
|
|
|
|
test('GET: Form { budget, queue, pause, letzterTagesjob, aktiveBetriebe }, Cache-Control no-store, leerer Betrieb', async () => {
|
|
const res = await rawGet(STATUS, staff)
|
|
assert.equal(res.status, 200)
|
|
assert.equal(res.headers.get('cache-control'), 'no-store')
|
|
assert.match(res.headers.get('content-type'), /application\/json/)
|
|
assert.deepEqual(Object.keys(res.body).sort(), SCHLUESSEL)
|
|
assert.deepEqual(Object.keys(res.body.budget).sort(), ['jeFunktionMax', 'nachrangig', 'spitzen', 'stunde', 'verworfeneG01'])
|
|
assert.deepEqual(res.body.budget.stunde, { benutzt: 0, max: 200 })
|
|
assert.deepEqual(res.body.budget.nachrangig.benutzt, 0)
|
|
assert.equal(res.body.budget.jeFunktionMax, 40)
|
|
assert.deepEqual(res.body.budget.spitzen, [])
|
|
assert.equal(res.body.budget.verworfeneG01, 0)
|
|
assert.deepEqual(res.body.queue, { wartet: 0, aeltesteMin: 0 })
|
|
assert.deepEqual(res.body.pause, { aktiv: false, bis: null })
|
|
assert.equal(res.body.letzterTagesjob, null)
|
|
assert.equal(res.body.aktiveBetriebe, 0)
|
|
})
|
|
|
|
test('GET: Budget, Warteschlange und Anmeldepause kommen aus dem laufenden Betrieb', async () => {
|
|
const jetzt = new Date()
|
|
// zwei gesendete Mails belegen das Stundenbudget, eine Anmeldung scheitert und startet die Pause (15 Minuten)
|
|
await einreihen(gastMail('f_A'), jetzt)
|
|
await einreihen(gastMail('f_A'), jetzt)
|
|
await warteAufVersand()
|
|
const gesendet = sink.sent.length
|
|
assert.equal(gesendet, 2)
|
|
const scheitert = {
|
|
async sendMail() {
|
|
throw EAUTH
|
|
},
|
|
}
|
|
setMailTransportForTests(scheitert)
|
|
const pausenDoc = await einreihen(gastMail('f_B'), jetzt)
|
|
await warteAufVersand()
|
|
await deleteDocument('termineOutbox', pausenDoc.$id)
|
|
// zwei wartende Eintraege: einer seit 12 Minuten faellig, einer erst in der Zukunft
|
|
legeEintrag('o_faellig', { naechsterVersuch: iso(Date.now() - 12 * MIN - 5000) })
|
|
legeEintrag('o_spaeter', { naechsterVersuch: iso(Date.now() + 30 * MIN) })
|
|
|
|
const res = await rawGet(STATUS, staff)
|
|
assert.equal(res.status, 200)
|
|
assert.equal(res.body.budget.stunde.benutzt, 2, 'nur die zwei gesendeten Mails belegen Plaetze, der gescheiterte Versuch gibt seinen frei')
|
|
assert.equal(res.body.budget.spitzen[0].featureId, 'f_A')
|
|
assert.equal(res.body.budget.spitzen[0].benutzt, 2)
|
|
assert.equal(res.body.queue.wartet, 2)
|
|
assert.equal(res.body.queue.aeltesteMin, 12)
|
|
assert.equal(res.body.pause.aktiv, true)
|
|
const bis = new Date(res.body.pause.bis).getTime()
|
|
assert.ok(bis > Date.now() && bis <= Date.now() + 15 * MIN, `Pause laeuft noch bis ${res.body.pause.bis}`)
|
|
})
|
|
|
|
test('GET: letzterTagesjob ist null, bis der Tagesjob gelaufen ist, danach { at, fehler, gedeckelt, geloescht } als reine Zaehlungen', async () => {
|
|
assert.equal((await rawGet(STATUS, staff)).body.letzterTagesjob, null)
|
|
const vor = Date.now()
|
|
const erg = await taeglicherJob(new Date())
|
|
assert.equal(erg.fehler, 0)
|
|
const letzter = (await rawGet(STATUS, staff)).body.letzterTagesjob
|
|
assert.deepEqual(Object.keys(letzter).sort(), ['at', 'fehler', 'gedeckelt', 'geloescht'])
|
|
assert.ok(new Date(letzter.at).getTime() >= vor - 1000, 'Zeitpunkt des Laufs')
|
|
assert.match(letzter.at, /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/)
|
|
assert.deepEqual({ fehler: letzter.fehler, gedeckelt: letzter.gedeckelt, geloescht: letzter.geloescht }, { fehler: 0, gedeckelt: 0, geloescht: 0 })
|
|
})
|
|
|
|
test('GET: aktiveBetriebe zaehlt nur aktive Termine-Dokumente (zwei aktive, ein abgeschaltetes, dazu Entwurf, bestellt und News)', async () => {
|
|
funktion('f_aktiv1')
|
|
funktion('f_aktiv2')
|
|
funktion('f_abgeschaltet', { state: 'disabled', enabled: false, disabledAt: iso(JETZT) })
|
|
funktion('f_entwurf', { state: 'off', enabled: false })
|
|
funktion('f_bestellt', { state: 'ordered', enabled: false })
|
|
funktion('f_news', { featureKey: 'news' })
|
|
assert.equal((await rawGet(STATUS, staff)).body.aktiveBetriebe, 2)
|
|
})
|
|
|
|
// ---------------------------------------------------------------- Lesefehler
|
|
|
|
test('Lesefehler der Warteschlange (failNext auf termineOutbox): 200, queue aus null-Werten, die uebrigen Werte bleiben, ein Log', async () => {
|
|
funktion('f_aktiv1')
|
|
aw.failNext({ collection: 'termineOutbox', method: 'GET', status: 500, message: 'Nachbau: Outbox kaputt' })
|
|
const res = await rawGet(STATUS, staff)
|
|
assert.equal(res.status, 200)
|
|
assert.deepEqual(res.body.queue, { wartet: null, aeltesteMin: null })
|
|
assert.equal(res.body.aktiveBetriebe, 1, 'andere Werte sind nicht betroffen')
|
|
assert.deepEqual(res.body.pause, { aktiv: false, bis: null })
|
|
assert.equal(res.body.budget.stunde.max, 200)
|
|
const zeilen = betriebLog()
|
|
assert.equal(zeilen.length, 1, zeilen.join('\n'))
|
|
assert.match(zeilen[0], /Warteschlange|queue/i)
|
|
// der naechste Aufruf liest wieder
|
|
assert.deepEqual((await rawGet(STATUS, staff)).body.queue, { wartet: 0, aeltesteMin: 0 })
|
|
})
|
|
|
|
test('Lesefehler der Betriebszaehlung (portalFeatures): aktiveBetriebe null, die Warteschlange bleibt lesbar', async () => {
|
|
funktion('f_aktiv1')
|
|
legeEintrag('o_1', { naechsterVersuch: iso(Date.now() - 3 * MIN) })
|
|
const res = await mitLesefehler('portalFeatures', () => rawGet(STATUS, staff))
|
|
assert.equal(res.status, 200)
|
|
assert.equal(res.body.aktiveBetriebe, null)
|
|
assert.deepEqual(res.body.queue, { wartet: 1, aeltesteMin: 3 })
|
|
assert.equal(betriebLog().length, 1)
|
|
})
|
|
|
|
test('Lesefehler ueberall: weiter 200 (nie ein 500), alles Lesbare null, nur Zahlen im Log', async () => {
|
|
funktion('f_aktiv1')
|
|
const res = await mitLesefehler('(termineOutbox|portalFeatures)', () => rawGet(STATUS, staff))
|
|
assert.equal(res.status, 200)
|
|
assert.equal(res.body.aktiveBetriebe, null)
|
|
assert.deepEqual(res.body.queue, { wartet: null, aeltesteMin: null })
|
|
assert.deepEqual(Object.keys(res.body).sort(), SCHLUESSEL)
|
|
assert.equal(res.body.budget.stunde.max, 200, 'das Budget liegt im Speicher und bleibt')
|
|
assert.equal(betriebLog().length, 2, 'ein Log je Lesefehler')
|
|
})
|
|
|
|
test('Lesefehler: das Log einer Pruefung folgt hoechstens stuendlich, statusDaten selbst liefert jedes Mal null', async () => {
|
|
const lese = () => mitLesefehler('termineOutbox', () => statusDaten(JETZT))
|
|
assert.deepEqual((await lese()).queue, { wartet: null, aeltesteMin: null })
|
|
assert.deepEqual((await lese()).queue, { wartet: null, aeltesteMin: null })
|
|
assert.equal(betriebLog().length, 1, 'zweimal gelesen, einmal geloggt')
|
|
const spaeter = await mitLesefehler('termineOutbox', () => statusDaten(plus(STUNDE + MIN)))
|
|
assert.equal(spaeter.queue.wartet, null)
|
|
assert.equal(betriebLog().length, 2, 'nach einer Stunde wieder')
|
|
})
|
|
|
|
// ---------------------------------------------------------------- statusDaten
|
|
|
|
test('statusDaten: Form und Werte zu einem festen Zeitpunkt, budget = mailBudget plus verworfeneG01', async () => {
|
|
funktion('f_aktiv1')
|
|
legeEintrag('o_alt', { naechsterVersuch: iso(plus(-25 * MIN)) })
|
|
legeEintrag('o_neu', { naechsterVersuch: iso(plus(-5 * MIN)) })
|
|
legeEintrag('o_gesendet', { status: 'gesendet', naechsterVersuch: iso(plus(-90 * MIN)) })
|
|
const daten = await statusDaten(JETZT)
|
|
assert.deepEqual(Object.keys(daten).sort(), SCHLUESSEL)
|
|
assert.deepEqual(daten.queue, { wartet: 2, aeltesteMin: 25 })
|
|
assert.deepEqual(daten.pause, { aktiv: false, bis: null })
|
|
assert.equal(daten.letzterTagesjob, null)
|
|
assert.equal(daten.aktiveBetriebe, 1)
|
|
assert.deepEqual(daten.budget, { ...mailBudget(JETZT), verworfeneG01: verworfeneG01() })
|
|
assert.equal(typeof daten.budget.verworfeneG01, 'number')
|
|
})
|
|
|
|
test('statusDaten: die Anmeldepause ist aktiv, solange ihr Ende nach now liegt, danach aktiv false und bis null', async () => {
|
|
const scheitert = {
|
|
async sendMail() {
|
|
throw EAUTH
|
|
},
|
|
}
|
|
setMailTransportForTests(scheitert)
|
|
const doc = await einreihen(gastMail('f_P'), JETZT)
|
|
await warteAufVersand()
|
|
await deleteDocument('termineOutbox', doc.$id)
|
|
|
|
const waehrend = await statusDaten(plus(4 * MIN))
|
|
assert.deepEqual(waehrend.pause, { aktiv: true, bis: plus(15 * MIN).toISOString() })
|
|
const danach = await statusDaten(plus(15 * MIN + 1))
|
|
assert.deepEqual(danach.pause, { aktiv: false, bis: null })
|
|
})
|
|
|
|
test('statusDaten: nach dem Tagesjob steht dessen Stand da, aktiveBetriebe zaehlt der Aufruf neu', async () => {
|
|
funktion('f_aktiv1')
|
|
await taeglicherJob(JETZT)
|
|
funktion('f_aktiv2')
|
|
const daten = await statusDaten(plus(MIN))
|
|
assert.deepEqual(daten.letzterTagesjob, { at: JETZT.toISOString(), fehler: 0, gedeckelt: 0, geloescht: 0 })
|
|
assert.equal(daten.aktiveBetriebe, 2)
|
|
})
|
|
|
|
test('statusDaten: ungueltiges now ist ein TypeError wie bei allen Termine-Bausteinen', async () => {
|
|
await assert.rejects(() => statusDaten(), TypeError)
|
|
await assert.rejects(() => statusDaten('heute'), TypeError)
|
|
await assert.rejects(() => statusDaten(new Date('x')), TypeError)
|
|
})
|
|
|
|
// ---------------------------------------------------------------- Datenschutz
|
|
|
|
test('Body: rekursiv keine Adresse und kein Gastname, auch nicht mit Mails, Eintraegen und Betrieben im System', async () => {
|
|
funktion('f_aktiv1')
|
|
await einreihen(gastMail('f_A'), new Date())
|
|
await warteAufVersand()
|
|
legeEintrag('o_wartet', { naechsterVersuch: iso(Date.now() - 11 * MIN), fehler: 'Anna Muster anna@example.com abgelehnt' })
|
|
aw.seed('termineBookings', {
|
|
$id: 'b_geheim',
|
|
featureId: 'f_A',
|
|
status: 'bestaetigt',
|
|
gastName: 'Anna Muster',
|
|
gastEmail: 'anna@example.com',
|
|
})
|
|
const res = await rawGet(STATUS, staff)
|
|
assert.equal(res.status, 200)
|
|
const texte = alleTexte(res.body)
|
|
assert.ok(texte.length > 5, 'der Body hat Inhalt')
|
|
for (const text of texte) {
|
|
assert.doesNotMatch(text, /@/, `Adresse in „${text}“`)
|
|
assert.doesNotMatch(text, /Anna|Muster|Salon/i, `Name in „${text}“`)
|
|
}
|
|
assert.doesNotMatch(JSON.stringify(res.body), /example\.com|salon-muster|Anna/i)
|
|
// nur Zaehlungen und Zeitpunkte: Zahlen, Wahrheitswerte, ISO-Zeiten und Funktions-IDs
|
|
for (const text of texte) assert.match(text, /^([a-zA-Z0-9_]+|\d{4}-\d{2}-\d{2}T[\d:.]+Z)$/, `unerwarteter Text „${text}“`)
|
|
})
|