- markOrdered speichert IP und Browser der Bestellung nicht mehr am Funktionsdokument (nur im Nachweis featureConsents, dort mit Löschfrist); Altwerte werden bei einer neuen Bestellung geleert - Aufbewahrungslauf (retention): consentIp, consentUserAgent, consentText, consentAt werden an allen Funktionsdokumenten eines fälligen Kunden geleert, auch an abgeschalteten; Fehlschlag hält den Kunden wie die übrigen abhängigen Daten - Einmal-Skript scripts/feature-consent-felder-leeren.mjs (Standard Probelauf, --apply leert nur, wo featureConsents die Angabe trägt) - 30-Tage-Reset (jobs.js): auch eine bezahlte Einrichtung mit Grund abo_ende_vor_einrichtung und noch nicht gemeldetem T19 wird nicht zurückgesetzt, solange zahlungMeldung nicht gemeldet ist (die Spur der Erstattung bleibt) - K22: enddatum ist das Abo-Ende (lage.endedAt), sonst der Tag des Abschaltens; loeschdatum bleibt 30 Tage nach dem Abschalten Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
1189 lines
59 KiB
JavaScript
1189 lines
59 KiB
JavaScript
import { test, after, beforeEach, mock } from 'node:test'
|
|
import assert from 'node:assert/strict'
|
|
import { createHash } from 'node:crypto'
|
|
import { startAppwriteMock } from './support/appwriteMock.js'
|
|
import { startFakeStripe } from './support/fakeStripe.js'
|
|
import { useTestEnv } from './support/testEnv.js'
|
|
import { schemaKeys } from './support/schema.js'
|
|
|
|
// Checkout je Funktion (Tasks B6 und B7): POST /api/funktionen/:key/checkout.
|
|
// Teil 1 (B6): Vorpruefungen in der Reihenfolge der Spezifikation (1 Kundenansicht, 2 Schluessel, 3 Zustimmung, 4 Projekt,
|
|
// 5 Schalter, 5b Unterlagen, 6 Bestellpruefung, 7 Wartungs-Abo). Jede Ablehnung laesst keine Spur zurueck: keine
|
|
// Stripe-Sitzung, kein featureConsents- und kein portalFeatures-Dokument.
|
|
// Teil 2 (B7, ab "Sperre, Zustand, Nachweis"): Zustandspruefung, Stripe-Sitzung, Zustimmungsnachweis vor der Weiterleitung,
|
|
// markOrdered; Wiederverwendung der offenen Sitzung, "Zahlung fortsetzen", parallele Klicks.
|
|
const aw = await startAppwriteMock({
|
|
schema: {
|
|
portalFeatures: schemaKeys('portalFeatures'),
|
|
subscriptions: schemaKeys('subscriptions'),
|
|
featureConsents: schemaKeys('featureConsents'),
|
|
termineConfig: schemaKeys('termineConfig'),
|
|
contractEvents: schemaKeys('contractEvents'),
|
|
},
|
|
})
|
|
const st = await startFakeStripe()
|
|
useTestEnv({ appwriteUrl: aw.url, stripeUrl: st.url })
|
|
const { config } = await import('../server/config.js')
|
|
const { startTestApp, post } = await import('./support/testApp.js')
|
|
const { customerCookie, kundenansichtCookie, seedFixtures, subscriptionDoc, wartungDoc } = await import('./support/fixtures.js')
|
|
const { default: funktionenRoutes } = await import('../server/routes/funktionen.js')
|
|
const { bestellbarerStand, entwurfDoc } = await import('./support/fragebogenStand.js')
|
|
const { deleteDocument } = await import('../server/services/appwriteAdmin.js')
|
|
const reg = await import('../server/services/featureRegistry.js')
|
|
const order = await import('../server/services/featureOrder.js')
|
|
const consents = await import('../server/services/featureConsents.js')
|
|
const sf = await import('../server/services/siteFeatures.js')
|
|
const { widerrufsbelehrungFunktion } = await import('../server/services/termine/rechtstexte.js')
|
|
|
|
const app = await startTestApp({ '/api/funktionen': funktionenRoutes })
|
|
after(() => Promise.all([app.close(), aw.close(), st.close()]))
|
|
|
|
const T = config.features.termine
|
|
const STANDARD_TERMINE_MODUS = { mode: config.termine.mode, pilotEmails: config.termine.pilotEmails }
|
|
const DAY_MS = 86400000
|
|
|
|
// seedFixtures gibt c1 nur ein Hosting-Abo (sub_h): Termine-Faelle ueberschreiben es mit einem Wartungs-Abo
|
|
const wartung = (patch = {}) => aw.seed('subscriptions', subscriptionDoc('sub_h', { ...wartungDoc, ...patch }))
|
|
|
|
// Jeder Test beginnt sauber: Standard-Abos (c1 Wartung), Pilot fuer alle, keine Haken-Ueberschreibungen, keine Dokumente
|
|
beforeEach(async () => {
|
|
for (const col of ['subscriptions', 'portalFeatures', 'featureConsents', 'termineConfig', 'contractEvents']) {
|
|
for (const id of aw.docs(col).keys()) await deleteDocument(col, id)
|
|
}
|
|
seedFixtures(aw)
|
|
aw.seed('websiteProjects', { $id: 'p1', customerId: 'c1', projectName: 'Salon' })
|
|
aw.seed('websiteProjects', { $id: 'p3', customerId: 'c3', projectName: 'Kiosk' })
|
|
aw.seed('websiteProjects', { $id: 'p5', customerId: 'c2', projectName: 'Fremd' })
|
|
wartung()
|
|
st.checkoutSessions.clear()
|
|
config.termine.mode = 'pilot'
|
|
config.termine.pilotEmails = ['*']
|
|
for (const name of ['verfuegbar', 'bestellpruefung', 'unterlagenBereit']) delete T[name]
|
|
})
|
|
after(() => {
|
|
config.termine.mode = STANDARD_TERMINE_MODUS.mode
|
|
config.termine.pilotEmails = STANDARD_TERMINE_MODUS.pilotEmails
|
|
})
|
|
|
|
const c1 = customerCookie('c1')
|
|
const checkout = (key, body, cookie = c1) => post(`/api/funktionen/${key}/checkout`, body, { cookie })
|
|
const pflichtHaken = (feature = T) => feature.consent.items.filter((item) => item.haken).map((item) => item.id)
|
|
const zustimmung = (patch = {}) => ({ version: T.consent.version, haken: pflichtHaken(), ...patch })
|
|
const bodyT = (extra = {}) => ({ projectId: 'p1', zustimmung: zustimmung(), ...extra })
|
|
const spuren = () => ({ sessions: st.checkoutSessions.size, consents: aw.docs('featureConsents').size, features: aw.docs('portalFeatures').size })
|
|
const keineSpuren = () => assert.deepEqual(spuren(), { sessions: 0, consents: 0, features: 0 }, 'Ablehnung ohne Spur')
|
|
const stumm = () => mock.method(console, 'error', () => {})
|
|
|
|
/** Zeichnet die Reihenfolge der Stufen auf: Haken unterlagenBereit/bestellpruefung und orderDeps.aboMemo (Zaehler). */
|
|
function ablaufAufzeichnen({ unterlagen = true, offen = [] } = {}) {
|
|
const ablauf = []
|
|
const args = []
|
|
T.unterlagenBereit = async () => {
|
|
ablauf.push('unterlagen')
|
|
return unterlagen
|
|
}
|
|
T.bestellpruefung = async (arg) => {
|
|
ablauf.push('pruefung')
|
|
args.push(arg)
|
|
return offen
|
|
}
|
|
const echt = order.orderDeps.aboMemo
|
|
order.orderDeps.aboMemo = () => {
|
|
ablauf.push('abo')
|
|
return echt()
|
|
}
|
|
return { ablauf, args, ende: () => (order.orderDeps.aboMemo = echt) }
|
|
}
|
|
|
|
// ------------------------------------------------------------------------------------------ 1 Kundenansicht ---
|
|
test('Kundenansicht: 403, auch mit gueltigem Body (Testanwendung ohne Waechter)', async () => {
|
|
const res = await checkout('termine', bodyT(), kundenansichtCookie())
|
|
assert.equal(res.status, 403)
|
|
assert.equal(res.body.error, 'In der Kundenansicht kann nicht bestellt werden.')
|
|
keineSpuren()
|
|
// News ebenso, und die Sperre kommt vor dem Schluessel: ein unbekannter Schluessel ist in der Kundenansicht 403, nicht 404
|
|
assert.equal((await checkout('news', { projectId: 'p1', consent: true }, kundenansichtCookie())).status, 403)
|
|
assert.equal((await checkout('nope', {}, kundenansichtCookie())).status, 403)
|
|
keineSpuren()
|
|
})
|
|
|
|
// ------------------------------------------------------------------------------------------ 2 Schluessel ---
|
|
test('unbekannter Schluessel: 404 (analyse, __proto__, nope, Gross-/Kleinschreibung)', async () => {
|
|
for (const key of ['analyse', '__proto__', 'nope', 'News', 'constructor']) {
|
|
const res = await checkout(key, bodyT())
|
|
assert.equal(res.status, 404, key)
|
|
assert.equal(res.body.error, 'Unbekannte Funktion', key)
|
|
}
|
|
keineSpuren()
|
|
})
|
|
|
|
// ------------------------------------------------------------------------------------------ 3 Zustimmung ---
|
|
test('Zustimmung fehlt oder ist leer: 400 zustimmung_fehlt', async () => {
|
|
const faelle = [
|
|
{ projectId: 'p1' },
|
|
{ projectId: 'p1', zustimmung: null },
|
|
{ projectId: 'p1', zustimmung: {} },
|
|
{ projectId: 'p1', zustimmung: zustimmung({ haken: [] }) },
|
|
{ projectId: 'p1', zustimmung: zustimmung({ haken: undefined }) },
|
|
{ projectId: 'p1', zustimmung: zustimmung({ version: undefined }) },
|
|
{ projectId: 'p1', zustimmung: 'alles' },
|
|
{ projectId: 'p1', zustimmung: [] },
|
|
{ projectId: 'p1', zustimmung: zustimmung({ haken: 'unterlagen' }) },
|
|
{ projectId: 'p1', zustimmung: zustimmung({ haken: [...pflichtHaken(), 42] }) },
|
|
{ projectId: 'p1', zustimmung: zustimmung({ haken: [...pflichtHaken(), null] }) },
|
|
]
|
|
for (const body of faelle) {
|
|
const res = await checkout('termine', body)
|
|
assert.equal(res.status, 400, JSON.stringify(body))
|
|
assert.equal(res.body.code, 'zustimmung_fehlt', JSON.stringify(body))
|
|
assert.match(res.body.error, /zu/)
|
|
}
|
|
keineSpuren()
|
|
})
|
|
|
|
test('Zustimmung: ein Pflicht-Haken fehlt oder eine Haken-ID ist unbekannt: 400', async () => {
|
|
const ids = pflichtHaken()
|
|
assert.equal(ids.length, 4, 'Terminplaner hat vier Haken')
|
|
for (const fehlt of ids) {
|
|
const res = await checkout('termine', bodyT({ zustimmung: zustimmung({ haken: ids.filter((id) => id !== fehlt) }) }))
|
|
assert.equal(res.status, 400, `ohne ${fehlt}`)
|
|
assert.equal(res.body.code, 'zustimmung_fehlt', `ohne ${fehlt}`)
|
|
}
|
|
// unbekannte ID, auch neben vollstaendigen Haken; ebenso die ID eines Items ohne Haken
|
|
for (const fremd of ['gibt-es-nicht', '__proto__', '']) {
|
|
const res = await checkout('termine', bodyT({ zustimmung: zustimmung({ haken: [...ids, fremd] }) }))
|
|
assert.equal(res.status, 400, `unbekannt ${fremd}`)
|
|
assert.equal(res.body.code, 'zustimmung_fehlt', `unbekannt ${fremd}`)
|
|
}
|
|
keineSpuren()
|
|
})
|
|
|
|
test('Zustimmung: falsche Fassung ist 409 fassung_veraltet mit der Serverfassung und allen Item-Texten', async () => {
|
|
const res = await checkout('termine', bodyT({ zustimmung: zustimmung({ version: '1999-01-01' }) }))
|
|
assert.equal(res.status, 409)
|
|
assert.equal(res.body.code, 'fassung_veraltet')
|
|
assert.equal(res.body.consent.version, T.consent.version)
|
|
assert.deepEqual(res.body.consent, reg.oeffentlicheAnsicht('termine').consent)
|
|
assert.deepEqual(
|
|
res.body.consent.items.map((item) => item.text),
|
|
T.consent.items.map((item) => item.text),
|
|
'alle Item-Texte'
|
|
)
|
|
assert.equal(typeof res.body.error, 'string')
|
|
// die Fassung entscheidet auch dann, wenn die Haken nicht passen (der Browser zeigt erst die neue Fassung)
|
|
const alt = await checkout('termine', bodyT({ zustimmung: zustimmung({ version: 'alt', haken: ['frueher-anderes-item'] }) }))
|
|
assert.equal(alt.status, 409)
|
|
assert.equal(alt.body.code, 'fassung_veraltet')
|
|
keineSpuren()
|
|
// zur Laufzeit geaenderte Serverfassung gilt sofort (config bei jedem Aufruf gelesen)
|
|
const vorher = T.consent.version
|
|
const alterBody = bodyT()
|
|
try {
|
|
T.consent.version = 'neuere-fassung'
|
|
const veraltet = await checkout('termine', alterBody)
|
|
assert.equal(veraltet.status, 409)
|
|
assert.equal(veraltet.body.consent.version, 'neuere-fassung')
|
|
} finally {
|
|
T.consent.version = vorher
|
|
}
|
|
})
|
|
|
|
test('consent: true gilt nur fuer Funktionen mit genau einem Haken: Termine 400, News weiter', async () => {
|
|
const termine = await checkout('termine', { projectId: 'p1', consent: true })
|
|
assert.equal(termine.status, 400)
|
|
assert.equal(termine.body.code, 'zustimmung_fehlt')
|
|
keineSpuren()
|
|
// News: consent true kommt durch die Zustimmung (fremdes Projekt => erst danach 403), false/fehlend nicht
|
|
assert.equal((await checkout('news', { projectId: 'p5', consent: true })).status, 403)
|
|
for (const body of [{ projectId: 'p1', consent: false }, { projectId: 'p1' }, { projectId: 'p1', consent: 'true' }, { projectId: 'p1', consent: 1 }]) {
|
|
const res = await checkout('news', body)
|
|
assert.equal(res.status, 400, JSON.stringify(body))
|
|
assert.equal(res.body.code, 'zustimmung_fehlt', JSON.stringify(body))
|
|
}
|
|
keineSpuren()
|
|
})
|
|
|
|
test('News mit zustimmung-Objekt: richtige Fassung kommt durch, alte ist 409 mit der News-Fassung', async () => {
|
|
const news = config.features.news
|
|
const ok = { version: news.consent.version, haken: ['beginn'] }
|
|
assert.equal((await checkout('news', { projectId: 'p5', zustimmung: ok })).status, 403, 'Zustimmung ok, fremdes Projekt')
|
|
const alt = await checkout('news', { projectId: 'p1', zustimmung: { ...ok, version: 'alt' } })
|
|
assert.equal(alt.status, 409)
|
|
assert.deepEqual(alt.body.consent, reg.oeffentlicheAnsicht('news').consent)
|
|
// die Haken des Terminplaners gelten nicht fuer News und umgekehrt
|
|
assert.equal((await checkout('news', { projectId: 'p1', zustimmung: { ...ok, haken: ['unterlagen'] } })).status, 400)
|
|
assert.equal((await checkout('termine', { projectId: 'p1', zustimmung: zustimmung({ haken: ['beginn'] }) })).status, 400)
|
|
keineSpuren()
|
|
})
|
|
|
|
// ------------------------------------------------------------------------------------------ 4 Projekt ---
|
|
test('Projekt: ohne projectId 400, fremdes Projekt 403', async () => {
|
|
for (const projectId of [undefined, '', 12, null, {}]) {
|
|
const res = await checkout('termine', { projectId, zustimmung: zustimmung() })
|
|
assert.equal(res.status, 400, String(projectId))
|
|
assert.equal(res.body.error, 'Bitte wähle ein Projekt.')
|
|
}
|
|
const fremd = await checkout('termine', bodyT({ projectId: 'p5' }))
|
|
assert.equal(fremd.status, 403)
|
|
assert.equal((await checkout('termine', bodyT({ projectId: 'gibt-es-nicht' }))).status, 403)
|
|
keineSpuren()
|
|
})
|
|
|
|
// ------------------------------------------------------------------------------------------ 5 Schalter ---
|
|
test('Schalter: mode off und Pilot mit fremder Adresse sind 409 nicht_verfuegbar, News unabhaengig vom Modus', async () => {
|
|
config.termine.mode = 'off'
|
|
const aus = await checkout('termine', bodyT())
|
|
assert.equal(aus.status, 409)
|
|
assert.equal(aus.body.error, 'Der Terminplaner ist noch nicht verfügbar.')
|
|
assert.equal(aus.body.code, 'nicht_verfuegbar')
|
|
keineSpuren()
|
|
|
|
config.termine.mode = 'pilot'
|
|
config.termine.pilotEmails = ['jemand-anderes@example.invalid']
|
|
const fremd = await checkout('termine', bodyT())
|
|
assert.equal(fremd.status, 409)
|
|
assert.equal(fremd.body.code, 'nicht_verfuegbar')
|
|
keineSpuren()
|
|
|
|
// eigene Adresse in der Pilotliste und Modus on lassen durch (die Stufen danach lehnen hier nichts ab)
|
|
for (const [mode, pilotEmails] of [['pilot', ['k@example.invalid']], ['on', []]]) {
|
|
config.termine.mode = mode
|
|
config.termine.pilotEmails = pilotEmails
|
|
const res = await checkout('termine', bodyT())
|
|
assert.notEqual(res.body?.code, 'nicht_verfuegbar', `${mode} ${pilotEmails}`)
|
|
}
|
|
|
|
// News kennt den Schalter nicht
|
|
config.termine.mode = 'off'
|
|
const news = await checkout('news', { projectId: 'p1', consent: true })
|
|
assert.equal(news.status, 200)
|
|
assert.match(news.body.url, /^https?:\/\//)
|
|
})
|
|
|
|
test('featureSperre: Text bei nicht verfuegbar, sonst null; Haken und Meldung ueberschreibbar', () => {
|
|
config.termine.mode = 'off'
|
|
assert.equal(reg.featureSperre('termine', 'k@example.invalid'), 'Der Terminplaner ist noch nicht verfügbar.')
|
|
assert.equal(reg.featureSperre('news', 'k@example.invalid'), null, 'News ohne Haken verfuegbar')
|
|
config.termine.mode = 'pilot'
|
|
config.termine.pilotEmails = ['k@example.invalid']
|
|
assert.equal(reg.featureSperre('termine', 'k@example.invalid'), null)
|
|
assert.equal(reg.featureSperre('termine', 'x@example.invalid'), 'Der Terminplaner ist noch nicht verfügbar.')
|
|
assert.equal(reg.featureSperre('termine', undefined), 'Der Terminplaner ist noch nicht verfügbar.')
|
|
config.termine.mode = 'on'
|
|
assert.equal(reg.featureSperre('termine', 'x@example.invalid'), null)
|
|
// ueberschriebener Haken und Meldung des Registers
|
|
const meldung = T.unavailableMessage
|
|
try {
|
|
T.verfuegbar = (email) => email === 'ok@example.invalid'
|
|
T.unavailableMessage = 'Gerade nicht.'
|
|
assert.equal(reg.featureSperre('termine', 'ok@example.invalid'), null)
|
|
assert.equal(reg.featureSperre('termine', 'k@example.invalid'), 'Gerade nicht.')
|
|
delete T.unavailableMessage
|
|
assert.equal(reg.featureSperre('termine', 'k@example.invalid'), 'Diese Funktion ist noch nicht verfügbar.', 'ohne Meldung im Register')
|
|
} finally {
|
|
T.unavailableMessage = meldung
|
|
delete T.verfuegbar
|
|
}
|
|
// unbekannter Schluessel: nicht verfuegbar (fail-closed)
|
|
assert.equal(typeof reg.featureSperre('analyse', 'k@example.invalid'), 'string')
|
|
})
|
|
|
|
test('Standard-Haken des Terminplaners: verfuegbar, bestellpruefung und unterlagenBereit sind gefuellt, News hat keine', async () => {
|
|
const haken = reg.featureHaken('termine')
|
|
for (const name of ['verfuegbar', 'bestellpruefung', 'unterlagenBereit']) assert.equal(typeof haken[name], 'function', name)
|
|
assert.equal(await haken.unterlagenBereit(), true, 'Standard: immer bereit')
|
|
assert.deepEqual(reg.featureHaken('news'), {})
|
|
// dieselben Funktionen bei jedem Aufruf (der Haken-Test in featureRegistry.test.js vergleicht per ===)
|
|
assert.equal(reg.featureHaken('termine').bestellpruefung, haken.bestellpruefung)
|
|
})
|
|
|
|
// ------------------------------------------------------------------------------------------ 5b Unterlagen ---
|
|
test('Unterlagen nicht abrufbar: 503 unterlagen_fehlen, ohne Spur', async () => {
|
|
const lauf = ablaufAufzeichnen({ unterlagen: false })
|
|
try {
|
|
const res = await checkout('termine', bodyT())
|
|
assert.equal(res.status, 503)
|
|
assert.equal(res.body.code, 'unterlagen_fehlen')
|
|
assert.equal(res.body.error, 'Die Vertragsunterlagen sind gerade nicht abrufbar. Bitte versuch es später noch einmal oder schreib uns im Chat.')
|
|
assert.deepEqual(lauf.ablauf, ['unterlagen'], 'weder Bestellpruefung noch Abo gelesen')
|
|
keineSpuren()
|
|
} finally {
|
|
lauf.ende()
|
|
}
|
|
})
|
|
|
|
// ------------------------------------------------------------------------------------------ 6 Bestellpruefung ---
|
|
test('Bestellpruefung nicht leer: 409 bestellung_offen mit den offenen Punkten, Haken ohne abo', async () => {
|
|
const offen = [{ schritt: 'zeiten', text: 'Zeiten bestätigen' }, { schritt: 'website', text: 'Impressum eintragen' }]
|
|
const lauf = ablaufAufzeichnen({ offen })
|
|
try {
|
|
const res = await checkout('termine', bodyT())
|
|
assert.equal(res.status, 409)
|
|
assert.equal(res.body.code, 'bestellung_offen')
|
|
assert.deepEqual(res.body.offen, offen)
|
|
assert.equal(typeof res.body.error, 'string')
|
|
assert.equal(lauf.args.length, 1)
|
|
const [arg] = lauf.args
|
|
assert.equal('abo' in arg, false, 'der Checkout uebergibt kein abo')
|
|
assert.equal(arg.customerId, 'c1')
|
|
assert.equal(arg.projectId, 'p1')
|
|
assert.equal(arg.feature, T, 'der Register-Eintrag')
|
|
assert.deepEqual(lauf.ablauf, ['unterlagen', 'pruefung'], 'die Abo-Stufe wird nicht gelesen')
|
|
keineSpuren()
|
|
} finally {
|
|
lauf.ende()
|
|
}
|
|
})
|
|
|
|
test('Bestellpruefung: kaputter Haken (keine Liste) lehnt ab, 500 ohne Rohtext (fail-closed)', async () => {
|
|
T.bestellpruefung = async () => 'sieht gut aus'
|
|
const log = stumm()
|
|
try {
|
|
const res = await checkout('termine', bodyT())
|
|
assert.equal(res.status, 500)
|
|
assert.doesNotMatch(JSON.stringify(res.body), /sieht gut aus|Liste/)
|
|
keineSpuren()
|
|
} finally {
|
|
log.mock.restore()
|
|
}
|
|
})
|
|
|
|
test('Standard-Haken liest den Entwurf nur, wenn es ein Dokument gibt, und ruft die Bestellpruefung ohne Abo', async () => {
|
|
const haken = reg.featureHaken('termine')
|
|
const arg = { customerId: 'c1', projectId: 'p1', feature: T }
|
|
// ohne Dokument: keine Abfrage von termineConfig, das echte Gate meldet nur die Branche offen (kein Abo-Punkt)
|
|
aw.failNext({ collection: 'termineConfig', status: 500 })
|
|
const ohneDoc = await haken.bestellpruefung(arg)
|
|
assert.deepEqual(ohneDoc.map((p) => p.schritt), ['branche'])
|
|
// mit Dokument: der Entwurf wird geladen (der eingeplante Lesefehler trifft)
|
|
aw.seed('portalFeatures', { $id: 'f_p1_termine', customerId: 'c1', projectId: 'p1', featureKey: 'termine', state: 'off', enabled: false, siteKey: 'abcdefghijklmnop' })
|
|
const log = stumm()
|
|
try {
|
|
await assert.rejects(haken.bestellpruefung(arg), (err) => err.status === 500)
|
|
} finally {
|
|
log.mock.restore()
|
|
}
|
|
// Dokument ohne gespeicherten Entwurf: wie ohne Dokument
|
|
assert.deepEqual((await haken.bestellpruefung(arg)).map((p) => p.schritt), ['branche'], 'danach wieder normal')
|
|
// bestellbarer Entwurf: leer; offener Fragebogen: Punkte der Schritte, nie der Abo-Punkt (mitAbo: false)
|
|
aw.seed('termineConfig', entwurfDoc('f_p1_termine', { customerId: 'c1', projectId: 'p1' }))
|
|
assert.deepEqual(await haken.bestellpruefung(arg), [])
|
|
const offen = bestellbarerStand()
|
|
offen.fragebogen.bestaetigt = []
|
|
aw.seed('termineConfig', entwurfDoc('f_p1_termine', { customerId: 'c1', projectId: 'p1', stand: offen }))
|
|
const punkte = await haken.bestellpruefung(arg)
|
|
assert.equal(punkte.length, 8)
|
|
assert.ok(punkte.every((p) => p.schritt !== 'bestellen'))
|
|
})
|
|
|
|
// ------------------------------------------------------------------------------------------ 7 Abo ---
|
|
const SCHRITTE_OHNE_BRANCHE = ['wer', 'leistungen', 'zeiten', 'regeln', 'gastfelder', 'benachrichtigung', 'website']
|
|
const ABO_TEXT = {
|
|
kein_abo: 'Für den Terminplaner brauchst du ein laufendes Wartungs-Abo.',
|
|
gekuendigt: 'Dein Wartungs-Abo ist gekündigt. Für den Terminplaner brauchst du ein laufendes Abo.',
|
|
nicht_aktiv:
|
|
'Dein Wartungs-Abo ist gerade nicht aktiv (zum Beispiel wegen einer offenen Zahlung). Sobald es wieder aktiv ist, kannst du den Terminplaner bestellen.',
|
|
}
|
|
|
|
function aboAbgelehnt(res, grund) {
|
|
assert.equal(res.status, 409, grund)
|
|
assert.equal(res.body.code, grund)
|
|
assert.equal(res.body.grund, grund, 'Grund ohne Praefix')
|
|
assert.equal(res.body.error, ABO_TEXT[grund])
|
|
keineSpuren()
|
|
}
|
|
|
|
test('Abo-Gruende: kein Abo (c3, c2 mit Hosting), Kuendigung, offene Zahlung', async () => {
|
|
T.bestellpruefung = async () => []
|
|
// c3 hat gar kein Abo, c2 nur Hosting
|
|
aboAbgelehnt(await checkout('termine', { projectId: 'p3', zustimmung: zustimmung() }, customerCookie('c3')), 'kein_abo')
|
|
aboAbgelehnt(await checkout('termine', { projectId: 'p5', zustimmung: zustimmung() }, customerCookie('c2')), 'kein_abo')
|
|
// c1 mit Wartungs-Abo, aber Hosting-Abo allein ist auch kein Wartungs-Abo
|
|
aw.seed('subscriptions', subscriptionDoc('sub_h', { ...wartungDoc, productKey: 'hosting' }))
|
|
aboAbgelehnt(await checkout('termine', bodyT()), 'kein_abo')
|
|
|
|
wartung({ cancelAtPeriodEnd: true })
|
|
aboAbgelehnt(await checkout('termine', bodyT()), 'gekuendigt')
|
|
wartung({ cancelAt: new Date(Date.now() + 30 * DAY_MS).toISOString() })
|
|
aboAbgelehnt(await checkout('termine', bodyT()), 'gekuendigt')
|
|
|
|
wartung({ status: 'past_due' })
|
|
aboAbgelehnt(await checkout('termine', bodyT()), 'nicht_aktiv')
|
|
wartung({ status: 'past_due', cancelAtPeriodEnd: true })
|
|
aboAbgelehnt(await checkout('termine', bodyT()), 'gekuendigt')
|
|
|
|
// laufendes, ungekuendigtes Abo: die Abo-Stufe lehnt nicht ab
|
|
wartung({ status: 'active' })
|
|
const ok = await checkout('termine', bodyT())
|
|
assert.notEqual(ok.body?.code, 'kein_abo')
|
|
assert.notEqual(ok.body?.grund, 'nicht_aktiv')
|
|
})
|
|
|
|
test('Wechsel-Regel (E1): Spiegel Wartung active, aber juengstes wechsel-Ereignis widerrufen => 409 kein_abo ohne Spur; ein neueres erledigtes hebt es auf', async () => {
|
|
T.bestellpruefung = async () => []
|
|
const wechsel = (nr, status) =>
|
|
aw.seed('contractEvents', {
|
|
$id: `ev_w${nr}`,
|
|
$createdAt: new Date(Date.UTC(2026, 8, 1, 0, 0, nr)).toISOString(),
|
|
customerId: 'c1',
|
|
subscriptionId: 'sub_h',
|
|
type: 'wechsel',
|
|
productKey: 'wartung',
|
|
details: '{}',
|
|
status,
|
|
})
|
|
wechsel(1, 'widerrufen')
|
|
aboAbgelehnt(await checkout('termine', bodyT()), 'kein_abo')
|
|
assert.equal(aw.docs('portalFeatures').size, 0, 'kein Dokument entstanden')
|
|
|
|
wechsel(2, 'erledigt')
|
|
const ok = await checkout('termine', bodyT())
|
|
assert.notEqual(ok.body?.code, 'kein_abo')
|
|
assert.notEqual(ok.body?.grund, 'nicht_aktiv')
|
|
})
|
|
|
|
test('Abo-Lesefehler: 500 ohne Rohtext, fail-closed ohne Spur', async () => {
|
|
T.bestellpruefung = async () => []
|
|
aw.failNext({ collection: 'subscriptions', status: 500, message: 'GEHEIMER-ROHTEXT interner Fehler' })
|
|
const log = stumm()
|
|
try {
|
|
const res = await checkout('termine', bodyT())
|
|
assert.equal(res.status, 500)
|
|
assert.doesNotMatch(JSON.stringify(res.body), /GEHEIMER-ROHTEXT/)
|
|
assert.equal(typeof res.body.error, 'string')
|
|
keineSpuren()
|
|
} finally {
|
|
log.mock.restore()
|
|
}
|
|
})
|
|
|
|
test('Abo-Stufe mit dem echten Gate: ohne Abo und bestellbarem Entwurf 409 kein_abo, mit offenem Fragebogen 409 bestellung_offen (ohne bestellen)', async () => {
|
|
const c3 = customerCookie('c3')
|
|
const entwurf = (stand) => {
|
|
aw.seed('portalFeatures', { $id: 'f_p3_termine', customerId: 'c3', projectId: 'p3', featureKey: 'termine', state: 'off', enabled: false, siteKey: 'abcdefghijklmnop' })
|
|
aw.seed('termineConfig', entwurfDoc('f_p3_termine', { customerId: 'c3', projectId: 'p3', stand }))
|
|
}
|
|
// Standard-Haken, Kunde ohne Abo, bestellbarer Entwurf: das Gate ist leer, die Abo-Stufe lehnt ab
|
|
entwurf(bestellbarerStand())
|
|
const featuresVorher = aw.docs('portalFeatures').size
|
|
const abgelehnt = await checkout('termine', { projectId: 'p3', zustimmung: zustimmung() }, c3)
|
|
assert.equal(abgelehnt.status, 409)
|
|
assert.equal(abgelehnt.body.code, 'kein_abo')
|
|
assert.notEqual(abgelehnt.body.code, 'bestellung_offen')
|
|
assert.equal(abgelehnt.body.error, ABO_TEXT.kein_abo)
|
|
assert.deepEqual(spuren(), { sessions: 0, consents: 0, features: featuresVorher }, 'keine Sitzung, kein Nachweis, kein neues Dokument')
|
|
// Kunde ohne Abo, Fragebogen offen: bestellung_offen, und die Liste enthaelt keinen Abo-Punkt (mitAbo: false)
|
|
const offen = bestellbarerStand()
|
|
offen.fragebogen.bestaetigt = ['branche']
|
|
entwurf(offen)
|
|
const res = await checkout('termine', { projectId: 'p3', zustimmung: zustimmung() }, c3)
|
|
assert.equal(res.status, 409)
|
|
assert.equal(res.body.code, 'bestellung_offen')
|
|
assert.equal(res.body.offen.length, 7)
|
|
assert.ok(res.body.offen.every((p) => p.schritt !== 'bestellen'), 'kein Eintrag bestellen')
|
|
assert.deepEqual(res.body.offen.map((p) => p.schritt), SCHRITTE_OHNE_BRANCHE)
|
|
assert.deepEqual(spuren(), { sessions: 0, consents: 0, features: featuresVorher })
|
|
// ohne Entwurf (gar kein Dokument): ebenfalls bestellung_offen mit der Branche, nie ein Abo-Punkt
|
|
for (const col of ['portalFeatures', 'termineConfig']) for (const id of aw.docs(col).keys()) await deleteDocument(col, id)
|
|
const leer = await checkout('termine', { projectId: 'p3', zustimmung: zustimmung() }, c3)
|
|
assert.equal(leer.status, 409)
|
|
assert.equal(leer.body.code, 'bestellung_offen')
|
|
assert.deepEqual(leer.body.offen.map((p) => p.schritt), ['branche'])
|
|
// ueberschriebener Haken mit leerer Liste: weiter die Abo-Stufe
|
|
T.bestellpruefung = async () => []
|
|
aboAbgelehnt(await checkout('termine', { projectId: 'p3', zustimmung: zustimmung() }, c3), 'kein_abo')
|
|
})
|
|
|
|
test('Echtes Gate bis zur Stripe-Sitzung: Kunde mit Wartungs-Abo und bestellbarem Entwurf kommt durch, mit offenem Punkt nicht', async () => {
|
|
const entwurf = (stand) => {
|
|
aw.seed('portalFeatures', { $id: 'f_p1_termine', customerId: 'c1', projectId: 'p1', featureKey: 'termine', state: 'off', enabled: false, siteKey: 'abcdefghijklmnop' })
|
|
aw.seed('termineConfig', entwurfDoc('f_p1_termine', { customerId: 'c1', projectId: 'p1', stand }))
|
|
}
|
|
// ein offener Punkt (Impressum fehlt): 409 bestellung_offen, keine Sitzung, kein Nachweis
|
|
const luecke = bestellbarerStand()
|
|
luecke.einstellung.betrieb.impressumUrl = null
|
|
entwurf(luecke)
|
|
const offen = await checkout('termine', bodyT())
|
|
assert.equal(offen.status, 409)
|
|
assert.equal(offen.body.code, 'bestellung_offen')
|
|
assert.deepEqual(offen.body.offen.map((p) => p.schritt), ['website'])
|
|
assert.deepEqual({ sessions: sitzungen().length, consents: nachweise().length }, { sessions: 0, consents: 0 })
|
|
// bestellbarer Entwurf: Gate leer, Abo laeuft, die Stripe-Sitzung entsteht
|
|
entwurf(bestellbarerStand())
|
|
const ok = await checkout('termine', bodyT())
|
|
assert.equal(ok.status, 200, JSON.stringify(ok.body))
|
|
assert.match(ok.body.url, /^https?:\/\//)
|
|
assert.equal(sitzungen().length, 1)
|
|
assert.equal(nachweise().length, 1)
|
|
assert.equal(termineDoc().state, 'ordered')
|
|
})
|
|
|
|
test('aboMeldung: Wortlaute aus dem Register, Akkusativ aus mails.formen.akk, unbekannter Grund wie unbekannt', () => {
|
|
for (const grund of ['kein_abo', 'gekuendigt', 'nicht_aktiv']) assert.equal(order.aboMeldung(T, grund), ABO_TEXT[grund], grund)
|
|
assert.equal(order.aboMeldung(T, 'unbekannt'), 'Das Abo konnte gerade nicht geprüft werden.')
|
|
assert.equal(order.aboMeldung(T, 'irgendwas'), 'Das Abo konnte gerade nicht geprüft werden.')
|
|
assert.equal(order.aboMeldung(T, undefined), 'Das Abo konnte gerade nicht geprüft werden.')
|
|
// die Funktion steht im Satz: Akkusativ der jeweiligen Funktion
|
|
assert.equal(order.aboMeldung(config.features.news, 'kein_abo'), 'Für die News-Pinnwand brauchst du ein laufendes Wartungs-Abo.')
|
|
assert.match(order.aboMeldung(config.features.news, 'nicht_aktiv'), /kannst du die News-Pinnwand bestellen\.$/)
|
|
assert.match(order.aboMeldung(T, 'nicht_aktiv'), /bestellen\.$/)
|
|
})
|
|
|
|
// ------------------------------------------------------------------------------------------ Reihenfolge ---
|
|
test('Reihenfolge der Stufen: jede Stufe steht vor der naechsten und laesst nichts zurueck', async () => {
|
|
const lauf = ablaufAufzeichnen({ offen: [{ schritt: 'zeiten', text: 'Zeiten bestätigen' }] })
|
|
try {
|
|
const nichtsGelesen = () => {
|
|
assert.deepEqual(lauf.ablauf, [], 'kein Haken, kein Abo gelesen')
|
|
keineSpuren()
|
|
}
|
|
config.termine.mode = 'off'
|
|
// 1 vor 2: Kundenansicht schlaegt unbekannten Schluessel und alles andere
|
|
assert.equal((await checkout('nope', {}, kundenansichtCookie())).status, 403)
|
|
// 2 vor 3: unbekannter Schluessel schlaegt fehlende Zustimmung
|
|
assert.equal((await checkout('nope', { projectId: 'p5' })).status, 404)
|
|
// 3 vor 4: fehlende Zustimmung schlaegt fremdes Projekt und fehlendes Projekt
|
|
assert.equal((await checkout('termine', { projectId: 'p5' })).status, 400)
|
|
assert.equal((await checkout('termine', {})).status, 400)
|
|
// 4 vor 5: Projekt schlaegt den Schalter (mode off)
|
|
assert.equal((await checkout('termine', { zustimmung: zustimmung() })).status, 400)
|
|
assert.equal((await checkout('termine', bodyT({ projectId: 'p5' }))).status, 403)
|
|
nichtsGelesen()
|
|
// 5: Schalter vor den Unterlagen, der Bestellpruefung und dem Abo
|
|
const aus = await checkout('termine', bodyT())
|
|
assert.equal(aus.body.code, 'nicht_verfuegbar')
|
|
nichtsGelesen()
|
|
|
|
config.termine.mode = 'pilot'
|
|
// 5b vor 6 und 7
|
|
lauf.ablauf.length = 0
|
|
const alt = T.unterlagenBereit
|
|
T.unterlagenBereit = async () => {
|
|
lauf.ablauf.push('unterlagen')
|
|
return false
|
|
}
|
|
const unterlagen = await checkout('termine', bodyT())
|
|
assert.equal(unterlagen.body.code, 'unterlagen_fehlen')
|
|
assert.deepEqual(lauf.ablauf, ['unterlagen'])
|
|
T.unterlagenBereit = alt
|
|
|
|
// 6 vor 7: offene Punkte, die Abo-Stufe (Zaehler) wird nicht angefasst - auch ohne Abo (c3)
|
|
lauf.ablauf.length = 0
|
|
const offen = await checkout('termine', { projectId: 'p3', zustimmung: zustimmung() }, customerCookie('c3'))
|
|
assert.equal(offen.status, 409)
|
|
assert.equal(offen.body.code, 'bestellung_offen')
|
|
assert.equal(offen.body.offen[0].schritt, 'zeiten')
|
|
assert.deepEqual(lauf.ablauf, ['unterlagen', 'pruefung'])
|
|
assert.equal(lauf.ablauf.filter((x) => x === 'abo').length, 0, 'Zaehler 0')
|
|
keineSpuren()
|
|
|
|
// danach leere Pruefung: die Abo-Stufe wird genau einmal gelesen
|
|
lauf.ablauf.length = 0
|
|
T.bestellpruefung = async (arg) => {
|
|
lauf.ablauf.push('pruefung')
|
|
assert.equal('abo' in arg, false)
|
|
return []
|
|
}
|
|
const weiter = await checkout('termine', bodyT())
|
|
assert.deepEqual(lauf.ablauf, ['unterlagen', 'pruefung', 'abo'], 'Zaehler 1')
|
|
assert.notEqual(weiter.body?.code, 'bestellung_offen')
|
|
assert.notEqual(weiter.body?.code, 'kein_abo')
|
|
} finally {
|
|
lauf.ende()
|
|
}
|
|
})
|
|
|
|
test('Abo-Memo: je Anfrage ein aboMemo(), News fragt das Abo nie', async () => {
|
|
const lauf = ablaufAufzeichnen()
|
|
try {
|
|
T.bestellpruefung = async () => []
|
|
await checkout('termine', bodyT())
|
|
assert.equal(lauf.ablauf.filter((x) => x === 'abo').length, 1)
|
|
lauf.ablauf.length = 0
|
|
// News (requiresWartungsAbo false) liest weder Abo noch Haken, c3 bestellt weiter
|
|
const news = await checkout('news', { projectId: 'p3', consent: true }, customerCookie('c3'))
|
|
assert.equal(news.status, 200)
|
|
assert.deepEqual(lauf.ablauf, [])
|
|
} finally {
|
|
lauf.ende()
|
|
}
|
|
})
|
|
|
|
// ------------------------------------------------------------------------------------------ Sperre, Zustand, Nachweis (B7) ---
|
|
// Ab hier der Rest der Route: die Vorpruefungen sind bestanden (Fragebogen-Pruefung per Haken uebersprungen; das echte Gate pruefen die Tests der Abschnitte 6 und 7).
|
|
const bereit = () => {
|
|
T.bestellpruefung = async () => []
|
|
}
|
|
const featureDoc = (state, extra = {}) =>
|
|
aw.seed('portalFeatures', { $id: 'f_p1_termine', customerId: 'c1', projectId: 'p1', featureKey: 'termine', state, enabled: state === 'active', siteKey: 'abcdefghijklmnop', ...extra })
|
|
const sitzungen = () => [...st.checkoutSessions.values()]
|
|
const nachweise = () => [...aw.docs('featureConsents').values()]
|
|
const termineDoc = () => aw.docs('portalFeatures').get('f_p1_termine')
|
|
const sha256 = (text) => createHash('sha256').update(text).digest('hex')
|
|
const belehrungText = async (key) => (await reg.featureHaken(key).belehrung?.()) ?? null
|
|
const erwarteteTexte = async (key) => consents.nachweisTexte(reg.featureConfig(key), { belehrung: await belehrungText(key) })
|
|
const hakenTexte = (feature) => feature.consent.items.filter((item) => item.haken).map((item) => item.text).join('\n\n')
|
|
|
|
test('Nachweis steht vor der Weiterleitung: Serverfassungen und -texte in Reihenfolge, Hash, ein Zeitpunkt', async () => {
|
|
bereit()
|
|
const vor = new Date().toISOString()
|
|
const res = await checkout('termine', bodyT())
|
|
const nach = new Date().toISOString()
|
|
assert.equal(res.status, 200, JSON.stringify(res.body))
|
|
assert.deepEqual(Object.keys(res.body), ['url'])
|
|
assert.equal(sitzungen().length, 1)
|
|
const [session] = sitzungen()
|
|
assert.equal(res.body.url, session.url)
|
|
|
|
assert.equal(nachweise().length, 1, 'genau ein Nachweis')
|
|
const [n] = nachweise()
|
|
assert.equal(n.stripeSessionId, session.id)
|
|
assert.deepEqual([n.customerId, n.projectId, n.featureKey], ['c1', 'p1', 'termine'])
|
|
assert.deepEqual(JSON.parse(n.texte), await erwarteteTexte('termine'), 'Texte in Anzeigereihenfolge')
|
|
assert.deepEqual(JSON.parse(n.fassungen), consents.zustimmungsfassungen(T))
|
|
assert.equal(n.textHash, sha256(n.texte), 'SHA-256 ueber die gespeicherten Texte')
|
|
assert.equal(n.textHash, consents.texteHash(JSON.parse(n.texte)))
|
|
assert.ok(n.ip, 'ip')
|
|
assert.ok(n.userAgent, 'userAgent')
|
|
assert.equal(n.konto, 'Kunde Eins <k@example.invalid>')
|
|
|
|
// ein Zeitpunkt: Nachweis, Stripe-Metadaten und Dokument tragen dieselbe Zeit des Klicks (Eingang der Anfrage)
|
|
const doc = termineDoc()
|
|
assert.equal(doc.state, 'ordered')
|
|
assert.equal(doc.stripeSessionId, session.id)
|
|
assert.equal(n.acceptedAt, session.metadata.consentAt)
|
|
assert.equal(n.acceptedAt, doc.consentAt)
|
|
assert.ok(vor <= n.acceptedAt && n.acceptedAt <= nach, `${vor} <= ${n.acceptedAt} <= ${nach}`)
|
|
assert.match(n.acceptedAt, /Z$/, 'UTC-Zeit mit Z')
|
|
assert.equal(doc.consentText, hakenTexte(T).slice(0, 1000), 'consentText aus den Haken-Texten')
|
|
// C1: IP und Browser stehen nur im Nachweis (featureConsents), nicht mehr am Funktionsdokument
|
|
assert.equal(doc.consentIp, '')
|
|
assert.equal(doc.consentUserAgent, '')
|
|
})
|
|
|
|
test('Belehrung im Nachweis: Eintrag belehrung (haken false) direkt vor dem Knopf, News hat keinen', async () => {
|
|
bereit()
|
|
// Task F7: der Haken liefert die Widerrufsbelehrung der Funktionen samt Muster-Widerrufsformular (Task F3)
|
|
const belehrung = widerrufsbelehrungFunktion()
|
|
assert.equal(await reg.featureHaken('termine').belehrung(), belehrung)
|
|
assert.match(belehrung, /^Widerrufsbelehrung\n\nWiderrufsrecht\n\nSie haben das Recht/)
|
|
assert.equal(reg.featureHaken('news').belehrung, undefined)
|
|
await checkout('termine', bodyT())
|
|
const texte = JSON.parse(nachweise()[0].texte)
|
|
assert.deepEqual(texte.slice(-2), [
|
|
{ id: 'belehrung', text: belehrung, haken: false },
|
|
{ id: 'knopf', text: T.consent.knopf, haken: false },
|
|
])
|
|
assert.equal(texte.length, 4 + 2, 'vier Haken, Belehrung, Knopf')
|
|
assert.deepEqual(texte.map((x) => x.id), [...T.consent.items.map((x) => x.id), 'belehrung', 'knopf'])
|
|
|
|
const news = await checkout('news', { projectId: 'p3', consent: true }, customerCookie('c3'))
|
|
assert.equal(news.status, 200)
|
|
const newsNachweis = nachweise().find((d) => d.featureKey === 'news')
|
|
assert.deepEqual(JSON.parse(newsNachweis.texte).map((x) => x.id), [...config.features.news.consent.items.map((x) => x.id), 'knopf'])
|
|
})
|
|
|
|
test('Belehrungs-Haken ueberschreibbar: der gelieferte Text steht im Nachweis, null laesst den Eintrag weg', async () => {
|
|
bereit()
|
|
T.belehrung = async () => 'Eigene Belehrung.'
|
|
await checkout('termine', bodyT())
|
|
assert.deepEqual(JSON.parse(nachweise()[0].texte).at(-2), { id: 'belehrung', text: 'Eigene Belehrung.', haken: false })
|
|
await deleteDocument('featureConsents', nachweise()[0].$id)
|
|
await deleteDocument('portalFeatures', 'f_p1_termine')
|
|
st.checkoutSessions.clear()
|
|
T.belehrung = async () => null
|
|
try {
|
|
assert.equal((await checkout('termine', bodyT())).status, 200)
|
|
assert.equal(JSON.parse(nachweise()[0].texte).some((x) => x.id === 'belehrung'), false)
|
|
} finally {
|
|
delete T.belehrung
|
|
}
|
|
})
|
|
|
|
test('Client-Texte zaehlen nicht: gespeichert werden die Registertexte und -fassungen', async () => {
|
|
bereit()
|
|
const res = await checkout('termine', bodyT({ texte: ['manipuliert'], fassungen: { zustimmung: 'x' }, konto: 'Fremd <f@x.de>', acceptedAt: '2000-01-01T00:00:00Z' }))
|
|
assert.equal(res.status, 200)
|
|
const [n] = nachweise()
|
|
assert.deepEqual(JSON.parse(n.texte), await erwarteteTexte('termine'))
|
|
assert.deepEqual(JSON.parse(n.fassungen), consents.zustimmungsfassungen(T))
|
|
assert.equal(n.konto, 'Kunde Eins <k@example.invalid>')
|
|
assert.ok(n.acceptedAt > '2020')
|
|
assert.doesNotMatch(JSON.stringify(nachweise()), /manipuliert/)
|
|
})
|
|
|
|
test('Fassungswechsel: alter Body 409, neuer Body 200 mit neuer Fassung im Nachweis', async () => {
|
|
bereit()
|
|
const alt = T.consent.version
|
|
try {
|
|
T.consent.version = 'ENTWURF-2099-01-01'
|
|
const veraltet = await checkout('termine', bodyT({ zustimmung: { version: alt, haken: pflichtHaken() } }))
|
|
assert.equal(veraltet.status, 409)
|
|
assert.equal(veraltet.body.code, 'fassung_veraltet')
|
|
keineSpuren()
|
|
const neu = await checkout('termine', bodyT({ zustimmung: { version: 'ENTWURF-2099-01-01', haken: pflichtHaken() } }))
|
|
assert.equal(neu.status, 200)
|
|
assert.equal(JSON.parse(nachweise()[0].fassungen).zustimmung, 'ENTWURF-2099-01-01')
|
|
} finally {
|
|
T.consent.version = alt
|
|
}
|
|
})
|
|
|
|
test('ohne Nachweis keine URL: scheitert das Speichern, gibt es kein ordered, keine Weiterleitung', async () => {
|
|
bereit()
|
|
const log = stumm()
|
|
try {
|
|
aw.failNext({ collection: 'featureConsents', method: 'POST', status: 500, message: 'GEHEIMER-ROHTEXT interner Fehler' })
|
|
const res = await checkout('termine', bodyT())
|
|
assert.equal(res.status, 500)
|
|
assert.equal(res.body.url, undefined)
|
|
assert.doesNotMatch(JSON.stringify(res.body), /GEHEIMER-ROHTEXT|https?:/)
|
|
assert.match(res.body.error, /Bestellung konnte gerade nicht gestartet werden/)
|
|
assert.equal(nachweise().length, 0)
|
|
const doc = termineDoc()
|
|
assert.ok(!doc || doc.state === 'off', 'nicht ordered')
|
|
assert.ok(!doc?.orderedAt, 'orderedAt leer')
|
|
// die Stripe-Sitzung ist angelegt, aber nie an den Browser gegangen; sie laeuft nach 24 h ab
|
|
assert.equal(sitzungen().length, 1)
|
|
// ein erneuter Klick geht normal durch: neue Sitzung, ein Nachweis zu ihr
|
|
const noch = await checkout('termine', bodyT())
|
|
assert.equal(noch.status, 200)
|
|
assert.equal(sitzungen().length, 2)
|
|
assert.equal(nachweise().length, 1)
|
|
assert.equal(nachweise()[0].stripeSessionId, sitzungen()[1].id)
|
|
} finally {
|
|
log.mock.restore()
|
|
}
|
|
})
|
|
|
|
test('Nachweis nicht speicherbar (zu lange Texte): 500 mit allgemeinem Text, kein Rohtext, kein ordered', async () => {
|
|
bereit()
|
|
const alt = T.consent.items[0].text
|
|
const log = stumm()
|
|
try {
|
|
T.consent.items[0].text = 'x'.repeat(30000)
|
|
const res = await checkout('termine', bodyT())
|
|
assert.equal(res.status, 500)
|
|
assert.doesNotMatch(JSON.stringify(res.body), /Zustimmungsnachweis|zu lang/)
|
|
assert.equal(res.body.url, undefined)
|
|
assert.ok(!termineDoc() || termineDoc().state === 'off')
|
|
} finally {
|
|
T.consent.items[0].text = alt
|
|
log.mock.restore()
|
|
}
|
|
})
|
|
|
|
test('Datenbankfehler beim markOrdered: 500 ohne Rohtext, der Nachweis steht schon, das Dokument nicht ordered', async () => {
|
|
bereit()
|
|
const log = stumm()
|
|
try {
|
|
aw.failNext({ collection: 'portalFeatures', method: 'PATCH', status: 400, message: 'Invalid document structure: Attribute "stripeSessionId" has invalid type.' })
|
|
const res = await checkout('termine', bodyT())
|
|
assert.equal(res.status, 500, JSON.stringify(res.body))
|
|
assert.match(res.body.error, /Bestellung konnte gerade nicht gestartet werden/)
|
|
assert.doesNotMatch(JSON.stringify(res.body), /Invalid document structure|stripeSessionId|https?:/)
|
|
assert.equal(nachweise().length, 1, 'Nachweis kommt vor markOrdered')
|
|
assert.equal(termineDoc().state, 'off')
|
|
} finally {
|
|
log.mock.restore()
|
|
}
|
|
})
|
|
|
|
test('Wiederverwendung: zweiter Aufruf bei offener Sitzung gibt dieselbe URL, ohne zweite Sitzung und zweiten Nachweis', async () => {
|
|
bereit()
|
|
const erste = await checkout('termine', bodyT())
|
|
const zweite = await checkout('termine', bodyT())
|
|
assert.equal(zweite.status, 200)
|
|
assert.equal(zweite.body.url, erste.body.url)
|
|
assert.equal(sitzungen().length, 1)
|
|
assert.equal(nachweise().length, 1)
|
|
// die Wiederverwendung fasst das Dokument nicht an (orderedAt bleibt der Zeitpunkt der Bestellung)
|
|
aw.seed('portalFeatures', { ...termineDoc(), orderedAt: '2026-01-01T00:00:00.000Z' })
|
|
await checkout('termine', bodyT())
|
|
assert.equal(termineDoc().orderedAt, '2026-01-01T00:00:00.000Z')
|
|
assert.equal(termineDoc().state, 'ordered')
|
|
})
|
|
|
|
test('Wiederverwendung ohne Nachweis: der Nachweis wird nachgetragen, bevor die URL zurueckkommt', async () => {
|
|
bereit()
|
|
const erste = await checkout('termine', bodyT())
|
|
await deleteDocument('featureConsents', nachweise()[0].$id)
|
|
assert.equal(nachweise().length, 0)
|
|
const vor = new Date().toISOString()
|
|
const zweite = await checkout('termine', bodyT())
|
|
assert.equal(zweite.status, 200)
|
|
assert.equal(zweite.body.url, erste.body.url)
|
|
assert.equal(sitzungen().length, 1, 'keine zweite Stripe-Sitzung')
|
|
assert.equal(nachweise().length, 1)
|
|
assert.equal(nachweise()[0].stripeSessionId, sitzungen()[0].id)
|
|
assert.ok(nachweise()[0].acceptedAt >= vor, 'Zeit des neuen Klicks')
|
|
assert.deepEqual(JSON.parse(nachweise()[0].texte), await erwarteteTexte('termine'))
|
|
|
|
// scheitert das Nachtragen, kommt keine URL zurueck
|
|
await deleteDocument('featureConsents', nachweise()[0].$id)
|
|
const log = stumm()
|
|
try {
|
|
aw.failNext({ collection: 'featureConsents', method: 'POST', status: 500 })
|
|
const kaputt = await checkout('termine', bodyT())
|
|
assert.equal(kaputt.status, 500)
|
|
assert.equal(kaputt.body.url, undefined)
|
|
assert.equal(nachweise().length, 0)
|
|
} finally {
|
|
log.mock.restore()
|
|
}
|
|
})
|
|
|
|
test('abgelaufene Sitzung: neue Sitzung und neuer Nachweis (zwei Dokumente, zwei Sitzungs-IDs)', async () => {
|
|
bereit()
|
|
const erste = await checkout('termine', bodyT())
|
|
sitzungen()[0].status = 'expired'
|
|
const zweite = await checkout('termine', bodyT())
|
|
assert.equal(zweite.status, 200)
|
|
assert.notEqual(zweite.body.url, erste.body.url)
|
|
assert.equal(sitzungen().length, 2)
|
|
assert.equal(nachweise().length, 2)
|
|
assert.equal(new Set(nachweise().map((n) => n.stripeSessionId)).size, 2)
|
|
assert.deepEqual(nachweise().map((n) => n.stripeSessionId).sort(), sitzungen().map((s) => s.id).sort())
|
|
assert.equal(termineDoc().stripeSessionId, sitzungen()[1].id)
|
|
assert.equal(termineDoc().consentAt, sitzungen()[1].metadata.consentAt)
|
|
})
|
|
|
|
test('parallele Klicks: eine Sitzung, ein Nachweis, beide 200 mit gleicher URL', async () => {
|
|
bereit()
|
|
const [a, b] = await Promise.all([checkout('termine', bodyT()), checkout('termine', bodyT())])
|
|
assert.deepEqual([a.status, b.status], [200, 200])
|
|
assert.equal(a.body.url, b.body.url)
|
|
assert.equal(sitzungen().length, 1)
|
|
assert.equal(nachweise().length, 1)
|
|
})
|
|
|
|
// ------------------------------------------------------------------------------------------ Zustaende ---
|
|
test('Zustaende: setup und active sind 409 orderedConflictMessage, disabled 409 disabledMessage, nirgends eine neue Sitzung', async () => {
|
|
bereit()
|
|
for (const [state, text] of [
|
|
['setup', T.orderedConflictMessage],
|
|
['active', T.orderedConflictMessage],
|
|
['disabled', T.disabledMessage],
|
|
]) {
|
|
featureDoc(state)
|
|
const vorher = spuren()
|
|
const res = await checkout('termine', bodyT())
|
|
assert.equal(res.status, 409, state)
|
|
assert.equal(res.body.error, text, state)
|
|
assert.equal(res.body.url, undefined, state)
|
|
assert.deepEqual(spuren(), vorher, `${state}: nichts angelegt`)
|
|
assert.equal(termineDoc().state, state, `${state}: unveraendert`)
|
|
}
|
|
// News: Meldungen aus dem Register der News
|
|
const news = config.features.news
|
|
aw.seed('portalFeatures', { $id: 'f_p3_news', customerId: 'c3', projectId: 'p3', featureKey: 'news', state: 'active', enabled: true, siteKey: 'abcdefghijklmnop' })
|
|
const nCookie = customerCookie('c3')
|
|
assert.equal((await checkout('news', { projectId: 'p3', consent: true }, nCookie)).body.error, news.orderedConflictMessage)
|
|
aw.seed('portalFeatures', { $id: 'f_p3_news', customerId: 'c3', projectId: 'p3', featureKey: 'news', state: 'disabled', enabled: false, siteKey: 'abcdefghijklmnop' })
|
|
assert.equal((await checkout('news', { projectId: 'p3', consent: true }, nCookie)).body.error, news.disabledMessage)
|
|
assert.equal(sitzungen().length, 0)
|
|
})
|
|
|
|
test('Zustand complete: 409, keine neue Sitzung, kein neuer Nachweis', async () => {
|
|
bereit()
|
|
await checkout('termine', bodyT())
|
|
sitzungen()[0].status = 'complete'
|
|
const res = await checkout('termine', bodyT())
|
|
assert.equal(res.status, 409)
|
|
assert.equal(res.body.error, T.orderedConflictMessage)
|
|
assert.equal(sitzungen().length, 1)
|
|
assert.equal(nachweise().length, 1)
|
|
// auch "Zahlung fortsetzen" bekommt keine URL
|
|
const fort = await checkout('termine', { projectId: 'p1', fortsetzen: true })
|
|
assert.equal(fort.status, 409)
|
|
assert.equal(fort.body.url, undefined)
|
|
})
|
|
|
|
test('Stripe nicht erreichbar: frische Bestellung (< 24 h) ist 409, aeltere bekommt eine neue Sitzung samt Nachweis', async () => {
|
|
bereit()
|
|
const log = mock.method(console, 'warn', () => {})
|
|
try {
|
|
featureDoc('ordered', { stripeSessionId: 'cs_weg', orderedAt: new Date().toISOString() })
|
|
const frisch = await checkout('termine', bodyT())
|
|
assert.equal(frisch.status, 409)
|
|
assert.equal(frisch.body.error, T.orderedConflictMessage)
|
|
assert.deepEqual(spuren(), { sessions: 0, consents: 0, features: 1 }, 'nichts angelegt')
|
|
featureDoc('ordered', { stripeSessionId: 'cs_weg', orderedAt: new Date(Date.now() - 2 * DAY_MS).toISOString() })
|
|
const alt = await checkout('termine', bodyT())
|
|
assert.equal(alt.status, 200)
|
|
assert.equal(sitzungen().length, 1)
|
|
assert.equal(nachweise().length, 1)
|
|
assert.equal(termineDoc().stripeSessionId, sitzungen()[0].id)
|
|
// ordered ohne gespeicherte Sitzungs-ID: ebenfalls eine neue Sitzung
|
|
featureDoc('ordered', { stripeSessionId: '' })
|
|
assert.equal((await checkout('termine', bodyT())).status, 200)
|
|
assert.equal(sitzungen().length, 2)
|
|
assert.equal(nachweise().length, 2)
|
|
} finally {
|
|
log.mock.restore()
|
|
}
|
|
})
|
|
|
|
// ------------------------------------------------------------------------------------------ Zahlung fortsetzen ---
|
|
test('Zahlung fortsetzen: Dokument ordered mit Nachweis gibt dieselbe URL, ohne zweite Sitzung und zweiten Nachweis', async () => {
|
|
bereit()
|
|
const erste = await checkout('termine', bodyT())
|
|
const fort = await checkout('termine', { projectId: 'p1', fortsetzen: true })
|
|
assert.equal(fort.status, 200, JSON.stringify(fort.body))
|
|
assert.equal(fort.body.url, erste.body.url)
|
|
assert.equal(sitzungen().length, 1)
|
|
assert.equal(nachweise().length, 1)
|
|
})
|
|
|
|
test('Zahlung fortsetzen ohne Nachweis: 409 zustimmung_fehlt ohne URL, danach mit zustimmung 200 und der Nachweis ist wieder da', async () => {
|
|
bereit()
|
|
const erste = await checkout('termine', bodyT())
|
|
await deleteDocument('featureConsents', nachweise()[0].$id)
|
|
const fort = await checkout('termine', { projectId: 'p1', fortsetzen: true })
|
|
assert.equal(fort.status, 409)
|
|
assert.equal(fort.body.code, 'zustimmung_fehlt')
|
|
assert.equal(fort.body.url, undefined)
|
|
assert.doesNotMatch(JSON.stringify(fort.body), /https?:/)
|
|
assert.equal(nachweise().length, 0, 'ohne Zustimmung entsteht kein Nachweis')
|
|
assert.equal(sitzungen().length, 1)
|
|
// der Kunde stimmt im Formular erneut zu
|
|
const neu = await checkout('termine', bodyT())
|
|
assert.equal(neu.status, 200)
|
|
assert.equal(neu.body.url, erste.body.url)
|
|
assert.equal(nachweise().length, 1)
|
|
assert.equal(nachweise()[0].stripeSessionId, sitzungen()[0].id)
|
|
assert.equal(sitzungen().length, 1)
|
|
})
|
|
|
|
test('Zahlung fortsetzen bei abgelaufener Sitzung: 409 zustimmung_fehlt, mit zustimmung eine neue Sitzung', async () => {
|
|
bereit()
|
|
await checkout('termine', bodyT())
|
|
sitzungen()[0].status = 'expired'
|
|
const fort = await checkout('termine', { projectId: 'p1', fortsetzen: true })
|
|
assert.equal(fort.status, 409)
|
|
assert.equal(fort.body.code, 'zustimmung_fehlt')
|
|
assert.equal(fort.body.url, undefined)
|
|
assert.equal(sitzungen().length, 1, 'fortsetzen legt nie eine Sitzung an')
|
|
assert.equal(nachweise().length, 1)
|
|
const neu = await checkout('termine', bodyT())
|
|
assert.equal(neu.status, 200)
|
|
assert.equal(sitzungen().length, 2)
|
|
assert.equal(nachweise().length, 2)
|
|
})
|
|
|
|
test('Zahlung fortsetzen: ein Lesefehler beim Nachweis ist 500 ohne URL (im Zweifel keine Weiterleitung)', async () => {
|
|
bereit()
|
|
await checkout('termine', bodyT())
|
|
const log = stumm()
|
|
try {
|
|
aw.failNext({ collection: 'featureConsents', method: 'GET', status: 500, message: 'GEHEIMER-ROHTEXT' })
|
|
const fort = await checkout('termine', { projectId: 'p1', fortsetzen: true })
|
|
assert.equal(fort.status, 500)
|
|
assert.equal(fort.body.url, undefined)
|
|
assert.doesNotMatch(JSON.stringify(fort.body), /GEHEIMER-ROHTEXT/)
|
|
} finally {
|
|
log.mock.restore()
|
|
}
|
|
})
|
|
|
|
test('Zahlung fortsetzen bei News: gleiche Regeln (Nachweis da, dieselbe URL)', async () => {
|
|
const erste = await checkout('news', { projectId: 'p3', consent: true }, customerCookie('c3'))
|
|
const fort = await checkout('news', { projectId: 'p3', fortsetzen: true }, customerCookie('c3'))
|
|
assert.equal(fort.status, 200)
|
|
assert.equal(fort.body.url, erste.body.url)
|
|
assert.equal(nachweise().length, 1)
|
|
assert.equal(sitzungen().length, 1)
|
|
})
|
|
|
|
// ------------------------------------------------------------------------------------------ Meldungen aus dem Register ---
|
|
test('markOrdered: Meldungen kommen aus dem Register der Funktion', async () => {
|
|
for (const [key, feature] of [['news', config.features.news], ['termine', T]]) {
|
|
for (const [state, text] of [['setup', feature.orderedConflictMessage], ['active', feature.orderedConflictMessage], ['disabled', feature.disabledMessage]]) {
|
|
aw.seed('portalFeatures', { $id: `f_p1_${key}`, customerId: 'c1', projectId: 'p1', featureKey: key, state, enabled: state === 'active', siteKey: 'abcdefghijklmnop' })
|
|
await assert.rejects(
|
|
() => sf.markOrdered({ customerId: 'c1', projectId: 'p1', key, sessionId: 'cs_x', consent: {} }),
|
|
(err) => err.status === 409 && err.message === text,
|
|
`${key} ${state}`
|
|
)
|
|
}
|
|
}
|
|
})
|
|
|
|
// ------------------------------------------------------------------------------------------ fortsetzen ---
|
|
|
|
test('fortsetzen: ohne Dokument oder ausser ordered ist 400 zustimmung_fehlt, es entsteht nichts', async () => {
|
|
const fall = { projectId: 'p1', fortsetzen: true }
|
|
const res = await checkout('termine', fall)
|
|
assert.equal(res.status, 400)
|
|
assert.equal(res.body.code, 'zustimmung_fehlt')
|
|
keineSpuren()
|
|
for (const state of ['off', 'setup', 'active', 'disabled']) {
|
|
featureDoc(state) // seed ersetzt das Dokument gleicher ID
|
|
const vorher = spuren()
|
|
const antwort = await checkout('termine', fall)
|
|
assert.equal(antwort.status, 400, state)
|
|
assert.equal(antwort.body.code, 'zustimmung_fehlt', state)
|
|
assert.deepEqual(spuren(), vorher, `${state}: nichts angelegt`)
|
|
assert.equal(vorher.features, 1)
|
|
assert.equal(aw.docs('portalFeatures').get('f_p1_termine').state, state, `${state}: Dokument unveraendert`)
|
|
}
|
|
// ein Dokument eines anderen Kunden zaehlt nicht als eigenes ordered
|
|
await deleteDocument('portalFeatures', 'f_p1_termine')
|
|
featureDoc('ordered', { customerId: 'c2' })
|
|
assert.equal((await checkout('termine', fall)).status, 400)
|
|
})
|
|
|
|
test('fortsetzen: fremdes Projekt ist 403, die Projektpruefung kommt vor dem Dokument', async () => {
|
|
featureDoc('ordered')
|
|
const res = await checkout('termine', { projectId: 'p5', fortsetzen: true })
|
|
assert.equal(res.status, 403)
|
|
assert.equal(aw.docs('featureConsents').size, 0)
|
|
assert.equal(st.checkoutSessions.size, 0)
|
|
// ohne projectId bleibt es bei 400 Projekt
|
|
const ohne = await checkout('termine', { fortsetzen: true })
|
|
assert.equal(ohne.status, 400)
|
|
assert.equal(ohne.body.error, 'Bitte wähle ein Projekt.')
|
|
})
|
|
|
|
test('fortsetzen bei ordered ueberspringt nur die Zustimmung: Schalter, Unterlagen, Pruefung und Abo gelten weiter', async () => {
|
|
featureDoc('ordered')
|
|
const fall = { projectId: 'p1', fortsetzen: true }
|
|
// Schalter
|
|
config.termine.mode = 'off'
|
|
assert.equal((await checkout('termine', fall)).body.code, 'nicht_verfuegbar')
|
|
config.termine.mode = 'pilot'
|
|
// Unterlagen, Bestellpruefung
|
|
const lauf = ablaufAufzeichnen({ unterlagen: false })
|
|
try {
|
|
assert.equal((await checkout('termine', fall)).body.code, 'unterlagen_fehlen')
|
|
T.unterlagenBereit = async () => true
|
|
T.bestellpruefung = async () => [{ schritt: 'branche', text: 'Branche wählen' }]
|
|
assert.equal((await checkout('termine', fall)).body.code, 'bestellung_offen')
|
|
T.bestellpruefung = async () => []
|
|
// Abo
|
|
wartung({ cancelAtPeriodEnd: true })
|
|
assert.equal((await checkout('termine', fall)).body.code, 'gekuendigt')
|
|
} finally {
|
|
lauf.ende()
|
|
}
|
|
assert.equal(st.checkoutSessions.size, 0)
|
|
assert.equal(aw.docs('featureConsents').size, 0)
|
|
})
|
|
|
|
test('fortsetzen und zustimmung zusammen: die Zustimmung gilt und wird geprueft', async () => {
|
|
featureDoc('ordered')
|
|
// unvollstaendige Zustimmung neben fortsetzen: 400 trotz ordered
|
|
const unvollstaendig = await checkout('termine', { projectId: 'p1', fortsetzen: true, zustimmung: zustimmung({ haken: [] }) })
|
|
assert.equal(unvollstaendig.status, 400)
|
|
assert.equal(unvollstaendig.body.code, 'zustimmung_fehlt')
|
|
// alte Fassung neben fortsetzen: 409
|
|
const alt = await checkout('termine', { projectId: 'p1', fortsetzen: true, zustimmung: zustimmung({ version: 'alt' }) })
|
|
assert.equal(alt.status, 409)
|
|
assert.equal(alt.body.code, 'fassung_veraltet')
|
|
// gueltige Zustimmung neben fortsetzen und ohne Dokument im Zustand ordered: die Zustimmung genuegt, der Zustand zaehlt nicht
|
|
await deleteDocument('portalFeatures', 'f_p1_termine')
|
|
config.termine.mode = 'off'
|
|
const gueltig = await checkout('termine', { projectId: 'p1', fortsetzen: true, zustimmung: zustimmung() })
|
|
assert.equal(gueltig.body.code, 'nicht_verfuegbar', 'Zustimmung geprueft und bestanden, weiter bis zum Schalter')
|
|
// News-Altform: consent: true neben fortsetzen gilt als Zustimmung (fremdes Projekt => erst danach 403), consent: false nicht
|
|
assert.equal((await checkout('news', { projectId: 'p5', fortsetzen: true, consent: true })).status, 403)
|
|
const news = await checkout('news', { projectId: 'p1', fortsetzen: true, consent: false })
|
|
assert.equal(news.status, 400, 'fortsetzen ohne ordered')
|
|
assert.equal(news.body.code, 'zustimmung_fehlt')
|
|
assert.equal(aw.docs('portalFeatures').size, 0)
|
|
assert.equal(st.checkoutSessions.size, 0)
|
|
})
|
|
|
|
test('fortsetzen ist nur true wirksam: andere Werte zaehlen nicht', async () => {
|
|
featureDoc('ordered')
|
|
for (const fortsetzen of ['true', 1, 'ja', {}, [], false, null]) {
|
|
const res = await checkout('termine', { projectId: 'p1', fortsetzen })
|
|
assert.equal(res.status, 400, JSON.stringify(fortsetzen))
|
|
assert.equal(res.body.code, 'zustimmung_fehlt', JSON.stringify(fortsetzen))
|
|
}
|
|
})
|
|
|
|
// ------------------------------------------------------------------------------------------ News ---
|
|
test('News braucht kein Abo und keinen Termine-Schalter: c3 bestellt weiter', async () => {
|
|
config.termine.mode = 'off'
|
|
const res = await checkout('news', { projectId: 'p3', consent: true }, customerCookie('c3'))
|
|
assert.equal(res.status, 200)
|
|
assert.match(res.body.url, /^https?:\/\//)
|
|
assert.equal(st.checkoutSessions.size, 1)
|
|
assert.equal([...aw.docs('portalFeatures').values()].filter((d) => d.featureKey === 'news' && d.state === 'ordered').length, 1)
|
|
})
|
|
|
|
// ------------------------------------------------------------------------------------------ Bausteine ---
|
|
test('pruefeZustimmung: gibt die Haken-IDs in Reihenfolge des Registers zurueck, wirft 400/409 mit body', () => {
|
|
const ids = pflichtHaken()
|
|
assert.deepEqual(order.pruefeZustimmung(T, { zustimmung: zustimmung() }), { akzeptiert: ids })
|
|
assert.deepEqual(order.pruefeZustimmung(T, { zustimmung: zustimmung({ haken: [...ids].reverse() }) }), { akzeptiert: ids }, 'Registerreihenfolge')
|
|
assert.deepEqual(order.pruefeZustimmung(T, { zustimmung: zustimmung({ haken: [...ids, ids[0]] }) }), { akzeptiert: ids }, 'Doppelte zaehlen einmal')
|
|
assert.deepEqual(order.pruefeZustimmung(config.features.news, { consent: true }), { akzeptiert: ['beginn'] })
|
|
|
|
assert.throws(
|
|
() => order.pruefeZustimmung(T, { consent: true }),
|
|
(err) => err.status === 400 && err.code === 'zustimmung_fehlt' && err.body.code === 'zustimmung_fehlt'
|
|
)
|
|
assert.throws(
|
|
() => order.pruefeZustimmung(T, undefined),
|
|
(err) => err.status === 400
|
|
)
|
|
assert.throws(
|
|
() => order.pruefeZustimmung(T, { zustimmung: zustimmung({ version: 'alt' }) }),
|
|
(err) => err.status === 409 && err.code === 'fassung_veraltet' && err.body.code === 'fassung_veraltet' && err.body.consent.version === T.consent.version
|
|
)
|
|
// die Ansicht im Fehler ist eine Kopie: Aenderungen daran aendern das Register nicht
|
|
try {
|
|
order.pruefeZustimmung(T, { zustimmung: zustimmung({ version: 'alt' }) })
|
|
} catch (err) {
|
|
err.body.consent.items[0].text = 'manipuliert'
|
|
assert.notEqual(T.consent.items[0].text, 'manipuliert')
|
|
}
|
|
})
|
|
|
|
test('istFortsetzen: nur fortsetzen === true ohne Zustimmung und ohne consent: true', () => {
|
|
assert.equal(order.istFortsetzen({ fortsetzen: true }), true)
|
|
assert.equal(order.istFortsetzen({ fortsetzen: true, zustimmung: null }), true)
|
|
assert.equal(order.istFortsetzen({ fortsetzen: true, zustimmung: undefined, consent: false }), true)
|
|
assert.equal(order.istFortsetzen({ fortsetzen: true, zustimmung: {} }), false)
|
|
assert.equal(order.istFortsetzen({ fortsetzen: true, consent: true }), false)
|
|
assert.equal(order.istFortsetzen({ fortsetzen: 'true' }), false)
|
|
assert.equal(order.istFortsetzen({}), false)
|
|
assert.equal(order.istFortsetzen(undefined), false)
|
|
assert.equal(order.istFortsetzen(null), false)
|
|
})
|
|
|
|
test('ladeEntwurf: Einstellung und Fragebogen aus termineConfig, fehlend oder kaputt ist null', async () => {
|
|
const einstellung = { betrieb: { name: 'Salon' } }
|
|
const fragebogen = { schritt: 'branche', branche: 'friseur' }
|
|
const doc = (extra) => aw.seed('termineConfig', { $id: 'f_p1_termine', customerId: 'c1', projectId: 'p1', einstellung: JSON.stringify(einstellung), version: 3, updatedAt: new Date().toISOString(), updatedBy: 'x', ...extra })
|
|
|
|
assert.deepEqual(await order.ladeEntwurf('f_p1_termine'), { einstellung: null, fragebogen: null }, 'kein Dokument')
|
|
// ladeEinstellung (Kontext-Modul) meldet einen Fragebogen, den die Normalisierung ablehnt, mit einer Logzeile: hier stumm
|
|
const leise = stumm()
|
|
try {
|
|
doc({ fragebogen: JSON.stringify(fragebogen) })
|
|
assert.deepEqual(await order.ladeEntwurf('f_p1_termine'), { einstellung, fragebogen })
|
|
doc({})
|
|
assert.deepEqual(await order.ladeEntwurf('f_p1_termine'), { einstellung, fragebogen: null }, 'Attribut fehlt')
|
|
for (const kaputt of ['{kaputt', '', '[1,2]', '42', '"text"', 'null']) {
|
|
doc({ fragebogen: kaputt })
|
|
assert.deepEqual(await order.ladeEntwurf('f_p1_termine'), { einstellung, fragebogen: null }, `fragebogen ${JSON.stringify(kaputt)}`)
|
|
}
|
|
// Einstellung unlesbar: null (der Fehler wird im Kontext-Modul geloggt), der Fragebogen bleibt
|
|
doc({ einstellung: '{kaputt', fragebogen: JSON.stringify(fragebogen) })
|
|
assert.deepEqual(await order.ladeEntwurf('f_p1_termine'), { einstellung: null, fragebogen })
|
|
// Lesefehler werden nicht verschluckt
|
|
doc({ fragebogen: JSON.stringify(fragebogen) })
|
|
aw.failNext({ collection: 'termineConfig', status: 500 })
|
|
await assert.rejects(order.ladeEntwurf('f_p1_termine'))
|
|
aw.failNext({ collection: 'termineConfig', method: 'GET', status: 500, after: 1 })
|
|
await assert.rejects(order.ladeEntwurf('f_p1_termine'))
|
|
await assert.doesNotReject(order.ladeEntwurf('f_p1_termine'))
|
|
await assert.rejects(order.ladeEntwurf(''), TypeError)
|
|
} finally {
|
|
leise.mock.restore()
|
|
}
|
|
})
|