Terminplaner TP2: Entwurfszugriff lesen, GET ohne Dokument, GET-Felder
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
31aa80affd
commit
250fc0c4df
3 files changed
+613
-11
No files matched your search
@@ -0,0 +1,516 @@
|
||||
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; die
|
||||
// Schreibregeln des Entwurfs (PATCH, Vorlage, PUT fragebogen) folgen in Teil 1b (C4b).
|
||||
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 } = 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 { resetKontextForTests } = await import('../server/services/termine/kontext.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/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 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()
|
||||
setzeUhrFuerTests(() => JETZT)
|
||||
config.termine.mode = 'pilot'
|
||||
config.termine.pilotEmails = ['*']
|
||||
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]
|
||||
})
|
||||
|
||||
// ---------------------------------------------------------------- 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)
|
||||
}
|
||||
})
|
||||
Reference in new issue
Block a user