- AVV Vorbemerkung (2): K16 der Textform-Aktivierung trägt nur Leistungsbeschreibung und AVV als PDF - AVV Nr. 9 Abs. 6, Anlage (Löschung), LB Nr. 8 Abs. 3 und Datenschutz 6c: das Einrichtungs-Ticket bleibt mit einer Kurzfassung der Einstellungen als Arbeitsnachweis (keine Daten von Gästen) - AVV Nr. 8 Abs. 2: "Gastdaten löschen" löscht nur wartende E-Mails; fehlgeschlagene behalten Adresse und Inhalt bis Nr. 9 Abs. 4 - Datenschutz § 7: Frist für Bestellungen ohne Abo (drei Jahre nach Ablauf des Jahres der Bestellung) - News-Leistungsbeschreibung vom 30.09.2026 unverändert archiviert (SHA-256-Pin), die geltende Datei verweist darauf - Kanzlei-Paket: Zitate, Q4/Q9/Q23, Livegang-Checkliste (Abschnitt 7.6/7.7), K23-Hinweis - build-legal-html: Rückwärtsschrägstrich in Navigationsadressen abgelehnt; TP1-Spezifikation E2 (ohne USt) - HTML, PDFs und Prüfsummen neu erzeugt (Terminplaner-Unterlagen und Datenschutzerklärung) Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
311 lines
18 KiB
JavaScript
311 lines
18 KiB
JavaScript
import { test, after, mock } from 'node:test'
|
||
import assert from 'node:assert/strict'
|
||
import { startAppwriteMock } from './support/appwriteMock.js'
|
||
import { useTestEnv } from './support/testEnv.js'
|
||
import { schemaKeys } from './support/schema.js'
|
||
import { TERMINE_DATETIMES } from '../server/services/termine/schema.js'
|
||
|
||
// Kurzfassung der Einbauanleitung im Einrichtungs-Ticket (TP2 Task D4): termineTicketDetails ist der Inhalt von
|
||
// config.features.termine.ticket.details. Es wirft nie ("bezahlt ist bezahlt, das Ticket entsteht immer"): bei Lesefehlern
|
||
// stehen Kopfzeilen, der Hinweis und der Verweis im Ticket. websiteProjects, workorders und customers legt das Setup-Skript nicht
|
||
// an: ohne Schema im Nachbau.
|
||
const aw = await startAppwriteMock({
|
||
schema: { portalFeatures: schemaKeys('portalFeatures'), termineConfig: schemaKeys('termineConfig'), termineBookings: schemaKeys('termineBookings') },
|
||
datetimes: TERMINE_DATETIMES,
|
||
})
|
||
after(() => aw.close())
|
||
useTestEnv({ appwriteUrl: aw.url, stripeUrl: 'http://127.0.0.1:1' })
|
||
const { config } = await import('../server/config.js')
|
||
const { ausVorlage } = await import('../server/services/termine/vorlagen.js')
|
||
const { neuerFeedToken } = await import('../server/services/termine/einstellungen.js')
|
||
const reg = await import('../server/services/featureRegistry.js')
|
||
const sf = await import('../server/services/siteFeatures.js')
|
||
const { termineTicketDetails } = await import('../server/services/termine/ticketDetails.js')
|
||
const { ticketStand, driftWarnung, gesundheitsMeldungen } = await import('../server/services/termine/einbauanleitung.js')
|
||
|
||
aw.seed('customers', { $id: 'c1', name: 'Kunde Eins', location: 'Berlin', email: 'kunde1@example.test' })
|
||
|
||
const NOW = new Date('2026-10-05T10:00:00Z')
|
||
const GEKUERZT = '… gekürzt, vollständig im Ticketsystem unter Kunden → Funktionen'
|
||
const HALBES_PAAR = /[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/
|
||
const MAX = reg.TICKET_DETAILS_MAX
|
||
const BETRIEB = {
|
||
name: 'Salon Haarlinie',
|
||
email: 'inhaber-intern@haarlinie.example',
|
||
telefon: '06301 1234',
|
||
adresse: 'Hauptstraße 1, 67722 Winnweiler',
|
||
kleinunternehmer: false,
|
||
impressumUrl: 'https://haarlinie.example/impressum',
|
||
datenschutzUrl: 'https://haarlinie.example/datenschutz',
|
||
bundesland: 'RP',
|
||
}
|
||
const PROJEKT = { projectName: 'Salon Haarlinie', liveDomain: 'haarlinie.example', subdomain: 'haarlinie' }
|
||
const SITE_KEY = 'abcdefghijklmnop'
|
||
const UPDATED_AT = '2026-10-03T08:15:00.000Z'
|
||
|
||
const einstellung = () => {
|
||
const e = ausVorlage('salon', BETRIEB, NOW)
|
||
e.farbe = '#9d4b6f'
|
||
return e
|
||
}
|
||
|
||
/** Maximalbefuellung wie im Plan: 50 Leistungen mit 100 Zeichen, 20 Personen, 5 Zusatzfelder, 10 Gesundheitsmeldungen, 6 Warnungen. */
|
||
function maximalEinstellung() {
|
||
const e = einstellung()
|
||
e.betrieb.telefon = ''
|
||
e.betrieb.adresse = ''
|
||
e.betrieb.impressumUrl = ''
|
||
e.betrieb.datenschutzUrl = ''
|
||
e.farbe = '#ffff00'
|
||
const vorlage = e.leistungen[0]
|
||
e.leistungen = Array.from({ length: 50 }, (_, i) => ({
|
||
...vorlage,
|
||
id: `s_max${String(i).padStart(5, '0')}`,
|
||
name: `${String(i + 1).padStart(2, '0')} ${'L'.repeat(97)}`,
|
||
aktiv: true,
|
||
ressourcen: [],
|
||
beschreibung: i < 10 ? 'Physiotherapie' : '',
|
||
}))
|
||
e.leistungen[0].name = 'Physiotherapie'
|
||
e.ressourcen = Array.from({ length: 20 }, (_, i) => ({
|
||
id: `r_max${String(i).padStart(5, '0')}`, typ: 'person', name: `Person ${i + 1} ${'P'.repeat(80)}`, aktiv: true, plan: { 1: [['09:00', '12:00']] }, feedToken: neuerFeedToken(),
|
||
}))
|
||
e.gastFelder = {
|
||
telefon: 'pflicht',
|
||
nachricht: 'optional',
|
||
zusatz: Array.from({ length: 5 }, (_, i) => ({ key: `z${i}`, label: `Zusatzfeld ${i + 1} ${'Z'.repeat(90)}`, typ: 'text', optionen: [], pflicht: false })),
|
||
}
|
||
return e
|
||
}
|
||
const MAX_FRAGEBOGEN = { einbau: { modus: 'knopf', knopfText: 'K'.repeat(40), stelle: 'S'.repeat(300), gestaltung: 'G'.repeat(1000), bemerkung: 'B'.repeat(1000) } }
|
||
|
||
let zaehler = 0
|
||
/** Projekt (Kunde c1) anlegen und den Terminplaner bestellen; mit e (und fb) liegt ein termineConfig-Dokument unter der Funktions-ID. */
|
||
async function bestelle({ e = null, fb = null, version = 1, projekt = PROJEKT } = {}) {
|
||
zaehler += 1
|
||
const projectId = `tt${zaehler}`
|
||
const sessionId = `cs_tt${zaehler}`
|
||
aw.seed('websiteProjects', { $id: projectId, customerId: 'c1', ...projekt })
|
||
const doc = await sf.markOrdered({ customerId: 'c1', projectId, key: 'termine', sessionId, consent: {} })
|
||
if (e) {
|
||
aw.seed('termineConfig', {
|
||
$id: doc.$id, customerId: 'c1', projectId, einstellung: JSON.stringify(e), version, updatedAt: UPDATED_AT, updatedBy: 'kunde:c1',
|
||
...(fb ? { fragebogen: JSON.stringify(fb) } : {}),
|
||
})
|
||
}
|
||
return { projectId, sessionId, doc }
|
||
}
|
||
|
||
/** ctx genau in der Form, die markPaid uebergibt: { feature: Register-Eintrag, doc, project, customer }. */
|
||
const ctxFuer = (doc, project = PROJEKT, customer = { name: 'Kunde Eins' }) => ({ feature: reg.featureConfig('termine'), doc, project, customer })
|
||
const absaetze = (text) => text.split('\n\n')
|
||
const stumm = () => mock.method(console, 'error', () => {})
|
||
|
||
// ------------------------------------------------------------------------------ termineTicketDetails ---
|
||
test('termineTicketDetails mit Einstellungen: Kopf mit Konfigurationsstand: Version 1, Snippet mit siteKey, Verweis, Stundenkontingent', async () => {
|
||
const { projectId, doc } = await bestelle({ e: einstellung() })
|
||
const text = await termineTicketDetails(ctxFuer(doc))
|
||
const [kopf] = absaetze(text)
|
||
assert.deepEqual(kopf.split('\n'), [
|
||
'Funktion: Terminplaner',
|
||
'Projekt: Salon Haarlinie',
|
||
'Domain: haarlinie.example',
|
||
'Kunde: Kunde Eins',
|
||
`siteKey: ${doc.siteKey}`,
|
||
'Konfigurationsstand: Version 1, 03.10.2026',
|
||
])
|
||
assert.ok(text.includes('Konfigurationsstand: Version 1'))
|
||
assert.equal(ticketStand(text), 1)
|
||
assert.ok(text.includes(`data-webklar-termine="${doc.siteKey}"`))
|
||
assert.ok(text.endsWith(`Verweis: ${reg.anleitungsLink('c1', projectId)}`))
|
||
assert.ok(text.includes('Nicht auf das Stundenkontingent anrechnen'))
|
||
assert.ok(text.includes('Umfang:\n- 4 Leistungen'), text)
|
||
assert.ok(!text.includes('Online-Terminbuchung') && !text.includes(BETRIEB.email), 'weder Datenschutz-Absatz noch Betriebs-E-Mail')
|
||
assert.ok(text.length <= MAX)
|
||
assert.ok(!text.includes('bezahlt'), 'hier wurde nur bestellt: keine Behauptung, die Einrichtung sei bezahlt')
|
||
})
|
||
|
||
test('termineTicketDetails: der Wunsch des Kunden aus dem Fragebogen steht im Ticket, der Modus im Snippet', async () => {
|
||
const fb = { einbau: { modus: 'knopf', knopfText: 'Termin sichern', stelle: 'Startseite unter dem Hero', gestaltung: 'Schlicht', bemerkung: 'Bitte vor dem 20.10.' } }
|
||
const { doc } = await bestelle({ e: einstellung(), fb })
|
||
const text = await termineTicketDetails(ctxFuer(doc))
|
||
assert.ok(text.includes(' Modus: knopf\n Knopf-Text: Termin sichern\n Stelle: Startseite unter dem Hero\n Gestaltung: Schlicht\n Bemerkung: Bitte vor dem 20.10.'), text)
|
||
assert.ok(text.includes('data-modus="knopf" data-knopf="Termin sichern"'))
|
||
})
|
||
|
||
test('termineTicketDetails ohne termineConfig: noch keine Einstellungen, Snippet, Verweis', async () => {
|
||
const { projectId, doc } = await bestelle()
|
||
const text = await termineTicketDetails(ctxFuer(doc))
|
||
assert.ok(text.includes('Konfigurationsstand: Version 0 (noch keine Einstellungen gespeichert)'), text)
|
||
assert.ok(text.includes('noch keine Einstellungen'))
|
||
assert.equal(ticketStand(text), 0)
|
||
assert.ok(text.includes(`data-webklar-termine="${doc.siteKey}"`))
|
||
assert.ok(text.endsWith(`Verweis: ${reg.anleitungsLink('c1', projectId)}`))
|
||
assert.ok(text.includes('Keine Einstellungen gespeichert'))
|
||
})
|
||
|
||
test('termineTicketDetails bei Lesefehler: loest auf, ohne Wurf, mit Kopfzeilen, Hinweis und Link', async () => {
|
||
const { projectId, doc } = await bestelle({ e: einstellung() })
|
||
const fehler = stumm()
|
||
try {
|
||
aw.failNext({ collection: 'termineConfig', method: 'GET', status: 500 })
|
||
const text = await termineTicketDetails(ctxFuer(doc))
|
||
const link = reg.anleitungsLink('c1', projectId)
|
||
for (const teil of ['Projekt: Salon Haarlinie', 'Domain: haarlinie.example', 'Kunde: Kunde Eins', `siteKey: ${doc.siteKey}`, `Verweis: ${link}`, 'Anleitung konnte nicht erzeugt werden, im WOMS öffnen', 'Nicht auf das Stundenkontingent anrechnen']) {
|
||
assert.ok(text.includes(teil), `${teil}\n---\n${text}`)
|
||
}
|
||
assert.equal(ticketStand(text), null, 'ohne Konfigurationsstand-Zeile kein Vergleich')
|
||
assert.equal(fehler.mock.callCount(), 1)
|
||
assert.ok(!String(fehler.mock.calls[0].arguments.join(' ')).includes(doc.siteKey), 'das Log nennt keine Daten des Betriebs')
|
||
} finally {
|
||
fehler.mock.restore()
|
||
}
|
||
// danach funktioniert es wieder (der Fehler war einmalig)
|
||
assert.ok((await termineTicketDetails(ctxFuer(doc))).includes('Konfigurationsstand: Version 1'))
|
||
})
|
||
|
||
test('termineTicketDetails wirft nie: auch nicht ohne Dokument, ohne ctx oder bei fehlender Collection', async () => {
|
||
const fehler = stumm()
|
||
try {
|
||
for (const ctx of [undefined, null, {}, { doc: null }, { doc: {} }, { doc: { $id: '' } }, { doc: { $id: 7 } }, ctxFuer({ $id: 'f_gibt_es_nicht', customerId: 'c1', projectId: 'p0', siteKey: SITE_KEY })]) {
|
||
const text = await termineTicketDetails(ctx)
|
||
assert.equal(typeof text, 'string', JSON.stringify(ctx))
|
||
assert.ok(text.includes('Terminplaner'), JSON.stringify(ctx))
|
||
assert.ok(text.length <= MAX && text.isWellFormed())
|
||
}
|
||
} finally {
|
||
fehler.mock.restore()
|
||
}
|
||
})
|
||
|
||
test('termineTicketDetails: Namen mit Markup bleiben Text, halbe Zeichenpaare werden ersetzt', async () => {
|
||
const e = einstellung()
|
||
e.leistungen[0].name = `Schnitt <b>x</b> \uD83D${'x'.repeat(8)}😀`
|
||
const projekt = { projectName: `Salon \uD83D <i>x</i>`, liveDomain: 'haarlinie.example' }
|
||
const { doc } = await bestelle({ e, projekt })
|
||
const text = await termineTicketDetails(ctxFuer(doc, projekt, { name: `Kunde \uDE00 & Co` }))
|
||
assert.ok(text.isWellFormed() && !HALBES_PAAR.test(text))
|
||
assert.ok(text.includes('<b>x</b>') && text.includes('<i>x</i>'), 'Klartext, kein HTML')
|
||
assert.ok(text.includes('😀'))
|
||
// ohne Kunde: die Kunden-ID wie in den Notfall-Details
|
||
assert.ok((await termineTicketDetails(ctxFuer(doc, projekt, null))).includes('Kunde: c1'))
|
||
})
|
||
|
||
test('Ticket als bleibender Arbeitsnachweis (AVV Nr. 9 Abs. 6): Betriebsangaben, Leistungsnamen, nur die ersten acht Personen, Wünsche des Kunden - und nichts von den Gästen', async () => {
|
||
const e = einstellung()
|
||
e.ressourcen = Array.from({ length: 10 }, (_, i) => ({
|
||
id: `r_tk${String(i).padStart(5, '0')}`, typ: 'person', name: `Mitarbeiter ${i + 1}`, aktiv: true, plan: { 1: [['09:00', '12:00']] }, feedToken: neuerFeedToken(),
|
||
}))
|
||
const fb = { einbau: { modus: 'inline', knopfText: '', stelle: 'Startseite', gestaltung: '', bemerkung: 'Bitte vor dem 20.10.' } }
|
||
const { doc } = await bestelle({ e, fb })
|
||
// eine Buchung dieses Terminplaners mit allen Angaben des Gastes: nichts davon darf ins Ticket gelangen
|
||
aw.seed('termineBookings', {
|
||
$id: 'b_tk1', featureId: doc.$id, customerId: 'c1', status: 'bestaetigt', leistungName: e.leistungen[0].name, ressourceName: 'Mitarbeiter 1',
|
||
gastName: 'Erika Gastgeberin', gastEmail: 'erika.gast@example.test', gastTelefon: '0171 5550000', gastNachricht: 'Bitte ohne Föhn', gastZusatz: '{"allergie":"Nuss"}', absageGrund: 'Krank',
|
||
})
|
||
const text = await termineTicketDetails(ctxFuer(doc))
|
||
assert.ok(text.includes('Projekt: Salon Haarlinie') && text.includes('Kunde: Kunde Eins'), 'Betriebsangaben')
|
||
assert.ok(text.includes(BETRIEB.telefon), 'Telefon des Betriebs (Ausweichtext im Einbau-Code)')
|
||
for (const l of e.leistungen) assert.ok(text.includes(l.name), `Leistung ${l.name}`)
|
||
for (let i = 1; i <= 8; i += 1) assert.ok(text.includes(`Mitarbeiter ${i}`), `Mitarbeiter ${i}`)
|
||
assert.ok(!text.includes('Mitarbeiter 9') && !text.includes('Mitarbeiter 10') && text.includes('und 2 weitere'), 'nur die ersten acht Namen')
|
||
assert.ok(text.includes('Bemerkung: Bitte vor dem 20.10.') && text.includes('Stelle: Startseite'), 'Wünsche des Kunden zum Einbau')
|
||
for (const gast of ['Erika', 'Gastgeberin', 'erika.gast', '0171', 'Föhn', 'Nuss', 'Krank']) assert.ok(!text.includes(gast), `keine Gastdaten: ${gast}`)
|
||
})
|
||
|
||
test('termineTicketDetails: Gesundheit (Physiotherapie) und Maximalbefuellung: enthaelt Physio und bleibt hoechstens 7000 Zeichen', async () => {
|
||
const e = maximalEinstellung()
|
||
const meldungen = gesundheitsMeldungen(e)
|
||
assert.equal(meldungen.length, 10)
|
||
const { doc } = await bestelle({ e, fb: MAX_FRAGEBOGEN, version: 12 })
|
||
const text = await termineTicketDetails(ctxFuer(doc))
|
||
assert.ok(text.length <= MAX, `kurz ${text.length}`)
|
||
assert.ok(text.includes('Physio'))
|
||
assert.ok(text.includes(`Gesundheitsbereich prüfen:\n${meldungen.map((m) => `- ${m}`).join('\n')}`), 'alle zehn Meldungen')
|
||
assert.ok(text.includes(`data-webklar-termine="${doc.siteKey}"`))
|
||
assert.ok(text.includes('Nicht auf das Stundenkontingent anrechnen'))
|
||
assert.equal(ticketStand(text), 12)
|
||
assert.ok(!text.includes('Online-Terminbuchung'))
|
||
for (const r of e.ressourcen) assert.ok(!text.includes(r.feedToken))
|
||
assert.ok(!text.includes(GEKUERZT), 'die Budgetliste laesst Teile weg und schneidet nicht ab')
|
||
// ticketTexte kuerzt nichts mehr
|
||
const texte = await reg.ticketTexte(ctxFuer(doc))
|
||
assert.equal(texte.details, text)
|
||
})
|
||
|
||
// ------------------------------------------------------------------------------------- Registry ---
|
||
test('Registry: featureConfig("termine").ticket.details(ctx) liefert den Text von termineTicketDetails, siteKey und $id aus ctx.doc', async () => {
|
||
const { doc } = await bestelle({ e: einstellung() })
|
||
const feature = reg.featureConfig('termine')
|
||
assert.equal(feature.ticket.topic(PROJEKT), 'Terminplaner einbauen – Salon Haarlinie')
|
||
// das Register-Objekt traegt keinen siteKey: der Text darf nur aus ctx.doc kommen
|
||
const ctx = { feature: { ...feature, siteKey: 'FALSCH_AUS_REGISTER', $id: 'f_falsch' }, doc, project: PROJEKT, customer: { name: 'Kunde Eins' } }
|
||
const ueberRegister = await feature.ticket.details(ctx)
|
||
assert.equal(ueberRegister, await termineTicketDetails(ctx))
|
||
assert.equal(ueberRegister, await termineTicketDetails(ctxFuer(doc)))
|
||
assert.ok(ueberRegister.includes(doc.siteKey) && !ueberRegister.includes('FALSCH_AUS_REGISTER'))
|
||
assert.ok(ueberRegister.includes('Konfigurationsstand: Version 1'), 'gelesen wurde das Dokument der Funktion (doc.$id)')
|
||
})
|
||
|
||
// ----------------------------------------------------------------------- Integration (Review Focus 1) ---
|
||
const workorder = (doc) => aw.docs('workorders').get(doc.ticketId)
|
||
|
||
test('markOrdered + markPaid: das Ticket traegt die Kurzfassung (hoechstens 7000, mit Snippet), die Stand-Zeile und der Vergleich meldet Aenderungen', async () => {
|
||
const { sessionId, doc: bestellt } = await bestelle({ e: maximalEinstellung(), fb: MAX_FRAGEBOGEN })
|
||
const { doc, created, fall } = await sf.markPaid({ sessionId })
|
||
assert.deepEqual([created, fall, doc.state], [true, null, 'setup'])
|
||
const ticket = workorder(doc)
|
||
assert.ok(ticket.details.length <= MAX, `details ${ticket.details.length}`)
|
||
assert.ok(ticket.details.includes(`data-webklar-termine="${bestellt.siteKey}"`))
|
||
assert.ok(ticket.details.includes('Einrichtung bezahlt, 149 €'), 'markPaid uebergibt das bezahlte Dokument (unlockedByPurchase)')
|
||
assert.ok(ticket.details.includes('Physio'))
|
||
assert.ok(!ticket.details.includes(GEKUERZT))
|
||
assert.equal(ticket.topic, 'Terminplaner einbauen – Salon Haarlinie')
|
||
// Ticket Version 1; die Einstellungen aendern sich spaeter: der Vergleich (Team-Route, D8) meldet es
|
||
const ticketVersion = ticketStand(ticket.details)
|
||
assert.equal(ticketVersion, 1)
|
||
assert.equal(driftWarnung(ticketVersion, 1), null)
|
||
assert.equal(driftWarnung(ticketVersion, 4), 'Einstellungen seit der Bestellung geändert (Ticket Version 1, jetzt 4).')
|
||
})
|
||
|
||
test('markPaid mit kaputtem termineConfig-Lesen legt das Ticket trotzdem an: created true, Notfall-Details mit Kopf, Hinweis und Link', async () => {
|
||
const { projectId, sessionId } = await bestelle({ e: einstellung() })
|
||
const fehler = stumm()
|
||
try {
|
||
aw.failNext({ collection: 'termineConfig', method: 'GET', status: 500 })
|
||
const { doc, created, fall } = await sf.markPaid({ sessionId })
|
||
assert.deepEqual([created, fall, doc.state], [true, null, 'setup'])
|
||
assert.ok(doc.ticketId, 'Zahlung nie ohne Ticket')
|
||
const ticket = workorder(doc)
|
||
assert.ok(ticket.details.includes(`siteKey: ${doc.siteKey}`))
|
||
assert.ok(ticket.details.includes('Anleitung konnte nicht erzeugt werden, im WOMS öffnen'))
|
||
assert.ok(ticket.details.includes(reg.anleitungsLink('c1', projectId)))
|
||
assert.ok(ticket.details.length <= MAX)
|
||
} finally {
|
||
fehler.mock.restore()
|
||
}
|
||
})
|
||
|
||
test('Webhook-Wiederholung nach einem Lesefehler: ohne Ticket bleibt setup fortsetzbar, die Kurzfassung entsteht beim zweiten Versuch', async () => {
|
||
const { sessionId } = await bestelle({ e: einstellung() })
|
||
const fehler = stumm()
|
||
try {
|
||
// das Anlegen des Tickets scheitert (nicht das Lesen): setup ohne ticketId
|
||
aw.failNext({ collection: 'workorders', method: 'POST', status: 500 })
|
||
await assert.rejects(() => sf.markPaid({ sessionId }))
|
||
const { doc, created } = await sf.markPaid({ sessionId })
|
||
assert.equal(created, true)
|
||
assert.ok(workorder(doc).details.includes('Konfigurationsstand: Version 1'))
|
||
} finally {
|
||
fehler.mock.restore()
|
||
}
|
||
})
|
||
|
||
test('config.features.termine.ticket.details ruft die Datei lazy auf (config.js bleibt ohne statischen Dienst-Import)', async () => {
|
||
const { readFileSync } = await import('node:fs')
|
||
const src = readFileSync(new URL('../server/config.js', import.meta.url), 'utf8')
|
||
assert.match(src, /import\('\.\/services\/termine\/ticketDetails\.js'\)/)
|
||
assert.ok(!/^import .*ticketDetails/m.test(src))
|
||
assert.equal(typeof config.features.termine.ticket.details, 'function')
|
||
})
|