import { test, after, beforeEach, afterEach, mock } from 'node:test' import assert from 'node:assert/strict' import { startAppwriteMock } from './support/appwriteMock.js' import { schemaKeys } from './support/schema.js' import { startFakeStripe } from './support/fakeStripe.js' import { useTestEnv } from './support/testEnv.js' import { TERMINE_DATETIMES } from '../server/services/termine/schema.js' // Entwurfszugriff der Kunden-Schnittstelle /api/termine (TP2, Task C4a): GET /einstellungen liest in JEDEM Zustand des // Funktionsdokuments (off, ordered, setup, active, disabled) und auch ohne Dokument (virtueller Kontext off, nichts wird // angelegt); alle anderen Routen behalten die Schranke active|disabled. Dazu die neuen GET-Felder (branchen, fragebogen, // 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. const aw = await startAppwriteMock({ schema: { portalFeatures: schemaKeys('portalFeatures'), termineConfig: schemaKeys('termineConfig'), termineBookings: schemaKeys('termineBookings'), termineClaims: schemaKeys('termineClaims'), termineOutbox: schemaKeys('termineOutbox'), subscriptions: schemaKeys('subscriptions'), contractEvents: schemaKeys('contractEvents'), featureConsents: schemaKeys('featureConsents'), customerViewLog: schemaKeys('customerViewLog'), }, datetimes: TERMINE_DATETIMES, defaultLimit: 25, }) const st = await startFakeStripe() 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 { bestellbarerStand, entwurfDoc } = await import('./support/fragebogenStand.js') const { deleteDocument, updateDocument } = await import('../server/services/appwriteAdmin.js') const { kundenansichtGuard } = await import('../server/middleware/kundenansichtGuard.js') const { default: termineRoutes, setzeUhrFuerTests, resetTermineRoutesForTests } = await import('../server/routes/termine.js') 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 { aboMeldung } = await import('../server/services/featureOrder.js') const app = await startTestApp({ '/': kundenansichtGuard, '/api/public/termine/v1': publicTermineRoutes, '/api/termine': termineRoutes, }) after(() => Promise.all([app.close(), aw.close(), st.close()])) const T = config.features.termine const JETZT = new Date('2026-10-05T08:00:00.000Z') const MODUS_VORHER = config.termine.mode const MAILS_VORHER = [...config.termine.pilotEmails] const MIN_ANTWORT_VORHER = config.termine.minAntwortMs const VIEW = 'a'.repeat(32) const SITE_KEY = 'abcdefghijklmnop' const FEATURE = 'f_p1_termine' const NICHT_AKTIV = 'Der Terminplaner ist für diese Website nicht aktiv.' const cookie = customerCookie('c1') const kv = (extra = {}) => ({ cookie: kundenansichtCookie(extra), headers: { 'x-kundenansicht': extra.viewId || VIEW } }) /** Zwei Zeilen der Zustandsmatrix: der Kunde selbst und ein Mitarbeiter in der Kundenansicht. */ const ZEILEN = [ ['Kunde', () => ({ cookie })], ['Kundenansicht', () => kv()], ] const ZUSTAENDE = ['keins', 'off', 'ordered', 'setup', 'active', 'disabled'] const wartung = (patch = {}) => aw.seed('subscriptions', subscriptionDoc('sub_h', { ...wartungDoc, ...patch })) const einstellungenUrl = (projectId = 'p1') => `/api/termine/einstellungen?projectId=${projectId}` const featureDoc = (state, over = {}) => ({ $id: FEATURE, customerId: 'c1', projectId: 'p1', featureKey: 'termine', state, enabled: state === 'active', siteKey: SITE_KEY, ...(state === 'active' ? { activatedAt: '2026-10-01T09:00:00.000Z' } : {}), ...(state === 'disabled' ? { disabledAt: '2026-10-04T10:00:00.000Z', disabledReason: 'abo_ende' } : {}), ...over, }) const groessen = () => ({ portalFeatures: aw.docs('portalFeatures').size, termineConfig: aw.docs('termineConfig').size, featureConsents: aw.docs('featureConsents').size, customerViewLog: aw.docs('customerViewLog').size, }) /** Zustand des Funktionsdokuments von Projekt p1: 'keins' = kein Dokument. Mit Entwurf (Einstellung und Fragebogen) in termineConfig. */ function setzeZustand(zustand, { stand = bestellbarerStand({ now: JETZT }), over = {} } = {}) { if (zustand === 'keins') return null aw.seed('portalFeatures', featureDoc(zustand, over)) if (stand) aw.seed('termineConfig', entwurfDoc(FEATURE, { customerId: 'c1', projectId: 'p1', stand })) return stand } beforeEach(async () => { for (const collection of ['portalFeatures', 'termineConfig', 'termineBookings', 'featureConsents', 'customerViewLog', 'subscriptions', 'contractEvents', 'customers', 'websiteProjects']) { for (const id of aw.docs(collection).keys()) await deleteDocument(collection, id) } resetKontextForTests() resetTermineRoutesForTests() resetPublicTermineForTests() resetTermineLimitsForTests() setzeUhrFuerTests(() => JETZT) config.termine.mode = 'pilot' config.termine.pilotEmails = ['*'] config.termine.minAntwortMs = 0 mock.method(console, 'error', () => {}) mock.method(console, 'warn', () => {}) seedFixtures(aw) aw.seed('customers', { ...customers.c1, companyName: 'Salon Müller', street: 'Hauptstr. 1', postalCode: '67722', city: 'Winnweiler', phone: '06301 1234' }) wartung() aw.seed('websiteProjects', { $id: 'p1', customerId: 'c1', projectName: 'Salon', subdomain: 'salon' }) aw.seed('websiteProjects', { $id: 'p3', customerId: 'c2', projectName: 'Fremd', subdomain: 'fremd' }) }) afterEach(() => { mock.restoreAll() config.termine.mode = MODUS_VORHER config.termine.pilotEmails = [...MAILS_VORHER] config.termine.minAntwortMs = MIN_ANTWORT_VORHER }) // ---------------------------------------------------------------- Zustandsmatrix: GET liest in jedem Zustand for (const zustand of ZUSTAENDE) { for (const [zeile, auth] of ZEILEN) { test(`GET einstellungen: Zustand ${zustand}, ${zeile} => 200 mit Zustand, Entwurf und Fragebogen; nichts wird angelegt`, async () => { const stand = setzeZustand(zustand) const vorher = groessen() const res = await send('GET', einstellungenUrl(), auth()) assert.equal(res.status, 200, JSON.stringify(res.body)) const b = res.body assert.equal(b.state, zustand === 'keins' ? 'off' : zustand) assert.equal(b.kundenansicht, zeile === 'Kundenansicht') if (stand) { assert.equal(b.version, 1) assert.equal(b.einstellung.betrieb.name, stand.einstellung.betrieb.name) assert.equal(b.fragebogen.branche, 'salon', 'der gespeicherte Fragebogen kommt mit') assert.deepEqual(b.fragebogen.bestaetigt, stand.fragebogen.bestaetigt) } else { assert.equal(b.einstellung, null) assert.equal(b.version, 0) assert.deepEqual(b.fragebogen, leerFragebogen()) } assert.ok(Array.isArray(b.vorlagen) && b.vorlagen.length === 9, 'die Vorlagen kommen in jedem Zustand') assert.equal(res.headers.get('cache-control'), 'no-store') assert.deepEqual(groessen(), vorher, 'Lesen legt nie ein Dokument an, ändert keins und protokolliert nichts') }) } } test('GET einstellungen ohne Funktionsdokument: 200 { state: off, einstellung: null, version: 0 }, portalFeatures und termineConfig bleiben leer', async () => { const vorher = groessen() const res = await send('GET', einstellungenUrl(), { cookie }) assert.equal(res.status, 200) assert.equal(res.body.state, 'off') assert.equal(res.body.einstellung, null) assert.equal(res.body.version, 0) assert.equal(res.body.siteKey, null, 'noch kein Dokument, also noch kein siteKey') assert.equal(res.body.aktivSeit, null) assert.equal(res.body.abgeschaltet, null) assert.equal(res.body.anfragenOffen, 0) assert.deepEqual(res.body.fehlt, []) assert.deepEqual(res.body.feeds, {}) assert.equal(aw.docs('portalFeatures').size, vorher.portalFeatures) assert.equal(aw.docs('portalFeatures').size, 0) assert.equal(aw.docs('termineConfig').size, 0) }) test('GET einstellungen: ein termineConfig-Dokument ohne Funktionsdokument wird nie gelesen (kein Dokument heißt leerer Entwurf)', async () => { aw.seed('termineConfig', entwurfDoc(FEATURE, { customerId: 'c1', projectId: 'p1' })) const res = await send('GET', einstellungenUrl(), { cookie }) assert.equal(res.status, 200) assert.equal(res.body.einstellung, null) assert.equal(res.body.version, 0) assert.deepEqual(res.body.fragebogen, leerFragebogen()) }) // ---------------------------------------------------------------- Alle anderen Routen behalten die Schranke active|disabled const ANDERE_ROUTEN = [ ['GET', '/api/termine/buchungen?projectId=p1&von=2026-10-01&bis=2026-10-31'], ['GET', '/api/termine/zeiten?projectId=p1&leistung=x&tag=2026-10-12'], ['GET', '/api/termine/export.csv?projectId=p1'], ['POST', '/api/termine/ressourcen/r_aaaaaaa1/feed', { projectId: 'p1' }], ['POST', '/api/termine/buchungen', { projectId: 'p1', art: 'sperre' }], ['POST', '/api/termine/buchungen/b_x/aktion', { projectId: 'p1', aktion: 'annehmen' }], ['DELETE', '/api/termine/buchungen/b_x?projectId=p1'], ] for (const zustand of ['keins', 'off', 'ordered', 'setup']) { for (const [zeile, auth] of ZEILEN) { test(`Zustand ${zustand}, ${zeile}: Buchungen, Zeiten, Export, Feed-Link und Termine eintragen bleiben 409 "nicht aktiv"`, async () => { setzeZustand(zustand) const vorher = groessen() for (const [method, pfad, body] of ANDERE_ROUTEN) { const res = await send(method, pfad, { ...auth(), body }) assert.equal(res.status, 409, `${method} ${pfad}`) assert.equal(res.body.error, NICHT_AKTIV, `${method} ${pfad}`) assert.equal(res.body.code, 'nicht_aktiv', `${method} ${pfad}`) } assert.deepEqual(groessen(), vorher) }) } } test('Zustand active und disabled: die Lese-Routen gehen weiter (Schranke active|disabled unverändert)', async () => { for (const zustand of ['active', 'disabled']) { await deleteDocument('portalFeatures', FEATURE).catch(() => {}) await deleteDocument('termineConfig', FEATURE).catch(() => {}) setzeZustand(zustand) const liste = await send('GET', '/api/termine/buchungen?projectId=p1&von=2026-10-01&bis=2026-10-31', { cookie }) assert.equal(liste.status, 200, zustand) assert.deepEqual(liste.body.buchungen, []) assert.equal((await send('GET', '/api/termine/export.csv?projectId=p1', { cookie })).status, 200, zustand) } }) test('Pilot-Schalter gilt auch für das Lesen des Entwurfs: Modus off oder Kunde nicht in der Liste => 409 "noch nicht verfügbar", ohne und mit Dokument', async () => { for (const zustand of ['keins', 'off']) { await deleteDocument('portalFeatures', FEATURE).catch(() => {}) setzeZustand(zustand) config.termine.pilotEmails = ['andere@example.invalid'] for (const auth of [{ cookie }, kv()]) { const res = await send('GET', einstellungenUrl(), auth) assert.equal(res.status, 409, zustand) assert.equal(res.body.error, 'Der Terminplaner ist noch nicht verfügbar.') assert.equal(res.body.code, 'nicht_verfuegbar') } config.termine.pilotEmails = ['*'] config.termine.mode = 'off' assert.equal((await send('GET', einstellungenUrl(), { cookie })).status, 409, `${zustand} bei Modus off`) config.termine.mode = 'pilot' } }) test('ohne Sitzung 401, ohne projectId 400, unbrauchbare projectId 403', async () => { assert.equal((await send('GET', einstellungenUrl())).status, 401) assert.equal((await send('GET', '/api/termine/einstellungen', { cookie })).status, 400) const unbrauchbar = await send('GET', einstellungenUrl('gibt es nicht'), { cookie }) assert.equal(unbrauchbar.status, 403) assert.equal(unbrauchbar.body.error, 'Kein Zugriff auf dieses Projekt.') }) // ---------------------------------------------------------------- Mandanten (Review-Focus 3) test('Mandanten: Kunde c2 mit dem Projekt von c1 => 403 auf GET, in jedem Zustand und in der Kundenansicht; nichts angelegt, nichts gelesen', async () => { for (const zustand of ['keins', 'off', 'ordered', 'setup', 'active', 'disabled']) { await deleteDocument('portalFeatures', FEATURE).catch(() => {}) await deleteDocument('termineConfig', FEATURE).catch(() => {}) setzeZustand(zustand) const vorher = groessen() for (const auth of [{ cookie: customerCookie('c2') }, kv({ customerId: 'c2' })]) { const res = await send('GET', einstellungenUrl('p1'), auth) assert.equal(res.status, 403, zustand) assert.equal(res.body.error, 'Kein Zugriff auf dieses Projekt.') assert.ok(!JSON.stringify(res.body).includes('Salon'), 'keine Daten des fremden Entwurfs') } assert.deepEqual(groessen(), vorher, zustand) } }) test('Mandanten: gehört das Funktionsdokument eines eigenen Projekts einem anderen Kunden, sieht der Kunde einen leeren Entwurf statt dessen Daten', async () => { aw.seed('portalFeatures', featureDoc('off', { $id: 'f_p3_termine', customerId: 'c1', projectId: 'p3', siteKey: 'bcdefghijklmnopq' })) aw.seed('termineConfig', entwurfDoc('f_p3_termine', { customerId: 'c1', projectId: 'p3' })) const res = await send('GET', einstellungenUrl('p3'), { cookie: customerCookie('c2') }) assert.equal(res.status, 200) assert.equal(res.body.state, 'off') assert.equal(res.body.einstellung, null) assert.equal(res.body.siteKey, null) assert.ok(!JSON.stringify(res.body).includes('Salon Muster')) }) // ---------------------------------------------------------------- Felder je Zustand test('branchen: Schlüssel, Name und gesundheit je Branche aus BRANCHEN (ohne Vorlage), der Gesundheitsbereich zuletzt', async () => { const res = await send('GET', einstellungenUrl(), { cookie }) assert.deepEqual(res.body.branchen, BRANCHEN.map(({ key, name, gesundheit }) => ({ key, name, gesundheit }))) assert.equal(res.body.branchen.length, 10) assert.deepEqual(res.body.branchen.at(-1), { key: 'gesundheit', name: 'Gesundheitsbereich', gesundheit: true }) assert.ok(res.body.branchen.every((b) => Object.keys(b).join() === 'key,name,gesundheit'), 'keine internen Felder wie vorlage') assert.equal(res.body.branchen.filter((b) => b.gesundheit).length, 1) }) test('einbau.snippet und feeds nur in active (feeds) und active/disabled (snippet); in off, ordered, setup und ohne Dokument leer', async () => { for (const zustand of ['keins', 'off', 'ordered', 'setup', 'active', 'disabled']) { await deleteDocument('portalFeatures', FEATURE).catch(() => {}) await deleteDocument('termineConfig', FEATURE).catch(() => {}) const stand = setzeZustand(zustand) const { body } = await send('GET', einstellungenUrl(), { cookie }) if (zustand === 'active' || zustand === 'disabled') { assert.match(body.einbau.snippet, new RegExp(`data-webklar-termine="${SITE_KEY}"`), zustand) } else { assert.equal(body.einbau.snippet, undefined, zustand) } if (zustand === 'active') { assert.equal(Object.keys(body.feeds).length, body.einstellung.ressourcen.length, 'Kalender-Link je Person') assert.ok(Object.keys(body.feeds).length > 0) } else { assert.deepEqual(body.feeds, {}, zustand) } // die Token stehen höchstens in den Links, nie in der Einstellung if (stand) { for (const r of stand.einstellung.ressourcen) { assert.equal(typeof r.feedToken, 'string') assert.ok(!JSON.stringify(body.einstellung).includes(r.feedToken), `${zustand}: kein Feed-Token in der Einstellung`) if (zustand !== 'active') assert.ok(!JSON.stringify(body).includes(r.feedToken), `${zustand}: auch sonst nirgends`) } } } }) test('abo, offen und bestellung gibt es nur in off; sonst null', async () => { for (const zustand of ['ordered', 'setup', 'active', 'disabled']) { await deleteDocument('portalFeatures', FEATURE).catch(() => {}) await deleteDocument('termineConfig', FEATURE).catch(() => {}) setzeZustand(zustand) const { body } = await send('GET', einstellungenUrl(), { cookie }) assert.equal(body.abo, null, zustand) assert.equal(body.offen, null, zustand) assert.equal(body.bestellung, null, zustand) } setzeZustand('off') const { body } = await send('GET', einstellungenUrl(), { cookie }) assert.notEqual(body.abo, null) assert.notEqual(body.offen, null) assert.notEqual(body.bestellung, null) }) test('E11 bleibt: disabled liefert das Feld abgeschaltet mit Grund, Zeitpunkt und Löschtag; Schreiben ist weiter 409 "abgeschaltet"', async () => { setzeZustand('disabled') const lesen = await send('GET', einstellungenUrl(), { cookie }) assert.deepEqual(lesen.body.abgeschaltet, { grund: 'abo_ende', seit: '2026-10-04T10:00:00.000Z', loeschungAm: '2026-11-03' }) const schreiben = await send('POST', '/api/termine/buchungen', { cookie, body: { projectId: 'p1', art: 'sperre' } }) assert.equal(schreiben.status, 409) assert.equal(schreiben.body.code, 'abgeschaltet') assert.match(schreiben.body.error, /Wartungs-Abo beendet/) for (const zustand of ['keins', 'off', 'ordered', 'setup', 'active']) { await deleteDocument('portalFeatures', FEATURE).catch(() => {}) setzeZustand(zustand) assert.equal((await send('GET', einstellungenUrl(), { cookie })).body.abgeschaltet, null, zustand) } }) // ---------------------------------------------------------------- Bestellung in off: abo, offen, bestellung test('off mit bestellbarem Entwurf und laufendem Wartungs-Abo: abo { ok: true }, offen leer, bestellung = Sicht des Registers', async () => { setzeZustand('off') const { body } = await send('GET', einstellungenUrl(), { cookie }) assert.deepEqual(body.abo, { ok: true }) assert.deepEqual(body.offen, []) assert.deepEqual(body.bestellung, JSON.parse(JSON.stringify(await bestellungAnsicht(T)))) assert.equal(body.bestellung.consent.knopf, T.consent.knopf, 'der Knopf zeigt genau den Registertext') assert.equal(body.bestellung.consent.version, T.consent.version) assert.deepEqual(body.bestellung.consent.items, T.consent.items.map((item) => ({ id: item.id, text: item.text, haken: Boolean(item.haken) }))) assert.equal(body.bestellung.preis, T.priceLabel) assert.equal(body.bestellung.preisHinweis, config.ustHinweis) assert.equal(typeof body.bestellung.belehrung, 'string') assert.ok(!('ticket' in body.bestellung) && !('mails' in body.bestellung), 'keine internen Registerfelder') }) test('off mit offenem Fragebogen: offen nennt die Punkte aus bestellpruefung mit Schritt und Text (ohne Abo-Punkt, wenn das Abo läuft)', async () => { const stand = bestellbarerStand({ now: JETZT }) stand.fragebogen.bestaetigt = stand.fragebogen.bestaetigt.filter((schritt) => schritt !== 'wer') stand.fragebogen.bundeslandGewaehlt = false setzeZustand('off', { stand }) const { body } = await send('GET', einstellungenUrl(), { cookie }) assert.deepEqual(body.offen, [ { schritt: 'wer', text: 'Bitte bestätige den Schritt „Wer wird gebucht“.' }, { schritt: 'zeiten', text: 'Bitte wähle das Bundesland deines Betriebs. Danach richten sich die Feiertage.' }, ]) assert.deepEqual(body.offen, bestellpruefung({ einstellung: stand.einstellung, fragebogen: stand.fragebogen, feature: T, abo: { ok: true } })) }) test('off ohne Entwurf (Dokument ohne Einstellung, oder gar kein Dokument): genau ein offener Punkt, die Wahl der Branche', async () => { const erwartet = [{ schritt: 'branche', text: 'Bitte starte den Fragebogen mit der Wahl deiner Branche.' }] assert.deepEqual((await send('GET', einstellungenUrl(), { cookie })).body.offen, erwartet, 'kein Dokument') aw.seed('portalFeatures', featureDoc('off')) const body = (await send('GET', einstellungenUrl(), { cookie })).body assert.deepEqual(body.offen, erwartet, 'Dokument ohne termineConfig') assert.deepEqual(body.abo, { ok: true }) assert.deepEqual(body.bestellung, JSON.parse(JSON.stringify(await bestellungAnsicht(T)))) }) test('Abo ohne laufendes Wartungs-Abo (nur Hosting, oder gar keins): abo { ok: false, grund: kein_abo }, offen mit dem Abo-Punkt (h) unter "bestellen"', async () => { setzeZustand('off') const text = 'Für den Terminplaner brauchst du ein laufendes Wartungs-Abo.' assert.equal(aboMeldung(T, 'kein_abo'), text) const faelle = [ ['nur Hosting', async () => {}], ['gar kein Abo', () => deleteDocument('subscriptions', 'sub_h')], ] for (const [name, vorbereitung] of faelle) { aw.seed('subscriptions', subscriptionDoc('sub_h', { ...wartungDoc, productKey: 'hosting' })) // wie seedFixtures await vorbereitung() const { body } = await send('GET', einstellungenUrl(), { cookie }) assert.equal(body.state, 'off', name) assert.deepEqual(body.abo, { ok: false, grund: 'kein_abo' }, name) assert.deepEqual(body.offen, [{ schritt: 'bestellen', text }], name) } }) test('Abo mit vorgemerkter Kündigung: grund gekuendigt; past_due: grund nicht_aktiv; jeweils mit dem Text aus aboMeldung', async () => { setzeZustand('off') wartung({ cancelAtPeriodEnd: true }) let body = (await send('GET', einstellungenUrl(), { cookie })).body assert.deepEqual(body.abo, { ok: false, grund: 'gekuendigt' }) assert.deepEqual(body.offen, [{ schritt: 'bestellen', text: aboMeldung(T, 'gekuendigt') }]) assert.equal(body.offen[0].text, 'Dein Wartungs-Abo ist gekündigt. Für den Terminplaner brauchst du ein laufendes Abo.') wartung({ status: 'past_due' }) body = (await send('GET', einstellungenUrl(), { cookie })).body assert.deepEqual(body.abo, { ok: false, grund: 'nicht_aktiv' }) assert.deepEqual(body.offen, [{ schritt: 'bestellen', text: aboMeldung(T, 'nicht_aktiv') }]) }) test('Lesefehler beim Abo: GET bleibt 200, abo null und der offene Punkt "nicht geprüft" (Text aus aboMeldung unbekannt); nichts Technisches nach außen', async () => { setzeZustand('off') aw.failNext({ collection: 'subscriptions', status: 500, message: 'geheimer Appwrite-Text' }) const res = await send('GET', einstellungenUrl(), { cookie }) assert.equal(res.status, 200) assert.equal(res.body.abo, null) assert.deepEqual(res.body.offen, [{ schritt: 'bestellen', text: aboMeldung(T, 'unbekannt') }]) assert.equal(res.body.offen[0].text, 'Das Abo konnte gerade nicht geprüft werden.') assert.ok(!JSON.stringify(res.body).includes('geheimer')) assert.ok(console.error.mock.calls.every((aufruf) => !String(aufruf.arguments[0]).includes('geheimer')), 'auch nicht im Log') // der nächste Aufruf liest wieder (nichts wird gemerkt) assert.deepEqual((await send('GET', einstellungenUrl(), { cookie })).body.abo, { ok: true }) }) test('Lesefehler beim Abo und offener Fragebogen: die übrigen Punkte bleiben, der Abo-Punkt kommt dazu (sortiert nach Schrittfolge, "bestellen" zuletzt)', async () => { const stand = bestellbarerStand({ now: JETZT }) stand.fragebogen.bestaetigt = [] setzeZustand('off', { stand }) aw.failNext({ collection: 'subscriptions', status: 503 }) const { body } = await send('GET', einstellungenUrl(), { cookie }) assert.equal(body.abo, null) assert.equal(body.offen.at(-1).schritt, 'bestellen') assert.equal(body.offen.at(-1).text, aboMeldung(T, 'unbekannt')) assert.equal(body.offen.length, 8 + 1, 'acht unbestätigte Schritte und der Abo-Punkt') }) test('Das Abo wird nur in off gelesen: in ordered, setup, active und disabled gibt es keine Abfrage auf subscriptions', async () => { for (const zustand of ['ordered', 'setup', 'active', 'disabled']) { await deleteDocument('portalFeatures', FEATURE).catch(() => {}) await deleteDocument('termineConfig', FEATURE).catch(() => {}) setzeZustand(zustand) aw.resetListCalls() assert.equal((await send('GET', einstellungenUrl(), { cookie })).status, 200, zustand) assert.deepEqual(aw.listCalls('subscriptions'), [], zustand) } await deleteDocument('portalFeatures', FEATURE).catch(() => {}) setzeZustand('off') aw.resetListCalls() await send('GET', einstellungenUrl(), { cookie }) assert.equal(aw.listCalls('subscriptions').length, 1, 'in off genau eine Abfrage') }) // ---------------------------------------------------------------- betriebVorschlag test('betriebVorschlag ohne Einstellung: Name, Anschrift, E-Mail und Telefon aus dem Konto; Website leer, wenn es nur die Vorschau-Adresse gibt', async () => { for (const zustand of ['keins', 'off']) { await deleteDocument('portalFeatures', FEATURE).catch(() => {}) if (zustand === 'off') aw.seed('portalFeatures', featureDoc('off')) const { body } = await send('GET', einstellungenUrl(), { cookie }) assert.deepEqual(body.betriebVorschlag, { name: 'Salon Müller', adresse: 'Hauptstr. 1, 67722 Winnweiler', email: 'k@example.invalid', telefon: '06301 1234', websiteUrl: '', }, zustand) } }) test('betriebVorschlag: die Website kommt nur aus liveDomain (https, ohne Pfad), nie aus der Vorschau-Adresse', async () => { const faelle = [ ['Live-Domain mit Schema und Pfad', { liveDomain: 'HTTPS://Www.Salon-Mueller.de/start?x=1' }, 'https://www.salon-mueller.de'], ['Live-Domain schlicht', { liveDomain: 'salon-mueller.de' }, 'https://salon-mueller.de'], ['Live-Domain ist die Vorschau-Adresse', { liveDomain: 'salon.project.webklar.com' }, ''], ['Live-Domain unbrauchbar', { liveDomain: 'kein host' }, ''], ['keine Live-Domain', { liveDomain: '' }, ''], ] for (const [name, over, erwartet] of faelle) { aw.seed('websiteProjects', { $id: 'p1', customerId: 'c1', projectName: 'Salon', subdomain: 'salon', ...over }) const { body } = await send('GET', einstellungenUrl(), { cookie }) assert.equal(body.betriebVorschlag.websiteUrl, erwartet, name) } // ohne Subdomain gilt die Live-Domain ebenfalls aw.seed('websiteProjects', { $id: 'p1', customerId: 'c1', projectName: 'Salon', liveDomain: 'salon-mueller.de' }) assert.equal((await send('GET', einstellungenUrl(), { cookie })).body.betriebVorschlag.websiteUrl, 'https://salon-mueller.de') }) test('betriebVorschlag ohne Firma und Telefon im Konto: Name aus dem Kontonamen, Rest leer', async () => { aw.seed('customers', { ...customers.c1 }) const { body } = await send('GET', einstellungenUrl(), { cookie }) assert.deepEqual(body.betriebVorschlag, { name: 'Kunde Eins', adresse: '', email: 'k@example.invalid', telefon: '', websiteUrl: '' }) }) test('betriebVorschlag gibt es nur ohne Einstellung: mit gespeicherter Einstellung fehlt das Feld (und das Konto wird nicht gelesen)', async () => { setzeZustand('off') const { body } = await send('GET', einstellungenUrl(), { cookie }) assert.ok(!('betriebVorschlag' in body)) for (const zustand of ['ordered', 'setup', 'active', 'disabled']) { await deleteDocument('portalFeatures', FEATURE).catch(() => {}) setzeZustand(zustand) assert.ok(!('betriebVorschlag' in (await send('GET', einstellungenUrl(), { cookie })).body), zustand) } }) test('betriebVorschlag auch in active und disabled ohne Einstellung (das Konto liefert die Vorgabe, nicht der Zustand)', async () => { for (const zustand of ['active', 'disabled']) { await deleteDocument('portalFeatures', FEATURE).catch(() => {}) setzeZustand(zustand, { stand: null }) const { body } = await send('GET', einstellungenUrl(), { cookie }) assert.equal(body.einstellung, null) assert.equal(body.betriebVorschlag.name, 'Salon Müller', zustand) } }) // ================================================================ Teil 1b (C4b): Schreibregeln des Entwurfs const PATCH_URL = '/api/termine/einstellungen' const GESPERRT = 'Dein Terminplaner ist bestellt. Die Einstellungen sind jetzt gesperrt, Änderungen laufen über den Chat.' const FEHLT_BEIDES = ['betrieb.telefon', 'betrieb.adresse'] const PUBLIC = '/api/public/termine/v1' const patch = (body, auth = { cookie }) => send('PATCH', PATCH_URL, { ...auth, body: { projectId: 'p1', ...body } }) /** Alle Dokumente, die ein Schreibversuch ändern könnte, als Text: gleicher Text = nichts angelegt, nichts geändert. */ const dokumente = () => JSON.stringify(['portalFeatures', 'termineConfig', 'customerViewLog', 'featureConsents'].map((c) => [c, [...aw.docs(c).entries()]])) const logEintraege = () => [...aw.docs('customerViewLog').values()] const termineConfigDoc = () => aw.docs('termineConfig').get(FEATURE) /** Ausgangsstand für einen PATCH: Zustand setzen (oder 'keins') und die Einstellung dazu zurückgeben. */ const ausgangsstand = (zustand) => setzeZustand(zustand) ?? bestellbarerStand({ now: JETZT }) const neueEinstellung = (basis, aendere = (e) => e) => { const e = structuredClone(basis.einstellung) e.betrieb.name = 'Salon Neu' return aendere(e) } const ohneTelefonUndAnschrift = (e) => { e.betrieb.telefon = '' e.betrieb.adresse = '' return e } // ---------------------------------------------------------------- Zustandsmatrix PATCH const ERLAUBT = { keins: 1, off: 2, active: 2, ordered: 2, setup: 2 } // erwartete Version nach dem Speichern 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' const erwartet = abgeschaltet ? '409 abgeschaltet' : gesperrt ? '409 gesperrt' : '200' test(`PATCH einstellungen: Zustand ${zustand}, ${zeile} => ${erwartet}`, async () => { const basis = ausgangsstand(zustand) const vorher = dokumente() const res = await patch({ version: zustand === 'keins' ? 0 : 1, einstellung: neueEinstellung(basis) }, auth()) if (abgeschaltet) { assert.equal(res.status, 409, JSON.stringify(res.body)) assert.equal(res.body.code, 'abgeschaltet') assert.match(res.body.error, /Wartungs-Abo beendet/, 'der Text von E11') assert.match(res.body.error, /Ab dem 03\.11\.2026 löschen wir die Daten\./) assert.equal(dokumente(), vorher, 'nichts geändert, nichts protokolliert') return } if (gesperrt) { assert.equal(res.status, 409, JSON.stringify(res.body)) assert.equal(res.body.code, 'gesperrt') assert.equal(res.body.error, GESPERRT) assert.equal(dokumente(), vorher, 'das Dokument bleibt unverändert') return } assert.equal(res.status, 200, JSON.stringify(res.body)) assert.equal(res.body.einstellung.betrieb.name, 'Salon Neu') assert.equal(res.body.version, ERLAUBT[zustand]) assert.deepEqual(res.body.fehlt, []) assert.equal(termineConfigDoc().version, ERLAUBT[zustand]) assert.equal(JSON.parse(termineConfigDoc().einstellung).betrieb.name, 'Salon Neu') assert.equal(termineConfigDoc().updatedBy, kundenansicht ? 'team:u_staff2' : 'kunde:c1') // der Zustand des Funktionsdokuments ändert sich durch das Speichern nie const feature = aw.docs('portalFeatures').get(FEATURE) assert.equal(feature.state, zustand === 'keins' ? 'off' : zustand) assert.equal(feature.enabled, zustand === 'active') // Protokoll nur in der Kundenansicht, ohne Inhalt der Einstellung assert.deepEqual(logEintraege().map((e) => [e.action, e.detail]), kundenansicht ? [['termin_einstellungen', 'Einstellungen gespeichert']] : []) }) } } test('PATCH einstellungen ohne Funktionsdokument: der erste Schreibzugriff legt Dokument (off, siteKey, ohne Kauf) und Einstellung an', async () => { assert.equal(aw.docs('portalFeatures').size, 0) const res = await patch({ version: 0, einstellung: neueEinstellung(bestellbarerStand({ now: JETZT })) }) assert.equal(res.status, 200, JSON.stringify(res.body)) assert.equal(aw.docs('portalFeatures').size, 1) const feature = aw.docs('portalFeatures').get(FEATURE) assert.equal(feature.customerId, 'c1') assert.equal(feature.projectId, 'p1') assert.equal(feature.featureKey, 'termine') assert.equal(feature.state, 'off') assert.equal(feature.enabled, false) assert.equal(feature.unlockedByPurchase, false) assert.equal(feature.purchaseStatus, '') assert.match(feature.siteKey, /^[a-z2-7]{16}$/) assert.equal(termineConfigDoc().customerId, 'c1') assert.equal(termineConfigDoc().projectId, 'p1') // danach liest GET den Entwurf in off const gelesen = (await send('GET', einstellungenUrl(), { cookie })).body assert.equal(gelesen.state, 'off') assert.equal(gelesen.version, 1) assert.equal(gelesen.einstellung.betrieb.name, 'Salon Neu') assert.equal(gelesen.siteKey, feature.siteKey) }) test('PATCH einstellungen: die Antwort trägt Einstellung, Version und fehlt - ohne Feed-Token', async () => { const basis = ausgangsstand('off') const res = await patch({ version: 1, einstellung: neueEinstellung(basis) }) assert.deepEqual(Object.keys(res.body).sort(), ['einstellung', 'fehlt', 'version']) assert.ok(res.body.einstellung.ressourcen.every((r) => !('feedToken' in r))) }) // ---------------------------------------------------------------- Mandanten (Review-Focus 3) test('Mandanten: Kunde c2 (und seine Kundenansicht) mit dem Projekt von c1 => 403 auf PATCH, in jedem Zustand; nichts angelegt, termineConfig unverändert', async () => { for (const zustand of ZUSTAENDE) { await deleteDocument('portalFeatures', FEATURE).catch(() => {}) await deleteDocument('termineConfig', FEATURE).catch(() => {}) const basis = ausgangsstand(zustand) const vorher = dokumente() for (const auth of [{ cookie: customerCookie('c2') }, kv({ customerId: 'c2' })]) { const res = await patch({ version: zustand === 'keins' ? 0 : 1, einstellung: neueEinstellung(basis) }, auth) assert.equal(res.status, 403, zustand) assert.equal(res.body.error, 'Kein Zugriff auf dieses Projekt.') } assert.equal(dokumente(), vorher, `${zustand}: nichts angelegt, nichts geändert`) if (zustand !== 'keins') assert.equal(JSON.parse(termineConfigDoc().einstellung).betrieb.name, 'Salon Muster', zustand) } }) test('Mandanten: ohne Sitzung 401, ohne projectId 400, unbrauchbare projectId 403, Pilot-Schalter 409 - nichts wird angelegt', async () => { const basis = ausgangsstand('keins') const eingabe = { version: 0, einstellung: neueEinstellung(basis) } assert.equal((await send('PATCH', PATCH_URL, { body: { projectId: 'p1', ...eingabe } })).status, 401) assert.equal((await send('PATCH', PATCH_URL, { cookie, body: eingabe })).status, 400) assert.equal((await patch({ ...eingabe, projectId: 'gibt es nicht' })).status, 403) config.termine.pilotEmails = ['andere@example.invalid'] const gesperrt = await patch(eingabe) assert.equal(gesperrt.status, 409) assert.equal(gesperrt.body.code, 'nicht_verfuegbar') assert.equal(aw.docs('portalFeatures').size, 0) assert.equal(aw.docs('termineConfig').size, 0) }) // ---------------------------------------------------------------- fehlt: der Entwurf überspringt nur die Pflichtangaben for (const zustand of ['keins', 'off']) { test(`PATCH ${zustand} ohne Telefon und Anschrift => 200 mit fehlt [betrieb.telefon, betrieb.adresse]; GET meldet dasselbe`, async () => { const basis = ausgangsstand(zustand) const res = await patch({ version: zustand === 'keins' ? 0 : 1, einstellung: neueEinstellung(basis, ohneTelefonUndAnschrift) }) assert.equal(res.status, 200, JSON.stringify(res.body)) assert.deepEqual(res.body.fehlt, FEHLT_BEIDES) assert.equal(res.body.einstellung.betrieb.telefon, '') assert.equal(JSON.parse(termineConfigDoc().einstellung).betrieb.adresse, '') assert.deepEqual((await send('GET', einstellungenUrl(), { cookie })).body.fehlt, FEHLT_BEIDES) }) } test('PATCH ohne Telefon und Anschrift: in active 400 (streng), im Entwurf (off, Kundenansicht in ordered) 200 - dieselbe Eingabe', async () => { const basis = ausgangsstand('active') const eingabe = neueEinstellung(basis, ohneTelefonUndAnschrift) const vorher = dokumente() const streng = await patch({ version: 1, einstellung: eingabe }) assert.equal(streng.status, 400, JSON.stringify(streng.body)) assert.deepEqual(Object.keys(streng.body.fehler).sort(), [...FEHLT_BEIDES].sort()) assert.match(streng.body.error, /Telefonnummer/) assert.equal(dokumente(), vorher, 'nichts gespeichert') await deleteDocument('portalFeatures', FEATURE) await deleteDocument('termineConfig', FEATURE) setzeZustand('ordered', { stand: basis }) const entwurf = await patch({ version: 1, einstellung: eingabe }, kv()) assert.equal(entwurf.status, 200, JSON.stringify(entwurf.body)) assert.deepEqual(entwurf.body.fehlt, FEHLT_BEIDES) }) test('PATCH ungültige Angaben bleiben im Entwurf 400 (nur die Pflichtangaben entfallen), ohne Dokument entsteht nichts', async () => { for (const zustand of ['keins', 'off']) { await deleteDocument('portalFeatures', FEATURE).catch(() => {}) await deleteDocument('termineConfig', FEATURE).catch(() => {}) const basis = ausgangsstand(zustand) const vorher = dokumente() const version = zustand === 'keins' ? 0 : 1 const url = await patch({ version, einstellung: neueEinstellung(basis, (e) => ({ ...e, betrieb: { ...e.betrieb, websiteUrl: 'kein link' } })) }) assert.equal(url.status, 400, `${zustand}: ${JSON.stringify(url.body)}`) assert.ok(url.body.fehler['betrieb.websiteUrl'], zustand) const farbe = await patch({ version, einstellung: neueEinstellung(basis, (e) => ({ ...e, farbe: 'rot' })) }) assert.equal(farbe.status, 400, zustand) assert.ok(farbe.body.fehler.farbe, zustand) const keinObjekt = await patch({ version, einstellung: 'text' }) assert.equal(keinObjekt.status, 400, zustand) for (const falsch of [undefined, -1, 1.5, '1', null]) { const res = await patch({ version: falsch, einstellung: neueEinstellung(basis) }) assert.equal(res.status, 400, `${zustand} version ${String(falsch)}`) assert.ok(res.body.fehler.version) } assert.equal(dokumente(), vorher, `${zustand}: ein abgelehnter Entwurf legt nichts an und ändert nichts`) } }) test('PATCH mit veralteter Version im Entwurf => 409 EINSTELLUNG_VERALTET, der gespeicherte Stand bleibt', async () => { const basis = ausgangsstand('off') const vorher = dokumente() const res = await patch({ version: 0, einstellung: neueEinstellung(basis) }) assert.equal(res.status, 409) assert.equal(res.body.code, 'EINSTELLUNG_VERALTET') assert.equal(dokumente(), vorher) }) // ---------------------------------------------------------------- Abo: ein Entwurf entsteht nur mit laufendem Wartungs-Abo const KEIN_ABO = aboMeldung(T, 'kein_abo') for (const zustand of ['keins', 'off']) { for (const [zeile, auth] of ZEILEN) { test(`Abo: ${zustand}, ${zeile}: nur Hosting oder gar kein Abo => PATCH 409 kein_abo mit dem Text aus aboMeldung; kein Dokument entsteht, nichts ändert sich`, async () => { const basis = ausgangsstand(zustand) const vorher = dokumente() assert.equal(KEIN_ABO, 'Für den Terminplaner brauchst du ein laufendes Wartungs-Abo.') for (const vorbereitung of [ () => aw.seed('subscriptions', subscriptionDoc('sub_h', { ...wartungDoc, productKey: 'hosting' })), // nur Hosting () => deleteDocument('subscriptions', 'sub_h'), // gar keins ]) { await vorbereitung() const res = await patch({ version: zustand === 'keins' ? 0 : 1, einstellung: neueEinstellung(basis) }, auth()) assert.equal(res.status, 409, JSON.stringify(res.body)) assert.equal(res.body.code, 'kein_abo') assert.equal(res.body.error, KEIN_ABO) assert.equal(dokumente(), vorher) } if (zustand === 'keins') { assert.equal(aw.docs('portalFeatures').size, 0) assert.equal(aw.docs('termineConfig').size, 0) } }) } } test('Abo: vorgemerkte Kündigung oder offene Zahlung (past_due) sperren den Entwurf nicht - PATCH bleibt 200 (die Bestellung sperrt Gate (h))', async () => { const basis = ausgangsstand('off') let version = 1 for (const aenderung of [{ cancelAtPeriodEnd: true }, { cancelAt: '2026-11-30T00:00:00.000Z' }, { status: 'past_due' }, { status: 'trialing' }]) { wartung(aenderung) const res = await patch({ version, einstellung: neueEinstellung(basis) }) assert.equal(res.status, 200, `${JSON.stringify(aenderung)}: ${JSON.stringify(res.body)}`) version = res.body.version } // dasselbe ohne Dokument: der Entwurf entsteht auch bei vorgemerkter Kündigung await deleteDocument('portalFeatures', FEATURE) await deleteDocument('termineConfig', FEATURE) wartung({ cancelAtPeriodEnd: true }) assert.equal((await patch({ version: 0, einstellung: neueEinstellung(basis) })).status, 200) assert.equal(aw.docs('portalFeatures').get(FEATURE).state, 'off') }) test('Abo: ein beendetes Wartungs-Abo (canceled) ist kein laufendes => PATCH 409 kein_abo', async () => { const basis = ausgangsstand('off') wartung({ status: 'canceled', endedAt: '2026-10-01T00:00:00.000Z' }) const res = await patch({ version: 1, einstellung: neueEinstellung(basis) }) assert.equal(res.status, 409) assert.equal(res.body.code, 'kein_abo') }) for (const zustand of ['keins', 'off']) { test(`Abo: Lesefehler (${zustand}) => PATCH 503 abo_pruefung ohne Appwrite-Text; nichts angelegt oder geändert; der nächste Versuch geht`, async () => { const basis = ausgangsstand(zustand) const vorher = dokumente() const version = zustand === 'keins' ? 0 : 1 aw.failNext({ collection: 'subscriptions', status: 500, message: 'geheimer Appwrite-Text' }) const res = await patch({ version, einstellung: neueEinstellung(basis) }) assert.equal(res.status, 503, JSON.stringify(res.body)) assert.equal(res.body.code, 'abo_pruefung') assert.equal(res.body.error, aboMeldung(T, 'unbekannt')) assert.ok(!JSON.stringify(res.body).includes('geheimer')) assert.ok(console.error.mock.calls.every((aufruf) => !String(aufruf.arguments[0]).includes('geheimer')), 'auch nicht im Log') assert.equal(dokumente(), vorher) assert.equal((await patch({ version, einstellung: neueEinstellung(basis) })).status, 200, 'nichts wird gemerkt') }) } test('Abo: gelesen wird nur in off (und ohne Dokument); in active und in der Kundenansicht von ordered und setup gibt es keine Abfrage, und es geht auch ohne Abo', async () => { await deleteDocument('subscriptions', 'sub_h') for (const [zustand, auth] of [['active', { cookie }], ['ordered', kv()], ['setup', kv()]]) { await deleteDocument('portalFeatures', FEATURE).catch(() => {}) await deleteDocument('termineConfig', FEATURE).catch(() => {}) const basis = ausgangsstand(zustand) aw.resetListCalls() const res = await patch({ version: 1, einstellung: neueEinstellung(basis) }, auth) assert.equal(res.status, 200, `${zustand}: ${JSON.stringify(res.body)}`) assert.deepEqual(aw.listCalls('subscriptions'), [], zustand) } await deleteDocument('portalFeatures', FEATURE).catch(() => {}) await deleteDocument('termineConfig', FEATURE).catch(() => {}) wartung() setzeZustand('off') aw.resetListCalls() assert.equal((await patch({ version: 1, einstellung: neueEinstellung(bestellbarerStand({ now: JETZT })) })).status, 200) 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 () => { 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 }) assert.equal(res.status, 409, `${zustand} ${pfad}`) assert.equal(res.body.code, 'nicht_aktiv') } assert.equal(dokumente(), vorher) } }) // ---------------------------------------------------------------- öffentlich: ein Entwurf ist nie sichtbar const feedToken = (stand) => stand.einstellung.ressourcen[0].feedToken /** Frisch lesen: der Speicher der öffentlichen Schnittstelle (30 s je siteKey) darf zwischen zwei Zuständen nichts merken. */ const oeffentlichConfig = (siteKey = SITE_KEY) => { resetKontextForTests() return send('GET', `${PUBLIC}/${siteKey}/config`) } const oeffentlichBuchung = (siteKey = SITE_KEY) => { resetKontextForTests() return send('POST', `${PUBLIC}/${siteKey}/buchungen`, { body: { name: 'Gast', email: 'gast@example.invalid' }, headers: { origin: app.url } }) } const oeffentlichKalender = (token, siteKey = SITE_KEY) => { resetKontextForTests() return send('GET', `${PUBLIC}/kalender/${siteKey}/${token}.ics`) } for (const zustand of ['off', 'ordered', 'setup', 'disabled']) { test(`öffentlich: ${zustand} mit vollständiger Einstellung => config und Buchung { aktiv: false }, Kalender-Link mit dem richtigen Token 404`, async () => { const stand = setzeZustand(zustand) assert.deepEqual(stand.einstellung.betrieb.telefon !== '' && stand.einstellung.betrieb.adresse !== '', true, 'die Einstellung ist vollständig') const config_ = await oeffentlichConfig() assert.equal(config_.status, 200) assert.deepEqual(config_.body, { aktiv: false }) const buchung = await oeffentlichBuchung() assert.equal(buchung.status, 200, JSON.stringify(buchung.body)) assert.deepEqual(buchung.body, { aktiv: false }) const kalender = await oeffentlichKalender(feedToken(stand)) assert.equal(kalender.status, 404) assert.ok(!String(kalender.body).includes('BEGIN:VCALENDAR')) }) } test('öffentlich: ein Entwurf, der per PATCH gespeichert wurde, bleibt unsichtbar - erst die Aktivierung macht ihn sichtbar', async () => { const basis = ausgangsstand('keins') const res = await patch({ version: 0, einstellung: neueEinstellung(basis) }) assert.equal(res.status, 200, JSON.stringify(res.body)) const siteKey = aw.docs('portalFeatures').get(FEATURE).siteKey // beim ersten Schreiben vergeben const token = JSON.parse(termineConfigDoc().einstellung).ressourcen[0].feedToken assert.deepEqual((await oeffentlichConfig(siteKey)).body, { aktiv: false }) assert.deepEqual((await oeffentlichBuchung(siteKey)).body, { aktiv: false }) assert.equal((await oeffentlichKalender(token, siteKey)).status, 404) // Aktivierung (Team): ab jetzt gilt dieselbe Einstellung öffentlich await updateDocument('portalFeatures', FEATURE, { state: 'active', enabled: true }) const aktiv = await oeffentlichConfig(siteKey) assert.equal(aktiv.body.aktiv, true, JSON.stringify(aktiv.body)) const buchung = await oeffentlichBuchung(siteKey) assert.notDeepEqual(buchung.body, { aktiv: false }, 'die Buchung wird jetzt geprüft (Formular-Token fehlt), nicht als inaktiv abgewiesen') assert.equal(buchung.status, 400) const kalender = await oeffentlichKalender(token, siteKey) assert.equal(kalender.status, 200) assert.match(kalender.body, /BEGIN:VCALENDAR/) }) test('öffentlich: active mit gespeicherter Einstellung geht (Gegenprobe zu off, ordered, setup), auch der Kalender-Link', async () => { const stand = setzeZustand('active') const aktiv = await oeffentlichConfig() assert.equal(aktiv.body.aktiv, true, JSON.stringify(aktiv.body)) assert.equal((await oeffentlichKalender(feedToken(stand))).status, 200) assert.equal((await oeffentlichKalender('x'.repeat(43))).status, 404, 'ein falsches Token bleibt 404') }) test('Einbauwünsche nie öffentlich: fragebogen.einbau.stelle und knopfText stehen in keiner öffentlichen Antwort', async () => { const stand = bestellbarerStand({ now: JETZT }) stand.fragebogen.einbau = { modus: 'knopf', knopfText: 'GEHEIMKNOPF', stelle: 'GEHEIMWUNSCH', gestaltung: 'GEHEIMDESIGN', bemerkung: 'GEHEIMNOTIZ' } setzeZustand('active', { stand }) const res = await oeffentlichConfig() assert.equal(res.body.aktiv, true, 'die Funktion ist aktiv, die Antwort enthält also Daten') const text = JSON.stringify(res.body) for (const verboten of ['GEHEIMWUNSCH', 'GEHEIMKNOPF', 'GEHEIMDESIGN', 'GEHEIMNOTIZ', 'fragebogen', 'einbau', 'knopfText']) { assert.ok(!text.includes(verboten), `"${verboten}" darf in der öffentlichen Config nicht stehen`) } // der Kunde selbst sieht seinen Entwurf natürlich weiter assert.equal((await send('GET', einstellungenUrl(), { cookie })).body.fragebogen.einbau.stelle, 'GEHEIMWUNSCH') // ... und derselbe Stand in den Entwurfszuständen ist öffentlich gar nicht erst erreichbar for (const zustand of ['off', 'ordered', 'setup']) { await deleteDocument('portalFeatures', FEATURE) await deleteDocument('termineConfig', FEATURE) setzeZustand(zustand, { stand }) assert.deepEqual((await oeffentlichConfig()).body, { aktiv: false }, zustand) } })