kontext.js: ctx.fragebogen (normalisiert; fehlt er oder ist er kaputt, der leere, kaputt mit einer Logzeile und ohne Wurf), ladeEinstellung liefert zusaetzlich fragebogen, neuer kontextFuerPortal(feature) fuer das Portal in jedem Zustand. speichereFragebogen schreibt unter termine-cfg:<id> nur fragebogen, updatedAt und updatedBy (ohne version; fehlt das Dokument: 409 keine_einstellung). Neue Option fragebogen bei speichereEinstellung schreibt Einstellung und Fragebogen in derselben Sperre. kontextPerSiteKey bleibt unveraendert (Entwurf nie oeffentlich), Tests dazu fuer off, ordered, setup, disabled und nach activate. Der Test ladeEntwurf in funktionenCheckout.test.js schaltet die neue Logzeile stumm. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
1184 lines
59 KiB
JavaScript
1184 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 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')
|
|
assert.equal(doc.consentIp, n.ip)
|
|
assert.equal(doc.consentUserAgent, n.userAgent)
|
|
})
|
|
|
|
test('Belehrung im Nachweis: Eintrag belehrung (haken false) direkt vor dem Knopf, News hat keinen', async () => {
|
|
bereit()
|
|
assert.equal(reg.BELEHRUNG_PLATZHALTER, 'ENTWURF (Block F): Widerrufsbelehrung mit Muster-Widerrufsformular.')
|
|
assert.equal(await reg.featureHaken('termine').belehrung(), reg.BELEHRUNG_PLATZHALTER)
|
|
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: reg.BELEHRUNG_PLATZHALTER, haken: false },
|
|
{ id: 'knopf', text: T.consent.knopf, haken: false },
|
|
])
|
|
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()
|
|
}
|
|
})
|