import { test, after } from 'node:test' import assert from 'node:assert/strict' import { startAppwriteMock } from './support/appwriteMock.js' import { startFakeStripe } from './support/fakeStripe.js' import { useTestEnv } from './support/testEnv.js' // Register config.features (News + Terminplaner) und featureRegistry.js. Dienste (siteFeatures, ticketAdmin) // brauchen die Test-Umgebung vor dem ersten Import von server/config.js. const aw = await startAppwriteMock() const st = await startFakeStripe() after(() => Promise.all([aw.close(), st.close()])) useTestEnv({ appwriteUrl: aw.url, stripeUrl: st.url }) const { config } = await import('../server/config.js') const reg = await import('../server/services/featureRegistry.js') const sf = await import('../server/services/siteFeatures.js') const { createFeatureTicket } = await import('../server/services/ticketAdmin.js') // Wortlaut-Pin des News-Satzes und der Fassung (der einzige Literal-Pin auf Texte, die Block F2 ändert) const NEWS_SATZ = 'Ich verlange ausdrücklich, dass WEBklar vor Ablauf der Widerrufsfrist mit dem Einbau beginnt. Mir ist bekannt, dass mein Widerrufsrecht erlischt, sobald der Einbau vollständig erbracht ist (§ 356 Abs. 4 BGB).' const NEWS_FASSUNG = '2026-09-30' const SITE_KEY = 'abcdefghijklmnop' // ---------------------------------------------------------------------------------- News (Festwerte) --- test('News-Eintrag entspricht den bisherigen Festwerten', async () => { const news = reg.featureConfig('news') assert.equal(news.key, 'news') assert.equal(news.product, 'newsSetup') assert.deepEqual(news.label, { name: 'News-Pinnwand', kurz: 'News', keine: 'keine News-Pinnwand' }) assert.equal(news.priceLabel, '30 €') assert.equal(news.setupEstimate, 'meist innerhalb von 1–2 Wochen') assert.equal(news.consent.version, NEWS_FASSUNG) assert.equal(news.consent.knopf, 'Zahlungspflichtig bestellen · 30 €') assert.deepEqual(news.consent.items, [{ id: 'beginn', haken: true, text: NEWS_SATZ }]) assert.deepEqual(news.leistungsbeschreibung, { version: NEWS_FASSUNG, url: '/vertragsunterlagen/funktionen-leistungsbeschreibung.html' }) assert.equal(news.avv, null) assert.equal(news.widerruf, null) assert.equal(news.returnHash, 'news') assert.equal(news.returnProjekt, false) assert.equal(news.requiresWartungsAbo, false) assert.equal(news.ticketFreischaltung, true) assert.equal( news.disabledMessage, 'Die News-Pinnwand wurde für diese Website abgeschaltet. Schreib uns im Chat, wenn du sie wieder möchtest.' ) assert.equal(news.disabledMessage, sf.DISABLED_MESSAGE) assert.equal(news.orderedConflictMessage, 'Die News-Pinnwand ist für dieses Projekt schon bestellt.') assert.equal(news.unavailableMessage, undefined) assert.equal(news.ticket.topic({ projectName: 'Bäckerei' }), 'News-Pinnwand einbauen – Bäckerei') assert.equal(news.ticket.topic({ subdomain: 'cafe' }), 'News-Pinnwand einbauen – cafe') assert.equal(news.ticket.topic(null), 'News-Pinnwand einbauen –') assert.deepEqual(news.mails, { funktion: 'News-Pinnwand', formen: { dein: 'Deine News-Pinnwand', nom: 'die News-Pinnwand', akk: 'die News-Pinnwand', gen: 'der News-Pinnwand', pron: 'sie' }, knopf: 'Beitrag schreiben', danach: 'Danach kannst Du Beiträge im Portal schreiben.', bereit: 'Du kannst ab sofort Beiträge schreiben, sie erscheinen direkt bei Dir auf der Seite.', preisHinweis: news.mails.preisHinweis, anleitung: false, }) assert.match(news.mails.preisHinweis, /einmalig/) }) test('News-Ticketdetails sind die sechs Zeilen des bisherigen Standards', async () => { const news = reg.featureConfig('news') const project = { $id: 'p1', projectName: 'Bäckerei', liveDomain: 'baeckerei.de', previewUrl: 'https://p.example', subdomain: 'baeckerei' } const ctx = { feature: news, doc: { siteKey: SITE_KEY, featureKey: 'news' }, project, customer: { name: 'Kunde' } } const expected = [ 'Funktion: news', 'Projekt: Bäckerei', 'Domain: baeckerei.de', `siteKey: ${SITE_KEY}`, `Einbau:
+ https://project.webklar.com/embed/news.js`, 'Anleitung: docs/funktionen/news-einbau.md (Repo Webklar-Kundenbereich)', ].join('\n') assert.equal(await news.ticket.details(ctx), expected) // Domain: liveDomain || previewUrl || subdomain; Projektname: projectName || subdomain const ohneLive = await news.ticket.details({ ...ctx, project: { projectName: 'X', previewUrl: 'https://p.example', subdomain: 's' } }) assert.match(ohneLive, /^Domain: https:\/\/p\.example$/m) const nurSub = await news.ticket.details({ ...ctx, project: { subdomain: 's' } }) assert.match(nurSub, /^Projekt: s$/m) assert.match(nurSub, /^Domain: s$/m) // Parität zu createFeatureTicket (Standardtexte): dasselbe Dokument, Wort für Wort aw.seed('customers', { $id: 'c1', name: 'Kunde Eins', location: 'Berlin' }) const { ticketId } = await createFeatureTicket({ customerId: 'c1', project, featureKey: 'news', siteKey: SITE_KEY }) const ticket = aw.docs('workorders').get(ticketId) assert.equal(ticket.details, expected) assert.equal(ticket.topic, news.ticket.topic(project)) }) // ---------------------------------------------------------------------------------- Alias funktionen --- test('Alias bleibt: config.funktionen folgt config.features.news zur Laufzeit', () => { const news = config.features.news assert.equal(config.funktionen.setupEstimate, news.setupEstimate) assert.equal(config.funktionen.consentText, news.consent.items[0].text) assert.equal(config.funktionen.consentText, NEWS_SATZ) const vorherEstimate = news.setupEstimate const vorherText = news.consent.items[0].text try { news.setupEstimate = 'in 3 Tagen' news.consent.items[0].text = 'Neuer Satz' assert.equal(config.funktionen.setupEstimate, 'in 3 Tagen') assert.equal(config.funktionen.consentText, 'Neuer Satz') } finally { news.setupEstimate = vorherEstimate news.consent.items[0].text = vorherText } assert.equal(config.funktionen.setupEstimate, vorherEstimate) assert.equal(config.funktionen.consentText, vorherText) }) // ---------------------------------------------------------------------------------- siteFeatures --- test('FEATURE_LABELS und FEATURE_KEYS unverändert, aus dem Register abgeleitet', () => { assert.deepEqual(sf.FEATURE_LABELS, { news: { name: 'News-Pinnwand', keine: 'keine News-Pinnwand' }, termine: { name: 'Terminplaner', keine: 'keinen Terminplaner' }, zeiten: { name: 'Öffnungszeiten live', keine: 'keine Öffnungszeiten live' }, }) assert.deepEqual(sf.FEATURE_KEYS, ['news', 'termine', 'zeiten']) assert.deepEqual(sf.FEATURE_KEYS, reg.featureKeys()) assert.deepEqual(reg.featureKeys(), ['news', 'termine', 'zeiten']) }) // ---------------------------------------------------------------------------------- Schlüsselschutz --- test('Schlüsselschutz: nur eigene Schlüssel des Registers', () => { for (const key of ['analyse', '__proto__', 'constructor', 'toString', 'hasOwnProperty', undefined, null, '', 'News']) { assert.equal(reg.featureConfig(key), null, `featureConfig(${String(key)})`) assert.equal(reg.oeffentlicheAnsicht(key), null, `oeffentlicheAnsicht(${String(key)})`) assert.deepEqual(reg.featureHaken(key), {}, `featureHaken(${String(key)})`) } assert.equal(reg.featureConfig('news'), config.features.news) assert.equal(reg.featureConfig('termine'), config.features.termine) }) // ---------------------------------------------------------------------------------- Haken --- test('Haken überschreibbar: config.features.. gewinnt gegen den Standard', () => { const termine = config.features.termine const vorherTermine = reg.featureHaken('termine') const vorherNews = reg.featureHaken('news') const fn = () => [] try { termine.bestellpruefung = fn const haken = reg.featureHaken('termine') assert.equal(haken.bestellpruefung, fn) // die anderen Haken bleiben beim Standard for (const name of ['verfuegbar', 'unterlagenBereit', 'belehrung', 'freischaltbar']) assert.equal(haken[name], vorherTermine[name]) } finally { delete termine.bestellpruefung } assert.deepEqual(reg.featureHaken('termine'), vorherTermine) // jeder der sechs Haken lässt sich setzen; Nicht-Funktionen und fremde Namen zählen nicht const namen = ['verfuegbar', 'bestellpruefung', 'unterlagenBereit', 'belehrung', 'freischaltbar', 'nachFreischaltung'] const news = config.features.news try { for (const name of namen) news[name] = () => name news.unbekannt = () => 'x' const haken = reg.featureHaken('news') assert.deepEqual(Object.keys(haken).sort(), [...namen].sort()) for (const name of namen) assert.equal(haken[name](), name) news.verfuegbar = 'kein Haken' assert.equal(reg.featureHaken('news').verfuegbar, vorherNews.verfuegbar) } finally { for (const name of [...namen, 'unbekannt']) delete news[name] } assert.deepEqual(reg.featureHaken('news'), vorherNews) }) // ---------------------------------------------------------------------------------- öffentliche Ansicht --- test('Ansicht ohne Funktionen: JSON-sicher, ohne ticket und mails', () => { for (const key of reg.featureKeys()) { const ansicht = reg.oeffentlicheAnsicht(key) assert.deepEqual(JSON.parse(JSON.stringify(ansicht)), ansicht) assert.deepEqual(Object.keys(ansicht).sort(), [ 'avv', 'consent', 'key', 'kurz', 'leistungsbeschreibung', 'name', 'priceLabel', 'requiresWartungsAbo', 'returnHash', 'setupEstimate', 'widerruf', ]) assert.equal(ansicht.ticket, undefined) assert.equal(ansicht.mails, undefined) assert.deepEqual(Object.keys(ansicht.consent).sort(), ['items', 'knopf', 'version']) for (const item of ansicht.consent.items) assert.deepEqual(Object.keys(item).sort(), ['haken', 'id', 'text']) } const news = reg.oeffentlicheAnsicht('news') assert.equal(news.name, 'News-Pinnwand') assert.equal(news.kurz, 'News') assert.equal(news.priceLabel, '30 €') assert.equal(news.consent.knopf, 'Zahlungspflichtig bestellen · 30 €') assert.deepEqual(news.consent.items, [{ id: 'beginn', text: NEWS_SATZ, haken: true }]) assert.equal(news.returnHash, 'news') assert.equal(news.requiresWartungsAbo, false) assert.equal(news.avv, null) // eine Kopie: Änderungen an der Ansicht ändern das Register nicht news.consent.items[0].text = 'verändert' news.leistungsbeschreibung.version = 'verändert' assert.equal(config.features.news.consent.items[0].text, NEWS_SATZ) assert.equal(config.features.news.leistungsbeschreibung.version, NEWS_FASSUNG) }) // ---------------------------------------------------------------------------------- Termine --- test('Termine: Preis und Struktur', async () => { const t = reg.featureConfig('termine') assert.equal(t.key, 'termine') assert.equal(t.product, 'termineSetup') assert.deepEqual(t.label, { name: 'Terminplaner', kurz: 'Termine', keine: 'keinen Terminplaner' }) assert.equal(t.priceLabel, '149 €') assert.equal(t.setupEstimate, 'meist innerhalb von 1–2 Wochen') assert.equal(t.consent.knopf, 'Zahlungspflichtig bestellen · 149 €') assert.deepEqual( t.consent.items.map((i) => i.id), ['unterlagen', 'gesundheit', 'beginn', 'information'] ) assert.deepEqual( t.consent.items.map((i) => i.haken), [true, true, true, true] ) for (const item of t.consent.items) { assert.equal(typeof item.text, 'string') assert.ok(item.text.length > 0) assert.equal(item.url, undefined, 'ohne url') } assert.equal(t.widerruf, null) assert.equal(t.returnHash, 'termine') assert.equal(t.returnProjekt, true) assert.equal(t.requiresWartungsAbo, true) assert.equal(t.ticketFreischaltung, true) assert.equal(t.disabledMessage, 'Der Terminplaner ist für diese Website abgeschaltet. Schreib uns im Chat, wenn Du ihn wieder möchtest.') assert.equal(t.orderedConflictMessage, 'Der Terminplaner ist für dieses Projekt schon bestellt.') assert.equal(t.unavailableMessage, 'Der Terminplaner ist noch nicht verfügbar.') assert.equal(config.ustHinweis, 'ohne Umsatzsteuer gemäß § 19 UStG') assert.equal(t.mails.preisHinweis, `einmalig, ${config.ustHinweis}; der Terminplaner läuft, solange Dein Wartungs-Abo läuft`) assert.ok(t.mails.preisHinweis.includes(config.ustHinweis)) assert.equal(t.mails.funktion, 'Terminplaner') assert.deepEqual(t.mails.formen, { dein: 'Dein Terminplaner', nom: 'der Terminplaner', akk: 'den Terminplaner', gen: 'des Terminplaners', pron: 'ihn', }) assert.equal(t.mails.knopf, 'Termine öffnen') assert.equal(t.mails.anleitung, true) assert.equal(typeof t.mails.danach, 'string') assert.equal(typeof t.mails.bereit, 'string') assert.equal(t.ticket.topic({ projectName: 'Salon' }), 'Terminplaner einbauen – Salon') assert.equal(t.ticket.topic({ subdomain: 'salon' }), 'Terminplaner einbauen – salon') assert.equal(typeof t.ticket.details, 'function') const ansicht = reg.oeffentlicheAnsicht('termine') assert.equal(ansicht.name, 'Terminplaner') assert.equal(ansicht.kurz, 'Termine') assert.equal(ansicht.priceLabel, '149 €') assert.equal(ansicht.requiresWartungsAbo, true) assert.equal(ansicht.returnHash, 'termine') }) test('Termine-Ticketdetails werden erst beim Aufruf geladen', async () => { // das Ziel termine/ticketDetails.js entsteht in Task B8; config.js importiert keine Dienste statisch const { readFileSync } = await import('node:fs') const src = readFileSync(new URL('../server/config.js', import.meta.url), 'utf8') assert.doesNotMatch(src, /^import .* from '\.\/services\//m) assert.match(src, /import\('\.\/services\/termine\/ticketDetails\.js'\)/) }) // ---------------------------------------------------------------------------------- Block F --- test('Bestelltexte ohne ENTWURF', { todo: 'Block F ersetzt die Platzhaltertexte' }, async () => { const t = config.features.termine const frei = (wert, was) => assert.doesNotMatch(JSON.stringify(wert), /ENTWURF/, `${was} enthält ENTWURF`) frei(t.consent.version, 'consent.version') frei(t.consent.items, 'consent.items') frei(t.leistungsbeschreibung, 'leistungsbeschreibung') frei(t.avv, 'avv') frei(t.widerruf, 'widerruf') // mails.danach und mails.bereit schreibt der Inhaber in seiner Mailrunde // ab B7 gibt es den Haken belehrung (Platzhaltertext bis F7) const belehrung = reg.featureHaken('termine').belehrung if (belehrung) frei(await belehrung(), 'belehrung') })