Terminplaner TP2: Branche und Vorlage im Entwurf, PUT /fragebogen
POST /api/termine/einstellungen/vorlage nimmt jetzt eine Branche (BRANCHEN), optionale
Betriebsfelder (nichtleere Werte ueberschreiben das Konto) und schreibt den Fragebogen mit
der Einstellung in einem Vorgang. Der Gesundheitsbereich ist ausgeschlossen (400
gesundheitsbereich, nichts wird angelegt). Eine bestehende Einstellung ersetzt nur
ersetzen:true mit passender version im Zustand off (neue IDs und Kalender-Token, Fragebogen
neu, Einbauwuensche bleiben). Die Route arbeitet im Entwurf (off, active, ordered/setup nur
in der Kundenansicht, disabled nie).
PUT /api/termine/fragebogen speichert nur den Fragebogen (Version der Einstellung bleibt),
ist in der Kundenansicht gesperrt (Waechter und Router), zaehlt gegen das Schreibbudget und
antwortet mit { fragebogen, offen }.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
1cd74226cb
commit
29a38a9592
4 files changed
+835
-33
No files matched your search
+124
-17
@@ -14,8 +14,8 @@ import { aktion, entsperre, manuell, sperre } from '../services/termine/ablaufBe
|
||||
import { EXPORT_SEITE, exportSeite, hole, imZeitraum, zaehleAnfragen, zaehleKuenftige } from '../services/termine/buchungen.js'
|
||||
import { fehlendePflichtangaben, neuerFeedToken, normalisiereEinstellung } from '../services/termine/einstellungen.js'
|
||||
import { loeschTag, loeschzeit } from '../services/termine/frist.js'
|
||||
import { BRANCHEN, bestellpruefung, bestellungAnsicht, leerFragebogen } from '../services/termine/fragebogen.js'
|
||||
import { kontextFuerPortal, kontextPerFeature, liveHost, speichereEinstellung } from '../services/termine/kontext.js'
|
||||
import { BRANCHEN, bestellpruefung, bestellungAnsicht, leerFragebogen, normalisiereFragebogen } from '../services/termine/fragebogen.js'
|
||||
import { kontextFuerPortal, kontextPerFeature, liveHost, speichereEinstellung, speichereFragebogen } from '../services/termine/kontext.js'
|
||||
import { zustellFehler } from '../services/termine/outbox.js'
|
||||
import { termineFuerKunde } from '../services/termine/schalter.js'
|
||||
import { belegungAus, freieZeiten } from '../services/termine/slots.js'
|
||||
@@ -31,7 +31,9 @@ import { ausUtc, datumKurz, plusTage, uhrzeit, zuUtc } from '../services/termine
|
||||
* Ausnahme Entwurf (TP2): GET /einstellungen liest in jedem Zustand (off, ordered, setup, active, disabled) und auch ohne
|
||||
* Funktionsdokument (virtueller Kontext `off`, nichts wird angelegt), siehe withTermine({ entwurf: true }). PATCH /einstellungen
|
||||
* schreibt den Entwurf nach eigenen Regeln (off und active ja; ordered und setup nur in der Kundenansicht; disabled nie; in off
|
||||
* nur mit laufendem Wartungs-Abo), speichert ihn ohne Pflichtangaben für Gäste und meldet sie in `fehlt`.
|
||||
* nur mit laufendem Wartungs-Abo), speichert ihn ohne Pflichtangaben für Gäste und meldet sie in `fehlt`. POST /einstellungen/vorlage
|
||||
* legt den Entwurf aus einer Branche an (oder ersetzt ihn, solange nichts bestellt ist) und schreibt dabei den Fragebogen mit;
|
||||
* PUT /fragebogen speichert nur den Fragebogen (nie die Version der Einstellung) und nie in der Kundenansicht.
|
||||
* Schreib-Routen zählen gegen 120 Änderungen pro Kunde und Stunde (im Speicher). Die Geschäftsregeln stehen in
|
||||
* services/termine/*; hier wird geprüft, übersetzt und protokolliert (Kundenansicht: termin_einstellungen, termin_aktion,
|
||||
* termin_manuell, nur nach Erfolg und ohne Gastdaten im Detail).
|
||||
@@ -72,7 +74,12 @@ const MELDUNG = {
|
||||
keineEinstellung: 'Bitte richte den Terminplaner zuerst ein.',
|
||||
zuViele: 'Zu viele Änderungen in kurzer Zeit. Bitte versuch es in einer Stunde wieder.',
|
||||
vorlage: 'Bitte wähle eine Vorlage aus der Liste.',
|
||||
branche: 'Bitte wähle eine Branche aus der Liste.',
|
||||
gesundheitsbereich: 'Für Betriebe im Gesundheitsbereich ist der Terminplaner nicht verfügbar. Wähle eine andere Branche, wenn dein Angebot nicht dazugehört.',
|
||||
betrieb: 'Die Betriebsangaben sind ungültig.',
|
||||
vorlageVorhanden: 'Es gibt schon Einstellungen. Eine Vorlage kannst du nur beim ersten Öffnen wählen.',
|
||||
nichtErsetzbar: 'Die Vorlage kannst du nur wechseln, solange du den Terminplaner noch nicht bestellt hast.',
|
||||
kundenansichtBestaetigen: 'In der Kundenansicht kann nichts bestätigt werden.',
|
||||
version: 'Die Version der Einstellungen fehlt. Bitte lade die Seite neu.',
|
||||
art: 'Bitte wähle „Termin eintragen“ oder „Zeit sperren“.',
|
||||
zeitraum: 'Bitte gib von und bis als Datum an (JJJJ-MM-TT).',
|
||||
@@ -361,28 +368,128 @@ async function betriebVorschlag(customerId, project) {
|
||||
return { name, adresse, email, telefon, websiteUrl: liveWebsite(project) }
|
||||
}
|
||||
|
||||
router.post('/einstellungen/vorlage', requireSession, withTermine({ schreiben: true }), limitWrites, route(async (req, res) => {
|
||||
const key = req.body?.vorlage
|
||||
if (typeof key !== 'string' || !Object.hasOwn(VORLAGEN, key)) throw stop(400, MELDUNG.vorlage, undefined, 'vorlage')
|
||||
if (req.ctx.einstellung) throw stop(409, MELDUNG.vorlageVorhanden, 'einstellung_vorhanden')
|
||||
// Betriebsfelder, die der Kunde beim Anlegen der Vorlage mitschicken darf (Anschrift und Telefon u. a. aus dem Konto vorbelegt)
|
||||
const BETRIEB_FELDER = ['name', 'adresse', 'telefon', 'email', 'websiteUrl']
|
||||
|
||||
const betrieb = await betriebAusKonto(getSessionCustomerId(req))
|
||||
/**
|
||||
* Die Betriebsfelder aus dem Rumpf der Vorlage-Route: nur die fünf bekannten, nur nichtleere Texte (ein leeres Feld lässt den
|
||||
* Wert aus dem Konto stehen). Kein Objekt oder ein Feld, das kein Text ist => 400. Prüft nur die Form; Länge, Adresse und
|
||||
* Mail-Format prüft die Einstellung selbst (ausVorlage).
|
||||
*/
|
||||
function betriebFelder(eingabe) {
|
||||
if (eingabe === undefined || eingabe === null) return {}
|
||||
if (!istObjekt(eingabe)) throw stop(400, MELDUNG.betrieb, undefined, 'betrieb')
|
||||
const felder = {}
|
||||
for (const feld of BETRIEB_FELDER) {
|
||||
const wert = eingabe[feld]
|
||||
if (wert === undefined || wert === null) continue
|
||||
if (typeof wert !== 'string') throw stop(400, MELDUNG.betrieb, undefined, `betrieb.${feld}`)
|
||||
if (wert.trim() !== '') felder[feld] = wert.trim()
|
||||
}
|
||||
return felder
|
||||
}
|
||||
|
||||
/**
|
||||
* Entwurf aus einer Vorlage anlegen (TP2: aus einer Branche, auch als Ersatz für den bestehenden Entwurf).
|
||||
* { projectId, branche } Branche laut BRANCHEN; unbekannt => 400; Gesundheitsbereich => 400 `gesundheitsbereich` (harter
|
||||
* Ausschluss, nichts wird angelegt, auch kein Funktionsdokument); die Branche steht danach im Fragebogen
|
||||
* { projectId, vorlage } wie in TP1: Vorlage nach Schlüssel, der Fragebogen bleibt unberührt
|
||||
* betrieb { name, adresse, telefon, email, websiteUrl }: überschreibt nur mit nichtleeren Werten das Konto
|
||||
* ersetzen: true + version eine bestehende Einstellung ersetzen: nur im Zustand `off` und nur mit der gelesenen Version (sonst 409
|
||||
* `EINSTELLUNG_VERALTET`); neue IDs und Kalender-Token, Fragebogen neu (Branche, keine bestätigten
|
||||
* Schritte, Preise und Bundesland ungeprüft), die Einbauwünsche bleiben. Einstellung und Fragebogen
|
||||
* werden in einem Vorgang unter der Sperre geschrieben (speichereEinstellung, Option fragebogen).
|
||||
* Ohne `ersetzen` gilt wie in TP1: gibt es schon Einstellungen, ist es 409 `einstellung_vorhanden`.
|
||||
*/
|
||||
router.post('/einstellungen/vorlage', requireSession, withTermine({ schreiben: true, entwurf: true }), limitWrites, route(async (req, res) => {
|
||||
const body = istObjekt(req.body) ? req.body : {}
|
||||
let key
|
||||
let brancheKey = null
|
||||
if (body.branche !== undefined && body.branche !== null) {
|
||||
const branche = typeof body.branche === 'string' ? BRANCHEN.find((b) => b.key === body.branche) : undefined
|
||||
if (!branche) throw stop(400, MELDUNG.branche, undefined, 'branche')
|
||||
if (branche.gesundheit) throw stop(400, MELDUNG.gesundheitsbereich, 'gesundheitsbereich', 'branche')
|
||||
key = branche.vorlage
|
||||
brancheKey = branche.key
|
||||
} else {
|
||||
key = body.vorlage
|
||||
if (typeof key !== 'string' || !Object.hasOwn(VORLAGEN, key)) throw stop(400, MELDUNG.vorlage, undefined, 'vorlage')
|
||||
}
|
||||
const eigene = betriebFelder(body.betrieb)
|
||||
|
||||
const ersetzen = body.ersetzen === true
|
||||
if (req.ctx.einstellung && !ersetzen) throw stop(409, MELDUNG.vorlageVorhanden, 'einstellung_vorhanden')
|
||||
if (ersetzen) {
|
||||
if (!Number.isInteger(body.version) || body.version < 0) throw stop(400, MELDUNG.version, undefined, 'version')
|
||||
if (req.ctx.feature.state !== 'off') throw stop(409, MELDUNG.nichtErsetzbar, 'nicht_ersetzbar')
|
||||
}
|
||||
|
||||
const customerId = getSessionCustomerId(req)
|
||||
const betrieb = { ...(await betriebAusKonto(customerId)), ...eigene }
|
||||
const now = uhr()
|
||||
let gespeichert
|
||||
try {
|
||||
// version 0: nur wenn noch nichts gespeichert ist, auch bei zwei gleichzeitigen Anfragen
|
||||
gespeichert = await speichereEinstellung(req.ctx.feature, () => {
|
||||
const baue = () => {
|
||||
try {
|
||||
return ausVorlage(key, betrieb, now)
|
||||
} catch (err) {
|
||||
throw Object.assign(new Error(err.message), { status: 400, fehler: err.fehler ?? { _: err.message } })
|
||||
}
|
||||
}, { version: 0, by: von(req), now })
|
||||
} catch (err) {
|
||||
throw err?.code === 'EINSTELLUNG_VERALTET' ? stop(409, MELDUNG.vorlageVorhanden, 'einstellung_vorhanden') : err
|
||||
}
|
||||
await logIfKundenansicht(req, 'termin_einstellungen', `Vorlage ${key}`)
|
||||
res.status(201).json({ einstellung: fuerKunde(gespeichert.einstellung), version: gespeichert.version, fehlt: fehlendePflichtangaben(gespeichert.einstellung) })
|
||||
|
||||
// Ohne Funktionsdokument legt erst dieser Handler es an, nach der Eingabeprüfung (eine abgelehnte Vorlage hinterlässt nichts)
|
||||
let feature = req.ctx.feature
|
||||
if (!feature.$id) {
|
||||
baue()
|
||||
feature = await ensureFeatureDoc(customerId, req.projectId, 'termine')
|
||||
}
|
||||
|
||||
// Branche oder Ersetzen: der Fragebogen beginnt neu, nur die Einbauwünsche bleiben (aus dem Stand unter der Sperre)
|
||||
const fragebogen = brancheKey !== null || ersetzen
|
||||
? (bisher) => ({ ...leerFragebogen(), branche: brancheKey ?? key, einbau: bisher.einbau })
|
||||
: undefined
|
||||
|
||||
let gespeichert
|
||||
try {
|
||||
// ohne ersetzen version 0: nur wenn noch nichts gespeichert ist, auch bei zwei gleichzeitigen Anfragen
|
||||
gespeichert = await speichereEinstellung(feature, baue, { version: ersetzen ? body.version : 0, by: von(req), now, fragebogen })
|
||||
} catch (err) {
|
||||
throw err?.code === 'EINSTELLUNG_VERALTET' && !ersetzen ? stop(409, MELDUNG.vorlageVorhanden, 'einstellung_vorhanden') : err
|
||||
}
|
||||
await logIfKundenansicht(req, 'termin_einstellungen', ersetzen ? `Vorlage ${key} ersetzt` : `Vorlage ${key}`)
|
||||
res.status(201).json({
|
||||
einstellung: fuerKunde(gespeichert.einstellung),
|
||||
version: gespeichert.version,
|
||||
fehlt: fehlendePflichtangaben(gespeichert.einstellung),
|
||||
fragebogen: gespeichert.fragebogen ?? req.ctx.fragebogen,
|
||||
})
|
||||
}))
|
||||
|
||||
/** Schritte bestätigt nur der Kunde selbst: jede Sitzung der Kundenansicht (viewAs) bekommt 403, vor jeder anderen Prüfung. */
|
||||
const nichtInKundenansicht = route(async (req, _res, next) => {
|
||||
if (req.session?.viewAs) throw stop(403, MELDUNG.kundenansichtBestaetigen)
|
||||
return next()
|
||||
})
|
||||
|
||||
/**
|
||||
* Fragebogen speichern (Branche, bestätigte Schritte, Einbauwünsche; PUT ersetzt den ganzen Fragebogen). Nie in der Kundenansicht:
|
||||
* Schritte bestätigt nur der Kunde selbst (die Erlaubnisliste des Wächters kennt den Pfad nicht, hier gilt es noch einmal).
|
||||
* Zustandsregeln wie beim Entwurf (off mit laufendem Wartungs-Abo und active ja, ordered, setup und disabled 409). Schreibt nur
|
||||
* fragebogen, updatedAt und updatedBy: die Version der Einstellung bleibt, ein offener Einstellungs-Editor kann weiter speichern.
|
||||
* Ohne Einstellung 409 `keine_einstellung`. Antwort { fragebogen, offen }; `offen` wie im GET nur in `off`. Kein Protokolleintrag.
|
||||
*/
|
||||
router.put('/fragebogen', requireSession, nichtInKundenansicht, withTermine({ schreiben: true, entwurf: true }), limitWrites, route(async (req, res) => {
|
||||
const geprueft = normalisiereFragebogen(req.body?.fragebogen)
|
||||
if (geprueft.fehler) throw Object.assign(new Error(Object.values(geprueft.fehler)[0]), { status: 400, fehler: geprueft.fehler })
|
||||
const { einstellung, feature } = mitEinstellung(req)
|
||||
|
||||
let gespeichert
|
||||
try {
|
||||
gespeichert = await speichereFragebogen(feature, () => geprueft.fragebogen, { by: von(req), now: uhr() })
|
||||
} catch (err) {
|
||||
throw err?.code === 'keine_einstellung' ? stop(409, MELDUNG.keineEinstellung, 'keine_einstellung') : err
|
||||
}
|
||||
const abo = feature.state === 'off' ? await aboFuerVorschau(getSessionCustomerId(req)) : null
|
||||
const offen = feature.state === 'off' ? bestellpruefung({ einstellung, fragebogen: gespeichert.fragebogen, feature: featureConfig('termine'), abo }) : null
|
||||
res.json({ fragebogen: gespeichert.fragebogen, offen })
|
||||
}))
|
||||
|
||||
/**
|
||||
|
||||
@@ -146,22 +146,29 @@ test('Erlaubnisliste: genau News, Terminplaner, Analyse-Link, Ende, Logout und L
|
||||
})
|
||||
|
||||
// Spec 2026-10-03, Abschnitt 3: unbekannte Schreib-Pfade unter /api/termine sind in der Kundenansicht gesperrt, bevor der Router
|
||||
// sie sieht (auch Pfade ohne Route und Gross-/Kleinschreibung); normale Kunden beruehrt der Waechter nicht
|
||||
// sie sieht (auch Pfade ohne Route und Gross-/Kleinschreibung); normale Kunden beruehrt der Waechter nicht.
|
||||
// PUT /fragebogen hat seit C5 eine echte Route: in der Kundenansicht sperrt sie der Waechter (403 view_blocked, der Router kommt nie
|
||||
// dran), ein normaler Kunde mit eigenem Projekt bekommt die Antwort der Route (hier 409 "noch nicht verfuegbar": der Pilot-Schalter
|
||||
// steht im Test auf off; weder 403 noch 404). Der 403 am Router selbst (ohne Waechter) steht in test/termineFragebogenRoutes.test.js.
|
||||
test('unbekanntes POST/PUT unter /api/termine in der Kundenansicht -> 403 view_blocked, normale Kunden nicht', async () => {
|
||||
const faelle = [
|
||||
['POST', '/api/termine/zukunft'],
|
||||
['PUT', '/api/termine/fragebogen'],
|
||||
['PUT', '/api/termine/fragebogen', { projectId: 'p1', fragebogen: {} }],
|
||||
['POST', '/api/termine/buchungen/abc/aktion/x'],
|
||||
['POST', '/API/Termine/Zukunft'],
|
||||
]
|
||||
for (const [method, p] of faelle) {
|
||||
const gesperrt = await call(method, p, { ...kv(), body: {} })
|
||||
for (const [method, p, body = {}] of faelle) {
|
||||
const gesperrt = await call(method, p, { ...kv(), body })
|
||||
assert.equal(gesperrt.status, 403, `${method} ${p}`)
|
||||
assert.equal(gesperrt.body.code, 'view_blocked', `${method} ${p}`)
|
||||
const kunde = await call(method, p, { cookie: customerCookie('c1'), body: {} })
|
||||
const kunde = await call(method, p, { cookie: customerCookie('c1'), body })
|
||||
assert.notEqual(kunde.status, 403, `${method} ${p}: normaler Kunde`)
|
||||
// PUT /fragebogen bekommt in Block C eine echte Route (dann antwortet sie selbst); die anderen Pfade gibt es nie
|
||||
if (!p.endsWith('/fragebogen')) assert.equal(kunde.status, 404, `${method} ${p}: der Router kennt den Pfad nicht`)
|
||||
if (p.endsWith('/fragebogen')) {
|
||||
assert.equal(kunde.status, 409, `${method} ${p}: die Route antwortet selbst`)
|
||||
assert.equal(kunde.body.code, 'nicht_verfuegbar', `${method} ${p}`)
|
||||
} else {
|
||||
assert.equal(kunde.status, 404, `${method} ${p}: der Router kennt den Pfad nicht`)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -12,7 +12,9 @@ import { TERMINE_DATETIMES } from '../server/services/termine/schema.js'
|
||||
// kundenansicht, abo, offen, bestellung, betriebVorschlag, einbau/feeds nur in active/disabled). Teil 1: nur Lesen.
|
||||
// Teil 1b (Task C4b): Schreibregeln des Entwurfs fuer PATCH /einstellungen (off und active ja, ordered und setup nur in der
|
||||
// Kundenansicht, disabled nie; in off nur mit laufendem Wartungs-Abo), PATCH mit `fehlt`, und die Gegenprobe an der
|
||||
// oeffentlichen Schnittstelle (ein Entwurf ist nie oeffentlich, Einbauwuensche nie). Vorlage und PUT fragebogen folgen mit C5/C6.
|
||||
// oeffentlichen Schnittstelle (ein Entwurf ist nie oeffentlich, Einbauwuensche nie).
|
||||
// Teil 2 (Task C5): POST /einstellungen/vorlage mit Branche (Fragebogen mit, Gesundheitsbereich ausgeschlossen, Ersetzen im Entwurf)
|
||||
// und PUT /fragebogen (speichert nur den Fragebogen, nie in der Kundenansicht), dazu das Schreibbudget.
|
||||
const aw = await startAppwriteMock({
|
||||
schema: {
|
||||
portalFeatures: schemaKeys('portalFeatures'),
|
||||
@@ -33,7 +35,7 @@ useTestEnv({ appwriteUrl: aw.url, stripeUrl: st.url })
|
||||
|
||||
const { config } = await import('../server/config.js')
|
||||
const { startTestApp, send } = await import('./support/testApp.js')
|
||||
const { customerCookie, kundenansichtCookie, seedFixtures, subscriptionDoc, wartungDoc, customers } = await import('./support/fixtures.js')
|
||||
const { customerCookie, kundenansichtCookie, viewAsCookie, seedFixtures, subscriptionDoc, wartungDoc, customers } = await import('./support/fixtures.js')
|
||||
const { bestellbarerStand, entwurfDoc } = await import('./support/fragebogenStand.js')
|
||||
const { deleteDocument, updateDocument } = await import('../server/services/appwriteAdmin.js')
|
||||
const { kundenansichtGuard } = await import('../server/middleware/kundenansichtGuard.js')
|
||||
@@ -41,15 +43,18 @@ const { default: termineRoutes, setzeUhrFuerTests, resetTermineRoutesForTests }
|
||||
const { default: publicTermineRoutes, resetPublicTermineForTests } = await import('../server/routes/publicTermine.js')
|
||||
const { resetKontextForTests } = await import('../server/services/termine/kontext.js')
|
||||
const { resetTermineLimitsForTests } = await import('../server/services/termine/limits.js')
|
||||
const { BRANCHEN, bestellpruefung, bestellungAnsicht, leerFragebogen } = await import('../server/services/termine/fragebogen.js')
|
||||
const { BRANCHEN, SCHRITTE, bestellpruefung, bestellungAnsicht, leerFragebogen } = await import('../server/services/termine/fragebogen.js')
|
||||
const { aboMeldung } = await import('../server/services/featureOrder.js')
|
||||
|
||||
// Der Router allein, ohne Waechter der Kundenansicht (Teil 2: der Router prueft die Kundenansicht bei PUT /fragebogen selbst).
|
||||
// Zuerst gestartet: send() spricht die zuletzt gestartete App an, das ist die mit Waechter.
|
||||
const direkt = await startTestApp({ '/api/termine': termineRoutes })
|
||||
const app = await startTestApp({
|
||||
'/': kundenansichtGuard,
|
||||
'/api/public/termine/v1': publicTermineRoutes,
|
||||
'/api/termine': termineRoutes,
|
||||
})
|
||||
after(() => Promise.all([app.close(), aw.close(), st.close()]))
|
||||
after(() => Promise.all([app.close(), direkt.close(), aw.close(), st.close()]))
|
||||
|
||||
const T = config.features.termine
|
||||
const JETZT = new Date('2026-10-05T08:00:00.000Z')
|
||||
@@ -819,14 +824,13 @@ test('Abo: gelesen wird nur in off (und ohne Dokument); in active und in der Kun
|
||||
assert.equal(aw.listCalls('subscriptions').length, 1, 'in off genau eine Abfrage')
|
||||
})
|
||||
|
||||
test('Schreibregeln: die anderen Schreib-Routen behalten die Schranke active (Vorlage und Feed-Link im Entwurf bleiben 409 "nicht aktiv")', async () => {
|
||||
test('Schreibregeln: die übrigen Schreib-Routen behalten die Schranke active (der Feed-Link im Entwurf bleibt 409 "nicht aktiv"; Vorlage und PUT fragebogen: Teil 2)', async () => {
|
||||
for (const zustand of ['keins', 'off', 'ordered', 'setup']) {
|
||||
await deleteDocument('portalFeatures', FEATURE).catch(() => {})
|
||||
await deleteDocument('termineConfig', FEATURE).catch(() => {})
|
||||
setzeZustand(zustand)
|
||||
const vorher = dokumente()
|
||||
for (const [method, pfad, body] of [
|
||||
['POST', '/api/termine/einstellungen/vorlage', { projectId: 'p1', vorlage: 'salon' }],
|
||||
['POST', '/api/termine/ressourcen/r_aaaaaaa1/feed', { projectId: 'p1' }],
|
||||
]) {
|
||||
const res = await send(method, pfad, { cookie, body })
|
||||
@@ -920,3 +924,688 @@ test('Einbauwünsche nie öffentlich: fragebogen.einbau.stelle und knopfText ste
|
||||
assert.deepEqual((await oeffentlichConfig()).body, { aktiv: false }, zustand)
|
||||
}
|
||||
})
|
||||
|
||||
// ================================================================ Teil 2 (C5): Branche und Vorlage im Entwurf, PUT /fragebogen
|
||||
|
||||
const VORLAGE_URL = '/api/termine/einstellungen/vorlage'
|
||||
const FRAGEBOGEN_URL = '/api/termine/fragebogen'
|
||||
const KUNDENANSICHT_TEXT = 'In der Kundenansicht kann nichts bestätigt werden.'
|
||||
const GESUNDHEIT_TEXT = 'Für Betriebe im Gesundheitsbereich ist der Terminplaner nicht verfügbar. Wähle eine andere Branche, wenn dein Angebot nicht dazugehört.'
|
||||
|
||||
const vorlage = (body, auth = { cookie }) => send('POST', VORLAGE_URL, { ...auth, body: { projectId: 'p1', ...body } })
|
||||
const putFragebogen = (fragebogen, auth = { cookie }, extra = {}) => send('PUT', FRAGEBOGEN_URL, { ...auth, body: { projectId: 'p1', fragebogen, ...extra } })
|
||||
/** Anfrage an den Router ohne Waechter der Kundenansicht. */
|
||||
async function sendDirekt(method, pfad, { body, cookie: c } = {}) {
|
||||
const res = await fetch(direkt.url + pfad, {
|
||||
method,
|
||||
headers: { 'content-type': 'application/json', ...(c ? { cookie: c } : {}) },
|
||||
body: JSON.stringify(body),
|
||||
})
|
||||
const text = await res.text()
|
||||
return { status: res.status, body: text ? JSON.parse(text) : null }
|
||||
}
|
||||
const gespeicherteEinstellung = () => JSON.parse(termineConfigDoc().einstellung)
|
||||
const gespeicherterFragebogen = () => JSON.parse(termineConfigDoc().fragebogen)
|
||||
const ids = (e) => [...e.ressourcen.map((r) => r.id), ...e.leistungen.map((l) => l.id)]
|
||||
const feedTokens = (e) => e.ressourcen.map((r) => r.feedToken)
|
||||
const EINBAU = { modus: 'knopf', knopfText: 'Termin buchen', stelle: 'Unter den Öffnungszeiten', gestaltung: 'Dunkel mit goldener Schrift', bemerkung: 'Bitte vor Freitag' }
|
||||
/** Bestellbarer Stand mit Einbauwünschen und Zustand des Funktionsdokuments (Version 1). */
|
||||
const standMitEinbau = (zustand = 'off') => {
|
||||
const stand = bestellbarerStand({ now: JETZT })
|
||||
stand.fragebogen.einbau = { ...EINBAU }
|
||||
setzeZustand(zustand, { stand })
|
||||
return stand
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- Vorlage mit Branche: Entwurf anlegen
|
||||
|
||||
test('Vorlage mit Branche salon ohne Dokument: 201, Funktionsdokument off mit dauerhaftem siteKey, termineConfig mit Fragebogen-Branche', async () => {
|
||||
assert.equal(aw.docs('portalFeatures').size, 0)
|
||||
const res = await vorlage({ branche: 'salon' })
|
||||
assert.equal(res.status, 201, JSON.stringify(res.body))
|
||||
assert.deepEqual(Object.keys(res.body).sort(), ['einstellung', 'fehlt', 'fragebogen', 'version'])
|
||||
assert.equal(res.body.version, 1)
|
||||
assert.deepEqual(res.body.fehlt, [])
|
||||
assert.equal(res.body.einstellung.vorlage, 'salon')
|
||||
assert.ok(res.body.einstellung.leistungen.length >= 2)
|
||||
assert.ok(res.body.einstellung.ressourcen.every((r) => !('feedToken' in r)), 'der Browser bekommt nie ein Token')
|
||||
assert.deepEqual(res.body.fragebogen, { ...leerFragebogen(), branche: 'salon' })
|
||||
|
||||
const feature = aw.docs('portalFeatures').get(FEATURE)
|
||||
assert.equal(feature.state, 'off')
|
||||
assert.equal(feature.enabled, false)
|
||||
assert.equal(feature.unlockedByPurchase, false)
|
||||
assert.match(feature.siteKey, /^[a-z2-7]{16}$/)
|
||||
assert.equal(termineConfigDoc().version, 1)
|
||||
assert.equal(termineConfigDoc().updatedBy, 'kunde:c1')
|
||||
assert.equal(termineConfigDoc().customerId, 'c1')
|
||||
assert.equal(termineConfigDoc().projectId, 'p1')
|
||||
assert.equal(gespeicherterFragebogen().branche, 'salon')
|
||||
assert.deepEqual(gespeicherterFragebogen(), res.body.fragebogen)
|
||||
assert.ok(gespeicherteEinstellung().ressourcen.every((r) => r.feedToken.length === 43), 'gespeichert hat jede Person ein Token')
|
||||
assert.equal(gespeicherteEinstellung().betrieb.name, 'Salon Müller', 'Betrieb aus dem Kundenkonto')
|
||||
assert.equal(logEintraege().length, 0, 'kein Eintrag für den Kunden selbst')
|
||||
|
||||
// GET zeigt denselben Stand, der Entwurf ist öffentlich unsichtbar
|
||||
const gelesen = (await send('GET', einstellungenUrl(), { cookie })).body
|
||||
assert.equal(gelesen.state, 'off')
|
||||
assert.equal(gelesen.fragebogen.branche, 'salon')
|
||||
assert.equal(gelesen.version, 1)
|
||||
assert.deepEqual((await oeffentlichConfig(feature.siteKey)).body, { aktiv: false })
|
||||
const siteKey = feature.siteKey
|
||||
assert.equal((await vorlage({ branche: 'salon' })).status, 409)
|
||||
assert.equal(aw.docs('portalFeatures').get(FEATURE).siteKey, siteKey, 'der siteKey bleibt')
|
||||
})
|
||||
|
||||
test('Vorlage mit Branche: jede Branche außer dem Gesundheitsbereich legt ihre Vorlage an, der Fragebogen nennt sie', async () => {
|
||||
const mitVorlage = BRANCHEN.filter((b) => !b.gesundheit)
|
||||
assert.ok(mitVorlage.length >= 9)
|
||||
for (const branche of mitVorlage) {
|
||||
await deleteDocument('portalFeatures', FEATURE).catch(() => {})
|
||||
await deleteDocument('termineConfig', FEATURE).catch(() => {})
|
||||
const res = await vorlage({ branche: branche.key })
|
||||
assert.equal(res.status, 201, `${branche.key}: ${JSON.stringify(res.body)}`)
|
||||
assert.equal(res.body.einstellung.vorlage, branche.vorlage, branche.key)
|
||||
assert.equal(res.body.fragebogen.branche, branche.key)
|
||||
assert.equal(gespeicherterFragebogen().branche, branche.key)
|
||||
}
|
||||
})
|
||||
|
||||
test('Vorlage: Betriebsfelder aus dem Rumpf überschreiben das Konto, leere Felder nicht, unbekannte und fremde Schlüssel werden ignoriert', async () => {
|
||||
const res = await vorlage({
|
||||
branche: 'salon',
|
||||
betrieb: { name: ' Haarstudio Neu ', email: '', telefon: ' ', adresse: '', websiteUrl: 'https://haarstudio.example.invalid', impressumUrl: 'https://boese.example.invalid/impressum' },
|
||||
})
|
||||
assert.equal(res.status, 201, JSON.stringify(res.body))
|
||||
const betrieb = res.body.einstellung.betrieb
|
||||
assert.equal(betrieb.name, 'Haarstudio Neu', 'ein nichtleerer Wert überschreibt das Konto')
|
||||
assert.equal(betrieb.websiteUrl, 'https://haarstudio.example.invalid/', 'die Adresse wird wie bei jeder Einstellung normalisiert')
|
||||
assert.equal(betrieb.email, 'k@example.invalid', 'ein leeres Feld lässt den Wert aus dem Konto')
|
||||
assert.equal(betrieb.telefon, '06301 1234')
|
||||
assert.equal(betrieb.adresse, 'Hauptstr. 1, 67722 Winnweiler')
|
||||
assert.notEqual(betrieb.impressumUrl, 'https://boese.example.invalid/impressum', 'nur die fünf Betriebsfelder gelten')
|
||||
assert.deepEqual(res.body.fehlt, [])
|
||||
})
|
||||
|
||||
test('Vorlage: Betriebsfelder ohne Konto-Angaben werden aus dem Rumpf gefüllt und "fehlt" schrumpft', async () => {
|
||||
await deleteDocument('customers', 'c1')
|
||||
aw.seed('customers', customers.c1) // Konto ohne Firma, Anschrift und Telefon
|
||||
const leer = await vorlage({ branche: 'salon' })
|
||||
assert.equal(leer.status, 201, JSON.stringify(leer.body))
|
||||
assert.deepEqual(leer.body.fehlt, FEHLT_BEIDES)
|
||||
await deleteDocument('termineConfig', FEATURE)
|
||||
const voll = await vorlage({ branche: 'salon', betrieb: { telefon: '0151 2345678', adresse: 'Weg 1, 12345 Ort' } })
|
||||
assert.equal(voll.status, 201, JSON.stringify(voll.body))
|
||||
assert.deepEqual(voll.body.fehlt, [])
|
||||
assert.equal(voll.body.einstellung.betrieb.telefon, '0151 2345678')
|
||||
})
|
||||
|
||||
test('Vorlage: Betriebsangaben in falscher Form => 400, nichts wird angelegt (auch kein Funktionsdokument)', async () => {
|
||||
for (const betrieb of ['Salon', 5, ['a'], { name: 5 }, { telefon: {} }, { email: ['x'] }, { websiteUrl: true }]) {
|
||||
const res = await vorlage({ branche: 'salon', betrieb })
|
||||
assert.equal(res.status, 400, JSON.stringify(betrieb))
|
||||
assert.ok(res.body.fehler?.betrieb || Object.keys(res.body.fehler ?? {}).some((k) => k.startsWith('betrieb.')), JSON.stringify(betrieb))
|
||||
}
|
||||
// eine Angabe, die die Einstellung ablehnt (ungültige Adresse): 400, ohne Dokument entsteht nichts
|
||||
const ungueltig = await vorlage({ branche: 'salon', betrieb: { websiteUrl: 'javascript:alert(1)' } })
|
||||
assert.equal(ungueltig.status, 400, JSON.stringify(ungueltig.body))
|
||||
assert.equal(aw.docs('portalFeatures').size, 0)
|
||||
assert.equal(aw.docs('termineConfig').size, 0)
|
||||
})
|
||||
|
||||
test('Vorlage ohne Branche (nur vorlage) bleibt wie in TP1: Einstellung aus der Vorlage, der gespeicherte Fragebogen bleibt unberührt', async () => {
|
||||
setzeZustand('active', { stand: null })
|
||||
const res = await vorlage({ vorlage: 'kosmetik' })
|
||||
assert.equal(res.status, 201, JSON.stringify(res.body))
|
||||
assert.equal(res.body.einstellung.vorlage, 'kosmetik')
|
||||
assert.deepEqual(res.body.fragebogen, leerFragebogen())
|
||||
assert.equal(termineConfigDoc().fragebogen, undefined, 'ohne Branche wird kein Fragebogen geschrieben')
|
||||
// und die Antwort auf eine Branche gewinnt gegenüber einer mitgeschickten vorlage
|
||||
await deleteDocument('termineConfig', FEATURE)
|
||||
const beides = await vorlage({ branche: 'tattoo', vorlage: 'salon' })
|
||||
assert.equal(beides.status, 201)
|
||||
assert.equal(beides.body.einstellung.vorlage, 'tattoo')
|
||||
assert.equal(beides.body.fragebogen.branche, 'tattoo')
|
||||
})
|
||||
|
||||
test('Vorlage: unbekannte Branche (auch Prototyp-Schlüssel, falsche Typen) => 400 mit Feld branche, nichts angelegt', async () => {
|
||||
for (const branche of ['gibtsnicht', '__proto__', 'constructor', 'toString', 'hasOwnProperty', '', 5, true, {}, ['salon']]) {
|
||||
const res = await vorlage({ branche })
|
||||
assert.equal(res.status, 400, JSON.stringify(branche))
|
||||
assert.equal(res.body.fehler.branche, 'Bitte wähle eine Branche aus der Liste.')
|
||||
assert.equal(res.body.code, undefined)
|
||||
}
|
||||
assert.equal(aw.docs('portalFeatures').size, 0)
|
||||
assert.equal(aw.docs('termineConfig').size, 0)
|
||||
})
|
||||
|
||||
// ---------------------------------------------------------------- Gesundheitsbereich: harter Ausschluss
|
||||
|
||||
test('Vorlage mit Branche gesundheit => 400 gesundheitsbereich; ohne vorhandenes Dokument wird weder portalFeatures noch termineConfig angelegt', async () => {
|
||||
const vorher = dokumente()
|
||||
for (const auth of [{ cookie }, kv()]) {
|
||||
const res = await vorlage({ branche: 'gesundheit' }, auth)
|
||||
assert.equal(res.status, 400, JSON.stringify(res.body))
|
||||
assert.equal(res.body.code, 'gesundheitsbereich')
|
||||
assert.equal(res.body.error, GESUNDHEIT_TEXT)
|
||||
assert.equal(res.body.fehler.branche, GESUNDHEIT_TEXT)
|
||||
}
|
||||
assert.equal(dokumente(), vorher)
|
||||
assert.equal(aw.docs('portalFeatures').size, 0)
|
||||
assert.equal(aw.docs('termineConfig').size, 0)
|
||||
assert.equal(aw.docs('customerViewLog').size, 0, 'abgelehnt heißt nichts protokolliert')
|
||||
})
|
||||
|
||||
test('Vorlage mit Branche gesundheit bei vorhandenem Entwurf (auch mit ersetzen): 400, der Entwurf und sein Fragebogen bleiben unverändert', async () => {
|
||||
standMitEinbau('off')
|
||||
const vorher = dokumente()
|
||||
for (const extra of [{}, { ersetzen: true, version: 1 }]) {
|
||||
const res = await vorlage({ branche: 'gesundheit', ...extra })
|
||||
assert.equal(res.status, 400, JSON.stringify(extra))
|
||||
assert.equal(res.body.code, 'gesundheitsbereich')
|
||||
}
|
||||
assert.equal(dokumente(), vorher)
|
||||
assert.equal(gespeicherterFragebogen().branche, 'salon')
|
||||
})
|
||||
|
||||
// ---------------------------------------------------------------- Zweimal / Zustandsmatrix
|
||||
|
||||
test('Vorlage zweimal => 409 einstellung_vorhanden, nichts überschrieben (Einstellung und Fragebogen); zwei gleichzeitige Anfragen: genau eine gewinnt', async () => {
|
||||
assert.equal((await vorlage({ branche: 'salon' })).status, 201)
|
||||
const einstellungVorher = termineConfigDoc().einstellung
|
||||
const zweite = await vorlage({ branche: 'tattoo' })
|
||||
assert.equal(zweite.status, 409)
|
||||
assert.equal(zweite.body.code, 'einstellung_vorhanden')
|
||||
assert.equal(zweite.body.error, 'Es gibt schon Einstellungen. Eine Vorlage kannst du nur beim ersten Öffnen wählen.')
|
||||
assert.equal(termineConfigDoc().einstellung, einstellungVorher)
|
||||
assert.equal(gespeicherterFragebogen().branche, 'salon')
|
||||
assert.equal(termineConfigDoc().version, 1)
|
||||
|
||||
await deleteDocument('termineConfig', FEATURE)
|
||||
const [a, b] = await Promise.all(['salon', 'kosmetik'].map((branche) => vorlage({ branche })))
|
||||
assert.deepEqual([a.status, b.status].sort(), [201, 409])
|
||||
const gewinner = a.status === 201 ? a : b
|
||||
assert.equal(gespeicherterFragebogen().branche, gewinner.body.fragebogen.branche, 'Einstellung und Fragebogen kommen vom selben Gewinner')
|
||||
assert.equal(gespeicherteEinstellung().vorlage, gewinner.body.einstellung.vorlage)
|
||||
})
|
||||
|
||||
for (const zustand of ZUSTAENDE) {
|
||||
for (const [zeile, auth] of ZEILEN) {
|
||||
const kundenansicht = zeile === 'Kundenansicht'
|
||||
const gesperrt = (zustand === 'ordered' || zustand === 'setup') && !kundenansicht
|
||||
const abgeschaltet = zustand === 'disabled'
|
||||
test(`Vorlage mit Branche: Zustand ${zustand}, ${zeile} => ${abgeschaltet ? '409 abgeschaltet' : gesperrt ? '409 gesperrt' : '201'}`, async () => {
|
||||
setzeZustand(zustand, { stand: null })
|
||||
const vorher = dokumente()
|
||||
const res = await vorlage({ branche: 'salon' }, auth())
|
||||
if (abgeschaltet || gesperrt) {
|
||||
assert.equal(res.status, 409, JSON.stringify(res.body))
|
||||
assert.equal(res.body.code, abgeschaltet ? 'abgeschaltet' : 'gesperrt')
|
||||
if (gesperrt) assert.equal(res.body.error, GESPERRT)
|
||||
assert.equal(dokumente(), vorher, 'nichts angelegt, nichts protokolliert')
|
||||
return
|
||||
}
|
||||
assert.equal(res.status, 201, JSON.stringify(res.body))
|
||||
assert.equal(gespeicherterFragebogen().branche, 'salon')
|
||||
assert.equal(termineConfigDoc().updatedBy, kundenansicht ? 'team:u_staff2' : 'kunde:c1')
|
||||
const feature = aw.docs('portalFeatures').get(FEATURE)
|
||||
assert.equal(feature.state, zustand === 'keins' ? 'off' : zustand, 'der Zustand des Funktionsdokuments ändert sich nie')
|
||||
assert.deepEqual(logEintraege().map((e) => [e.action, e.detail]), kundenansicht ? [['termin_einstellungen', 'Vorlage salon']] : [])
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
test('Vorlage mit Branche: ohne laufendes Wartungs-Abo 409 kein_abo und nichts angelegt; Abo nicht lesbar => 503, kein Rohtext', async () => {
|
||||
await deleteDocument('subscriptions', 'sub_h')
|
||||
const vorher = dokumente()
|
||||
const ohne = await vorlage({ branche: 'salon' })
|
||||
assert.equal(ohne.status, 409)
|
||||
assert.equal(ohne.body.code, 'kein_abo')
|
||||
assert.equal(ohne.body.error, aboMeldung(T, 'kein_abo'))
|
||||
assert.equal(dokumente(), vorher)
|
||||
|
||||
wartung()
|
||||
aw.failNext({ collection: 'subscriptions', status: 500, message: 'geheimer Appwrite-Text' })
|
||||
const fehler = await vorlage({ branche: 'salon' })
|
||||
assert.equal(fehler.status, 503)
|
||||
assert.equal(fehler.body.code, 'abo_pruefung')
|
||||
assert.ok(!JSON.stringify(fehler.body).includes('geheimer Appwrite-Text'))
|
||||
assert.equal(dokumente(), vorher)
|
||||
assert.equal((await vorlage({ branche: 'salon' })).status, 201, 'der nächste Versuch geht')
|
||||
})
|
||||
|
||||
test('Vorlage: fremdes Projekt (Kunde c2) => 403, ohne Sitzung 401, ohne projectId 400, Pilot-Schalter 409; nichts angelegt', async () => {
|
||||
const vorher = dokumente()
|
||||
const fremd = await vorlage({ branche: 'salon' }, { cookie: customerCookie('c2') })
|
||||
assert.equal(fremd.status, 403)
|
||||
assert.equal(fremd.body.error, 'Kein Zugriff auf dieses Projekt.')
|
||||
assert.equal((await send('POST', VORLAGE_URL, { body: { projectId: 'p1', branche: 'salon' } })).status, 401)
|
||||
assert.equal((await send('POST', VORLAGE_URL, { cookie, body: { branche: 'salon' } })).status, 400)
|
||||
config.termine.pilotEmails = ['andere@example.invalid']
|
||||
const gesperrt = await vorlage({ branche: 'salon' })
|
||||
assert.equal(gesperrt.status, 409)
|
||||
assert.equal(gesperrt.body.code, 'nicht_verfuegbar')
|
||||
assert.equal(dokumente(), vorher)
|
||||
})
|
||||
|
||||
// ---------------------------------------------------------------- Vorlage ersetzen (nur Entwurf in off)
|
||||
|
||||
test('Vorlage ersetzen in off: neue Leistungen, neue IDs und Feed-Token, Fragebogen neu mit der Branche, bestaetigt leer, einbau bleibt', async () => {
|
||||
const stand = standMitEinbau('off')
|
||||
const alt = gespeicherteEinstellung()
|
||||
assert.deepEqual(gespeicherterFragebogen().bestaetigt, [...SCHRITTE])
|
||||
assert.equal(gespeicherterFragebogen().preiseGeprueft, true)
|
||||
|
||||
const res = await vorlage({ branche: 'tattoo', ersetzen: true, version: 1 })
|
||||
assert.equal(res.status, 201, JSON.stringify(res.body))
|
||||
assert.equal(res.body.version, 2)
|
||||
assert.equal(res.body.einstellung.vorlage, 'tattoo')
|
||||
assert.deepEqual(res.body.einstellung.leistungen.map((l) => l.name), ['Beratungsgespräch', 'Kleines Tattoo', 'Tattoo-Session'])
|
||||
assert.ok(res.body.einstellung.ressourcen.every((r) => !('feedToken' in r)))
|
||||
assert.deepEqual(res.body.fragebogen, { ...leerFragebogen(), branche: 'tattoo', einbau: stand.fragebogen.einbau })
|
||||
assert.deepEqual(res.body.fragebogen.bestaetigt, [])
|
||||
assert.equal(res.body.fragebogen.preiseGeprueft, false)
|
||||
assert.equal(res.body.fragebogen.bundeslandGewaehlt, false)
|
||||
assert.deepEqual(res.body.fragebogen.einbau, EINBAU, 'die Einbauwünsche bleiben')
|
||||
|
||||
const neu = gespeicherteEinstellung()
|
||||
assert.equal(termineConfigDoc().version, 2)
|
||||
assert.deepEqual(gespeicherterFragebogen(), res.body.fragebogen, 'gespeichert, was die Antwort nennt')
|
||||
assert.ok(!ids(neu).some((id) => ids(alt).includes(id)), 'neue IDs für Personen und Leistungen')
|
||||
assert.ok(!feedTokens(neu).some((t) => feedTokens(alt).includes(t)), 'neue Kalender-Token')
|
||||
assert.ok(feedTokens(neu).every((t) => t.length === 43))
|
||||
assert.equal(termineConfigDoc().updatedBy, 'kunde:c1')
|
||||
assert.equal(aw.docs('portalFeatures').get(FEATURE).state, 'off')
|
||||
assert.deepEqual(res.body.fehlt, [])
|
||||
})
|
||||
|
||||
test('Vorlage ersetzen: Einstellung und Fragebogen werden in einem Schreibvorgang gespeichert (genau ein Schreibzugriff auf termineConfig)', async () => {
|
||||
standMitEinbau('off')
|
||||
// der zweite passende Schreibzugriff scheitert: gäbe es einen zweiten, wäre die Antwort kein 201
|
||||
aw.failNext({ collection: 'termineConfig', method: 'PATCH', status: 500, after: 1 })
|
||||
const res = await vorlage({ branche: 'kosmetik', ersetzen: true, version: 1 })
|
||||
assert.equal(res.status, 201, JSON.stringify(res.body))
|
||||
assert.equal(gespeicherterFragebogen().branche, 'kosmetik')
|
||||
assert.equal(gespeicherteEinstellung().vorlage, 'kosmetik')
|
||||
// den eingeplanten Fehler verbrauchen: der nächste Schreibzugriff scheitert (500 ohne Rohtext), der Stand bleibt
|
||||
const stand = termineConfigDoc()
|
||||
const verbraucht = await putFragebogen({ ...res.body.fragebogen, bestaetigt: ['branche'] })
|
||||
assert.equal(verbraucht.status, 500)
|
||||
assert.deepEqual(termineConfigDoc(), stand)
|
||||
})
|
||||
|
||||
test('Vorlage ersetzen mit vorlage statt branche: der Fragebogen nennt den Schlüssel der Vorlage als Branche; betrieb überschreibt', async () => {
|
||||
standMitEinbau('off')
|
||||
const res = await vorlage({ vorlage: 'fotograf', ersetzen: true, version: 1, betrieb: { name: 'Foto Neu' } })
|
||||
assert.equal(res.status, 201, JSON.stringify(res.body))
|
||||
assert.equal(res.body.fragebogen.branche, 'fotograf')
|
||||
assert.equal(res.body.einstellung.betrieb.name, 'Foto Neu')
|
||||
assert.deepEqual(res.body.fragebogen.einbau, EINBAU)
|
||||
})
|
||||
|
||||
test('Vorlage ersetzen ohne gültige version => 400, mit falscher version => 409 EINSTELLUNG_VERALTET; nichts geändert', async () => {
|
||||
standMitEinbau('off')
|
||||
const vorher = dokumente()
|
||||
for (const version of [undefined, null, '1', 1.5, -1, true, {}]) {
|
||||
const res = await vorlage({ branche: 'tattoo', ersetzen: true, ...(version === undefined ? {} : { version }) })
|
||||
assert.equal(res.status, 400, String(version))
|
||||
assert.ok(res.body.fehler.version, String(version))
|
||||
}
|
||||
for (const version of [0, 2, 9]) {
|
||||
const res = await vorlage({ branche: 'tattoo', ersetzen: true, version })
|
||||
assert.equal(res.status, 409, String(version))
|
||||
assert.equal(res.body.code, 'EINSTELLUNG_VERALTET')
|
||||
}
|
||||
assert.equal(dokumente(), vorher)
|
||||
})
|
||||
|
||||
test('Vorlage mit Einstellung ohne ersetzen (auch mit version) => 409 einstellung_vorhanden; ersetzen nur mit true', async () => {
|
||||
standMitEinbau('off')
|
||||
const vorher = dokumente()
|
||||
for (const extra of [{}, { version: 1 }, { ersetzen: false, version: 1 }, { ersetzen: 'true', version: 1 }, { ersetzen: 1, version: 1 }]) {
|
||||
const res = await vorlage({ branche: 'tattoo', ...extra })
|
||||
assert.equal(res.status, 409, JSON.stringify(extra))
|
||||
assert.equal(res.body.code, 'einstellung_vorhanden', JSON.stringify(extra))
|
||||
}
|
||||
assert.equal(dokumente(), vorher)
|
||||
})
|
||||
|
||||
for (const zustand of ['active', 'ordered', 'setup']) {
|
||||
test(`Vorlage ersetzen in ${zustand} => 409 nicht_ersetzbar (auch mit richtiger version), Dokumente unverändert`, async () => {
|
||||
standMitEinbau(zustand)
|
||||
const vorher = dokumente()
|
||||
// ordered und setup schreibt nur die Kundenansicht; dort gilt dieselbe Regel
|
||||
for (const auth of zustand === 'active' ? [{ cookie }, kv()] : [kv()]) {
|
||||
const res = await vorlage({ branche: 'tattoo', ersetzen: true, version: 1 }, auth)
|
||||
assert.equal(res.status, 409, JSON.stringify(res.body))
|
||||
assert.equal(res.body.code, 'nicht_ersetzbar')
|
||||
assert.equal(res.body.error, 'Die Vorlage kannst du nur wechseln, solange du den Terminplaner noch nicht bestellt hast.')
|
||||
}
|
||||
assert.equal(dokumente(), vorher)
|
||||
})
|
||||
}
|
||||
|
||||
test('Vorlage ersetzen in disabled => 409 abgeschaltet, Dokumente unverändert', async () => {
|
||||
standMitEinbau('disabled')
|
||||
const vorher = dokumente()
|
||||
const res = await vorlage({ branche: 'tattoo', ersetzen: true, version: 1 })
|
||||
assert.equal(res.status, 409)
|
||||
assert.equal(res.body.code, 'abgeschaltet')
|
||||
assert.equal(dokumente(), vorher)
|
||||
})
|
||||
|
||||
test('Vorlage ersetzen in der Kundenansicht (off): erlaubt, protokolliert ohne Betriebsdaten, updatedBy ist der Mitarbeiter', async () => {
|
||||
standMitEinbau('off')
|
||||
const res = await vorlage({ branche: 'tattoo', ersetzen: true, version: 1, betrieb: { name: 'Geheimer Betrieb' } }, kv())
|
||||
assert.equal(res.status, 201, JSON.stringify(res.body))
|
||||
assert.equal(termineConfigDoc().updatedBy, 'team:u_staff2')
|
||||
assert.deepEqual(logEintraege().map((e) => [e.action, e.detail]), [['termin_einstellungen', 'Vorlage tattoo ersetzt']])
|
||||
assert.ok(!JSON.stringify(logEintraege()).includes('Geheimer Betrieb'))
|
||||
})
|
||||
|
||||
test('Vorlage ersetzen bei unlesbarer Einstellung (off): mit der Version des Dokuments geht es, ohne ersetzen bleibt es 409', async () => {
|
||||
aw.seed('portalFeatures', featureDoc('off'))
|
||||
aw.seed('termineConfig', { $id: FEATURE, customerId: 'c1', projectId: 'p1', einstellung: '{kaputt', version: 3 })
|
||||
const ohne = await vorlage({ branche: 'salon' })
|
||||
assert.equal(ohne.status, 409)
|
||||
assert.equal(ohne.body.code, 'einstellung_vorhanden')
|
||||
assert.equal(termineConfigDoc().einstellung, '{kaputt')
|
||||
const mit = await vorlage({ branche: 'salon', ersetzen: true, version: 3 })
|
||||
assert.equal(mit.status, 201, JSON.stringify(mit.body))
|
||||
assert.equal(mit.body.version, 4)
|
||||
assert.equal(gespeicherteEinstellung().vorlage, 'salon')
|
||||
assert.equal(gespeicherterFragebogen().branche, 'salon')
|
||||
})
|
||||
|
||||
// ---------------------------------------------------------------- PUT /fragebogen
|
||||
|
||||
const beispielFragebogen = (over = {}) => ({
|
||||
...leerFragebogen(),
|
||||
branche: 'salon',
|
||||
bestaetigt: ['branche', 'wer'],
|
||||
bundeslandGewaehlt: true,
|
||||
einbau: { modus: 'inline', knopfText: '', stelle: 'Auf der Startseite', gestaltung: '', bemerkung: '' },
|
||||
...over,
|
||||
})
|
||||
|
||||
test('PUT fragebogen speichert nur den Fragebogen: version und einstellung der Einstellung bleiben, updatedAt/updatedBy ändern sich, Antwort { fragebogen, offen }', async () => {
|
||||
setzeZustand('off')
|
||||
const vorher = termineConfigDoc()
|
||||
const res = await putFragebogen(beispielFragebogen())
|
||||
assert.equal(res.status, 200, JSON.stringify(res.body))
|
||||
assert.deepEqual(Object.keys(res.body).sort(), ['fragebogen', 'offen'])
|
||||
assert.deepEqual(res.body.fragebogen, beispielFragebogen())
|
||||
assert.deepEqual(gespeicherterFragebogen(), beispielFragebogen())
|
||||
const danach = termineConfigDoc()
|
||||
assert.equal(danach.version, vorher.version, 'die Version der Einstellung bleibt')
|
||||
assert.equal(danach.einstellung, vorher.einstellung, 'die Einstellung bleibt byte-gleich')
|
||||
assert.equal(danach.updatedBy, 'kunde:c1')
|
||||
assert.equal(new Date(danach.updatedAt).toISOString(), JETZT.toISOString(), 'updatedAt ist die Uhrzeit der Anfrage')
|
||||
assert.equal(aw.docs('portalFeatures').get(FEATURE).state, 'off')
|
||||
assert.equal(logEintraege().length, 0, 'kein Eintrag im Protokoll der Kundenansicht')
|
||||
// GET liest den neuen Stand
|
||||
assert.deepEqual((await send('GET', einstellungenUrl(), { cookie })).body.fragebogen, beispielFragebogen())
|
||||
})
|
||||
|
||||
test('PUT fragebogen: bereinigt die Eingabe wie normalisiereFragebogen (Reihenfolge, Doppelte, Fremdes, Steuerzeichen) und ersetzt den ganzen Fragebogen', async () => {
|
||||
setzeZustand('off')
|
||||
const res = await putFragebogen({
|
||||
branche: 'salon',
|
||||
bestaetigt: ['website', 'branche', 'branche', 'unbekannt'],
|
||||
einbau: { modus: 'knopf', knopfText: 'Jetzt\u0000 buchen', stelle: 'x', ignoriert: 'y' },
|
||||
boese: true,
|
||||
})
|
||||
assert.equal(res.status, 200, JSON.stringify(res.body))
|
||||
assert.deepEqual(res.body.fragebogen.bestaetigt, ['branche', 'website'])
|
||||
assert.equal(res.body.fragebogen.einbau.knopfText, 'Jetzt buchen')
|
||||
assert.ok(!('boese' in res.body.fragebogen) && !('ignoriert' in res.body.fragebogen.einbau))
|
||||
assert.equal(res.body.fragebogen.preiseGeprueft, false, 'was fehlt, ist leer (PUT ersetzt, ergänzt nicht)')
|
||||
assert.deepEqual(gespeicherterFragebogen(), res.body.fragebogen)
|
||||
})
|
||||
|
||||
test('PUT fragebogen: ein offener Editor mit der alten version kann danach weiter per PATCH speichern (kein 409), auch zwei Tabs', async () => {
|
||||
const stand = setzeZustand('off')
|
||||
const geladen = (await send('GET', einstellungenUrl(), { cookie })).body // Tab A und Tab B haben version 1
|
||||
assert.equal(geladen.version, 1)
|
||||
for (let i = 0; i < 3; i += 1) assert.equal((await putFragebogen(beispielFragebogen({ bestaetigt: SCHRITTE.slice(0, i + 1) }))).status, 200)
|
||||
assert.equal(termineConfigDoc().version, 1)
|
||||
const tabA = await patch({ version: geladen.version, einstellung: neueEinstellung(stand) })
|
||||
assert.equal(tabA.status, 200, JSON.stringify(tabA.body))
|
||||
assert.equal(tabA.body.version, 2)
|
||||
assert.equal((await putFragebogen(beispielFragebogen({ bestaetigt: [...SCHRITTE] }))).status, 200, 'Tab B bestätigt weiter')
|
||||
const tabB = await patch({ version: 2, einstellung: neueEinstellung(stand, (e) => ({ ...e, betrieb: { ...e.betrieb, name: 'Salon B' } })) })
|
||||
assert.equal(tabB.status, 200, JSON.stringify(tabB.body))
|
||||
assert.equal(gespeicherteEinstellung().betrieb.name, 'Salon B')
|
||||
assert.deepEqual(gespeicherterFragebogen().bestaetigt, [...SCHRITTE], 'der PATCH hat den Fragebogen nicht angefasst')
|
||||
})
|
||||
|
||||
test('PUT fragebogen mit ungültigem Modus => 400 mit fehler[einbau.modus]; ungültige Formen => 400; gespeichert wird nichts', async () => {
|
||||
setzeZustand('off')
|
||||
const vorher = dokumente()
|
||||
const modus = await putFragebogen(beispielFragebogen({ einbau: { modus: 'popup' } }))
|
||||
assert.equal(modus.status, 400, JSON.stringify(modus.body))
|
||||
assert.equal(modus.body.fehler['einbau.modus'], 'Bitte wähle, ob der Terminplaner im Text oder als Knopf erscheint.')
|
||||
assert.equal(modus.body.error, modus.body.fehler['einbau.modus'])
|
||||
for (const eingabe of [undefined, null, 'x', 5, [], { branche: 'gibtsnicht' }, { bestaetigt: 'branche' }, { preiseGeprueft: 'ja' }]) {
|
||||
const res = await send('PUT', FRAGEBOGEN_URL, { cookie, body: { projectId: 'p1', ...(eingabe === undefined ? {} : { fragebogen: eingabe }) } })
|
||||
assert.equal(res.status, 400, JSON.stringify(eingabe))
|
||||
assert.ok(res.body.fehler && Object.keys(res.body.fehler).length > 0, JSON.stringify(eingabe))
|
||||
}
|
||||
assert.equal(dokumente(), vorher)
|
||||
})
|
||||
|
||||
test('PUT fragebogen ohne Einstellung => 409 keine_einstellung, nichts angelegt (kein Funktionsdokument, kein termineConfig)', async () => {
|
||||
// kein Funktionsdokument
|
||||
const vorher = dokumente()
|
||||
const ohneDokument = await putFragebogen(beispielFragebogen())
|
||||
assert.equal(ohneDokument.status, 409)
|
||||
assert.equal(ohneDokument.body.code, 'keine_einstellung')
|
||||
assert.equal(dokumente(), vorher)
|
||||
assert.equal(aw.docs('portalFeatures').size, 0)
|
||||
// Funktionsdokument ohne termineConfig
|
||||
setzeZustand('off', { stand: null })
|
||||
const ohneKonfig = await putFragebogen(beispielFragebogen())
|
||||
assert.equal(ohneKonfig.status, 409)
|
||||
assert.equal(ohneKonfig.body.code, 'keine_einstellung')
|
||||
assert.equal(aw.docs('termineConfig').size, 0)
|
||||
})
|
||||
|
||||
for (const zustand of ZUSTAENDE) {
|
||||
const erwartet = { keins: '409 keine_einstellung', off: '200', ordered: '409 gesperrt', setup: '409 gesperrt', active: '200', disabled: '409 abgeschaltet' }[zustand]
|
||||
test(`PUT fragebogen: Zustand ${zustand} => ${erwartet}`, async () => {
|
||||
setzeZustand(zustand)
|
||||
const vorher = dokumente()
|
||||
const res = await putFragebogen(beispielFragebogen())
|
||||
if (erwartet === '200') {
|
||||
assert.equal(res.status, 200, JSON.stringify(res.body))
|
||||
assert.deepEqual(gespeicherterFragebogen(), beispielFragebogen())
|
||||
assert.equal(aw.docs('portalFeatures').get(FEATURE).state, zustand)
|
||||
return
|
||||
}
|
||||
assert.equal(res.status, 409, JSON.stringify(res.body))
|
||||
assert.equal(res.body.code, erwartet.split(' ')[1])
|
||||
if (zustand === 'ordered' || zustand === 'setup') assert.equal(res.body.error, GESPERRT)
|
||||
if (zustand === 'disabled') assert.match(res.body.error, /Wartungs-Abo beendet/)
|
||||
assert.equal(dokumente(), vorher, 'nichts geändert')
|
||||
})
|
||||
}
|
||||
|
||||
test('PUT fragebogen: in off nur mit laufendem Wartungs-Abo (409 kein_abo, 503 bei Lesefehler); in active ohne Abo-Abfrage', async () => {
|
||||
setzeZustand('off')
|
||||
await deleteDocument('subscriptions', 'sub_h')
|
||||
const vorher = dokumente()
|
||||
const ohne = await putFragebogen(beispielFragebogen())
|
||||
assert.equal(ohne.status, 409)
|
||||
assert.equal(ohne.body.code, 'kein_abo')
|
||||
assert.equal(ohne.body.error, aboMeldung(T, 'kein_abo'))
|
||||
assert.equal(dokumente(), vorher)
|
||||
|
||||
wartung()
|
||||
aw.failNext({ collection: 'subscriptions', status: 500, message: 'geheimer Appwrite-Text' })
|
||||
const fehler = await putFragebogen(beispielFragebogen())
|
||||
assert.equal(fehler.status, 503)
|
||||
assert.equal(fehler.body.code, 'abo_pruefung')
|
||||
assert.ok(!JSON.stringify(fehler.body).includes('geheimer Appwrite-Text'))
|
||||
assert.equal(dokumente(), vorher)
|
||||
|
||||
await deleteDocument('subscriptions', 'sub_h')
|
||||
await deleteDocument('portalFeatures', FEATURE)
|
||||
await deleteDocument('termineConfig', FEATURE)
|
||||
setzeZustand('active')
|
||||
aw.resetListCalls()
|
||||
assert.equal((await putFragebogen(beispielFragebogen())).status, 200, 'active läuft auch ohne Abo weiter (Abgleich: Block E)')
|
||||
assert.deepEqual(aw.listCalls('subscriptions'), [])
|
||||
})
|
||||
|
||||
test('PUT fragebogen: offen ist die Bestellprüfung des gespeicherten Stands (nur in off), null in active', async () => {
|
||||
const stand = setzeZustand('off')
|
||||
const unvollstaendig = await putFragebogen(beispielFragebogen())
|
||||
assert.equal(unvollstaendig.status, 200)
|
||||
const erwartet = bestellpruefung({ einstellung: stand.einstellung, fragebogen: unvollstaendig.body.fragebogen, feature: T, abo: { ok: true } })
|
||||
assert.ok(erwartet.length > 0)
|
||||
assert.deepEqual(unvollstaendig.body.offen, erwartet, 'dieselbe Liste wie im GET')
|
||||
assert.ok(unvollstaendig.body.offen.some((p) => p.schritt === 'leistungen'), 'noch nicht bestätigte Schritte stehen drin')
|
||||
|
||||
const vollstaendig = await putFragebogen({ ...stand.fragebogen })
|
||||
assert.equal(vollstaendig.status, 200)
|
||||
assert.deepEqual(vollstaendig.body.offen, [], 'alles bestätigt und bestellbar')
|
||||
assert.deepEqual((await send('GET', einstellungenUrl(), { cookie })).body.offen, [])
|
||||
|
||||
await deleteDocument('portalFeatures', FEATURE)
|
||||
await deleteDocument('termineConfig', FEATURE)
|
||||
setzeZustand('active')
|
||||
assert.equal((await putFragebogen(beispielFragebogen())).body.offen, null)
|
||||
})
|
||||
|
||||
test('PUT fragebogen: Branche gesundheit lässt sich speichern, das Gate (offen) blockt sie aber', async () => {
|
||||
setzeZustand('off')
|
||||
const res = await putFragebogen({ ...bestellbarerStand({ now: JETZT }).fragebogen, branche: 'gesundheit' })
|
||||
assert.equal(res.status, 200, JSON.stringify(res.body))
|
||||
assert.ok(res.body.offen.some((p) => p.schritt === 'branche'))
|
||||
})
|
||||
|
||||
// ---------------------------------------------------------------- PUT: Kundenansicht
|
||||
|
||||
for (const zustand of ZUSTAENDE) {
|
||||
test(`PUT fragebogen in der Kundenansicht (${zustand}) => 403 view_blocked über den Wächter; nichts geändert, nichts protokolliert`, async () => {
|
||||
setzeZustand(zustand)
|
||||
const vorher = dokumente()
|
||||
const res = await putFragebogen(beispielFragebogen(), kv())
|
||||
assert.equal(res.status, 403, JSON.stringify(res.body))
|
||||
assert.equal(res.body.code, 'view_blocked')
|
||||
assert.equal(dokumente(), vorher)
|
||||
})
|
||||
|
||||
test(`PUT fragebogen in der Kundenansicht (${zustand}) => 403 mit Text direkt am Router (ohne Wächter); vor jeder Zustandsprüfung, nichts geändert`, async () => {
|
||||
setzeZustand(zustand)
|
||||
const vorher = dokumente()
|
||||
const res = await sendDirekt('PUT', FRAGEBOGEN_URL, { cookie: kundenansichtCookie(), body: { projectId: 'p1', fragebogen: beispielFragebogen() } })
|
||||
assert.equal(res.status, 403, JSON.stringify(res.body))
|
||||
assert.equal(res.body.error, KUNDENANSICHT_TEXT)
|
||||
assert.equal(dokumente(), vorher)
|
||||
})
|
||||
}
|
||||
|
||||
test('PUT fragebogen am Router: dieselbe Anfrage mit der Sitzung des Kunden selbst geht durch (der Router sperrt nur die Kundenansicht)', async () => {
|
||||
setzeZustand('off')
|
||||
const res = await sendDirekt('PUT', FRAGEBOGEN_URL, { cookie, body: { projectId: 'p1', fragebogen: beispielFragebogen() } })
|
||||
assert.equal(res.status, 200, JSON.stringify(res.body))
|
||||
assert.deepEqual(gespeicherterFragebogen(), beispielFragebogen())
|
||||
})
|
||||
|
||||
test('PUT fragebogen am Router: auch eine alte Ansicht (viewAs ohne viewId) und die Ansicht eines anderen Kunden schreiben nichts', async () => {
|
||||
setzeZustand('off')
|
||||
const vorher = dokumente()
|
||||
for (const c of [viewAsCookie('c1'), kundenansichtCookie({ customerId: 'c2' })]) {
|
||||
const res = await sendDirekt('PUT', FRAGEBOGEN_URL, { cookie: c, body: { projectId: 'p1', fragebogen: beispielFragebogen() } })
|
||||
assert.ok([401, 403].includes(res.status), String(res.status))
|
||||
}
|
||||
assert.equal(dokumente(), vorher)
|
||||
})
|
||||
|
||||
// ---------------------------------------------------------------- PUT: Mandanten und Zugang
|
||||
|
||||
test('PUT fragebogen: Kunde c2 mit dem Projekt von c1 => 403 in jedem Zustand; nichts angelegt, nichts geändert', async () => {
|
||||
for (const zustand of ZUSTAENDE) {
|
||||
await deleteDocument('portalFeatures', FEATURE).catch(() => {})
|
||||
await deleteDocument('termineConfig', FEATURE).catch(() => {})
|
||||
setzeZustand(zustand)
|
||||
const vorher = dokumente()
|
||||
const res = await putFragebogen(beispielFragebogen(), { cookie: customerCookie('c2') })
|
||||
assert.equal(res.status, 403, zustand)
|
||||
assert.equal(res.body.error, 'Kein Zugriff auf dieses Projekt.')
|
||||
assert.equal(dokumente(), vorher, zustand)
|
||||
}
|
||||
})
|
||||
|
||||
test('PUT fragebogen: ohne Sitzung 401, ohne projectId 400, unbrauchbare projectId 403, Pilot-Schalter 409', async () => {
|
||||
setzeZustand('off')
|
||||
const vorher = dokumente()
|
||||
assert.equal((await send('PUT', FRAGEBOGEN_URL, { body: { projectId: 'p1', fragebogen: beispielFragebogen() } })).status, 401)
|
||||
assert.equal((await send('PUT', FRAGEBOGEN_URL, { cookie, body: { fragebogen: beispielFragebogen() } })).status, 400)
|
||||
assert.equal((await putFragebogen(beispielFragebogen(), { cookie }, { projectId: 'gibt es nicht' })).status, 403)
|
||||
config.termine.pilotEmails = ['andere@example.invalid']
|
||||
const gesperrt = await putFragebogen(beispielFragebogen())
|
||||
assert.equal(gesperrt.status, 409)
|
||||
assert.equal(gesperrt.body.code, 'nicht_verfuegbar')
|
||||
config.termine.pilotEmails = ['*']
|
||||
config.termine.mode = 'off'
|
||||
assert.equal((await putFragebogen(beispielFragebogen())).status, 409)
|
||||
assert.equal(dokumente(), vorher)
|
||||
})
|
||||
|
||||
test('PUT fragebogen und Vorlage: die Antworten sind nie zwischenspeicherbar', async () => {
|
||||
setzeZustand('off')
|
||||
const res = await putFragebogen(beispielFragebogen())
|
||||
assert.equal(res.headers.get('cache-control'), 'no-store')
|
||||
assert.equal((await vorlage({ branche: 'gesundheit' })).headers.get('cache-control'), 'no-store')
|
||||
})
|
||||
|
||||
// ---------------------------------------------------------------- Schreibbudget
|
||||
|
||||
test('Schreibbudget: ein kompletter Durchlauf (1 Vorlage, 8 PATCH, 8 PUT, 5 Editor-Speicherungen) bleibt unter 120 je Stunde, der 121. Aufruf ist 429', async () => {
|
||||
const vorlageErgebnis = await vorlage({ branche: 'salon' })
|
||||
assert.equal(vorlageErgebnis.status, 201, JSON.stringify(vorlageErgebnis.body))
|
||||
let version = vorlageErgebnis.body.version
|
||||
let einstellung = vorlageErgebnis.body.einstellung
|
||||
let aufrufe = 1
|
||||
|
||||
const speichern = async (name) => {
|
||||
einstellung = { ...einstellung, betrieb: { ...einstellung.betrieb, name } }
|
||||
const res = await patch({ version, einstellung })
|
||||
aufrufe += 1
|
||||
assert.equal(res.status, 200, `Aufruf ${aufrufe}: ${JSON.stringify(res.body)}`)
|
||||
version = res.body.version
|
||||
einstellung = res.body.einstellung
|
||||
}
|
||||
for (let i = 0; i < 8; i += 1) await speichern(`Salon Schritt ${i + 1}`) // je Schritt ein PATCH ...
|
||||
for (let i = 0; i < 8; i += 1) { // ... und ein PUT mit dem Haken
|
||||
const res = await putFragebogen(beispielFragebogen({ bestaetigt: SCHRITTE.slice(0, i + 1) }))
|
||||
aufrufe += 1
|
||||
assert.equal(res.status, 200, `Aufruf ${aufrufe}: ${JSON.stringify(res.body)}`)
|
||||
}
|
||||
for (let i = 0; i < 5; i += 1) await speichern(`Salon Editor ${i + 1}`) // fünf Speicherungen im Editor
|
||||
assert.equal(aufrufe, 22)
|
||||
|
||||
// bis 120 Aufrufe geht alles durch ...
|
||||
while (aufrufe < 120) {
|
||||
const res = await putFragebogen(beispielFragebogen())
|
||||
aufrufe += 1
|
||||
assert.equal(res.status, 200, `Aufruf ${aufrufe}`)
|
||||
}
|
||||
// ... der 121. ist 429, gleich welche Schreib-Route (PUT, PATCH, Vorlage teilen sich das Budget des Kunden)
|
||||
const vorher = dokumente()
|
||||
for (const res of [
|
||||
await putFragebogen(beispielFragebogen()),
|
||||
await patch({ version, einstellung }),
|
||||
await vorlage({ branche: 'tattoo', ersetzen: true, version }),
|
||||
]) {
|
||||
assert.equal(res.status, 429)
|
||||
assert.equal(res.body.error, 'Zu viele Änderungen in kurzer Zeit. Bitte versuch es in einer Stunde wieder.')
|
||||
}
|
||||
assert.equal(dokumente(), vorher, 'abgewiesen heißt nichts geschrieben')
|
||||
assert.equal((await send('GET', einstellungenUrl(), { cookie })).status, 200, 'Lesen bleibt frei')
|
||||
// eine Stunde später ist das Budget wieder da
|
||||
setzeUhrFuerTests(() => new Date(JETZT.getTime() + 61 * 60 * 1000))
|
||||
assert.equal((await putFragebogen(beispielFragebogen())).status, 200)
|
||||
})
|
||||
@@ -246,7 +246,7 @@ test('ohne Pilot (Kunde nicht in der Liste, Modus off) 409 "noch nicht verfuegba
|
||||
assert.equal((await send('GET', einstellungenUrl(), { cookie })).status, 409)
|
||||
})
|
||||
|
||||
test('Funktion aus, noch nicht bestellt oder ohne Dokument: Einstellungen lesen geht (Entwurf), Vorlage, Feed-Link, Buchungen und Zeiten bleiben 409 "nicht aktiv"', async () => {
|
||||
test('Funktion aus, noch nicht bestellt oder ohne Dokument: Einstellungen lesen geht (Entwurf), Feed-Link, Buchungen und Zeiten bleiben 409 "nicht aktiv"', async () => {
|
||||
// Lesen der Einstellungen: in jedem Zustand und auch ohne Dokument (Zustandsmatrix: test/termineFragebogenRoutes.test.js)
|
||||
for (const projectId of ['p2', 'p5']) {
|
||||
const lesen = await send('GET', einstellungenUrl(projectId), { cookie })
|
||||
@@ -254,12 +254,11 @@ test('Funktion aus, noch nicht bestellt oder ohne Dokument: Einstellungen lesen
|
||||
assert.equal(lesen.body.state, 'off', projectId)
|
||||
assert.equal(lesen.body.einstellung, null, projectId)
|
||||
assert.equal(lesen.body.version, 0, projectId)
|
||||
// alles andere (PATCH der Einstellungen im Entwurf: Schreibregeln in test/termineFragebogenRoutes.test.js, Teil 1b)
|
||||
// alles andere (PATCH der Einstellungen und POST der Vorlage im Entwurf: Schreibregeln in test/termineFragebogenRoutes.test.js, Teile 1b und 2)
|
||||
for (const [method, pfad, body] of [
|
||||
['GET', `/api/termine/buchungen?projectId=${projectId}&von=2026-10-01&bis=2026-10-31`],
|
||||
['GET', `/api/termine/zeiten?projectId=${projectId}`],
|
||||
['GET', `/api/termine/export.csv?projectId=${projectId}`],
|
||||
['POST', '/api/termine/einstellungen/vorlage', { projectId, vorlage: 'salon' }],
|
||||
['POST', `/api/termine/ressourcen/${R1}/feed`, { projectId }],
|
||||
['POST', '/api/termine/buchungen', { projectId, art: 'sperre' }],
|
||||
]) {
|
||||
|
||||
Reference in new issue
Block a user