296 lines
14 KiB
JavaScript
296 lines
14 KiB
JavaScript
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); die Fassung ist der
|
||
// Tag <F> = config.termine.avvVersion, beim Anheben von <F> ändert sich auch dieser Pin
|
||
const NEWS_SATZ =
|
||
'Ich verlange ausdrücklich, dass WEBklar vor Ablauf der Widerrufsfrist mit dem Einbau beginnt. Mir ist bekannt, dass mein Widerrufsrecht mit der vollständigen Erbringung des Einbaus erlischt (§ 356 Abs. 5 Nr. 2 BGB). Widerrufe ich vor der vollständigen Erbringung, schulde ich einen angemessenen Betrag für die bis dahin erbrachte Leistung (§ 357a Abs. 2 BGB).'
|
||
const NEWS_FASSUNG = '2026-10-05'
|
||
|
||
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: <div data-webklar-news="${SITE_KEY}"></div> + 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' },
|
||
})
|
||
assert.deepEqual(sf.FEATURE_KEYS, ['news', 'termine'])
|
||
assert.deepEqual(sf.FEATURE_KEYS, reg.featureKeys())
|
||
assert.deepEqual(reg.featureKeys(), ['news', 'termine'])
|
||
})
|
||
|
||
// ---------------------------------------------------------------------------------- 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.<key>.<haken> 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 fünf Haken lässt sich setzen; Nicht-Funktionen und fremde Namen zählen nicht
|
||
const namen = ['verfuegbar', 'bestellpruefung', 'unterlagenBereit', 'belehrung', 'freischaltbar']
|
||
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')
|
||
})
|