import { test, after, beforeEach, afterEach, mock } from 'node:test' import assert from 'node:assert/strict' import { startAppwriteMock } from './support/appwriteMock.js' import { startFakeStripe } from './support/fakeStripe.js' import { mailSink } from './support/mailSink.js' import { schemaKeys } from './support/schema.js' import { useTestEnv } from './support/testEnv.js' import { ZEITEN_DATETIMES } from '../server/services/zeiten/schema.js' import { TERMINE_DATETIMES } from '../server/services/termine/schema.js' import { SHOP_DATETIMES } from '../server/services/shop/schema.js' import { UNTERKUNFT_DATETIMES } from '../server/services/unterkunft/schema.js' // GET /api/funktionen in der neuen Ansicht: je Projekt und Funktion `kurzinfo` (Live-Detail der Zeile in "Meine"). Nur aktive Funktionen // bekommen einen Text, jede Einzelabfrage ist begrenzt und fehlertolerant (Fehler oder Zeitueberschreitung ergibt null), Berliner Zeit. // Ohne neue Ansicht bleibt die Antwort wie sie war und nichts davon wird gelesen. const aw = await startAppwriteMock({ schema: { portalFeatures: schemaKeys('portalFeatures'), subscriptions: schemaKeys('subscriptions'), contractEvents: schemaKeys('contractEvents'), featureConsents: schemaKeys('featureConsents'), shopConfig: schemaKeys('shopConfig'), ukEinstellungen: schemaKeys('ukEinstellungen'), oeffnungszeiten: schemaKeys('oeffnungszeiten'), zeitenErinnerungen: schemaKeys('zeitenErinnerungen'), termineBookings: schemaKeys('termineBookings'), shopOrders: schemaKeys('shopOrders'), newsPosts: schemaKeys('newsPosts'), ukAnfragen: schemaKeys('ukAnfragen'), }, datetimes: { ...ZEITEN_DATETIMES, termineBookings: TERMINE_DATETIMES.termineBookings, shopOrders: SHOP_DATETIMES.shopOrders, ukAnfragen: UNTERKUNFT_DATETIMES.ukAnfragen, newsPosts: ['visibleFrom', 'visibleUntil', 'takenDownAt', 'createdAt', 'updatedAt'], portalFeatures: ['orderedAt', 'activatedAt', 'disabledAt', 'consentAt', 'aboEndeMailAt', 'aboEndeAt', 'einschaltMailAt'], featureConsents: ['acceptedAt'], }, defaultLimit: 25, }) const st = await startFakeStripe() useTestEnv({ appwriteUrl: aw.url, stripeUrl: st.url }) const { config } = await import('../server/config.js') const { startTestApp, get } = await import('./support/testApp.js') const { customerCookie, seedFixtures, subscriptionDoc, wartungDoc } = await import('./support/fixtures.js') const { deleteDocument } = await import('../server/services/appwriteAdmin.js') const { setMailTransportForTests } = await import('../server/services/mailer.js') const { default: funktionenRoutes } = await import('../server/routes/funktionen.js') const { resetZeitenCacheForTests } = await import('../server/services/zeiten/cache.js') const { einschaltenDeps } = await import('../server/services/zeiten/einschalten.js') const { kurzinfoDeps } = await import('../server/services/kundenansicht/kurzinfo.js') const app = await startTestApp({ '/api/funktionen': funktionenRoutes }) after(() => Promise.all([app.close(), aw.close(), st.close()])) after(() => setMailTransportForTests(null)) const JETZT = new Date('2026-10-17T08:00:00.000Z') // Samstag 10:00 Uhr in Berlin (Sommerzeit) const c1 = customerCookie('c1') const vorher = { neu: { mode: config.kundenansichtNeu.mode, pilotEmails: [...config.kundenansichtNeu.pilotEmails] }, termine: config.termine.mode, shop: { mode: config.shop.mode, pilotEmails: [...config.shop.pilotEmails] }, unterkunft: { mode: config.unterkunft.mode, pilotEmails: [...config.unterkunft.pilotEmails] }, zeiten: { mode: config.zeiten.mode, pilotEmails: [...config.zeiten.pilotEmails] }, analyse: config.analyse.enabled, } after(() => { Object.assign(config.kundenansichtNeu, vorher.neu) config.termine.mode = vorher.termine Object.assign(config.shop, vorher.shop) Object.assign(config.unterkunft, vorher.unterkunft) Object.assign(config.zeiten, vorher.zeiten) config.analyse.enabled = vorher.analyse }) const SAMMLUNGEN = ['subscriptions', 'portalFeatures', 'featureConsents', 'oeffnungszeiten', 'workorders', 'websiteProjects', 'contractEvents', 'shopConfig', 'ukEinstellungen', 'termineBookings', 'shopOrders', 'newsPosts', 'ukAnfragen'] const leere = async (...collections) => { for (const col of collections) for (const id of aw.docs(col).keys()) await deleteDocument(col, id) } const projekt = (id, over = {}) => aw.seed('websiteProjects', { $id: id, customerId: 'c1', projectName: `Projekt ${id}`, subdomain: id, repoFullName: `JUSN/${id}`, previewMode: 'auto', previewDetected: 'static', ...over }) const fid = (p, key) => `f_${p}_${key}` const funktion = (p, key, state = 'active', over = {}) => aw.seed('portalFeatures', { $id: fid(p, key), customerId: 'c1', projectId: p, featureKey: key, siteKey: `${key}${p}`.padEnd(16, 'x').slice(0, 16), state, enabled: state === 'active', ...over }) const NAMEN = ['news', 'termine', 'shop', 'unterkunft', 'zeiten'] const alleAktiv = (p) => NAMEN.forEach((k) => funktion(p, k)) const inStd = (h) => new Date(JETZT.getTime() + h * 3600000).toISOString() let zaehler = 0 const nr = () => `d${(zaehler += 1)}` const buchung = (p, over) => aw.seed('termineBookings', { $id: nr(), featureId: fid(p, 'termine'), customerId: 'c1', projectId: p, status: 'bestaetigt', quelle: 'online', endsAt: inStd(1), blockEndsAt: inStd(1), ...over }) const bestellung = (p, status, over = {}) => aw.seed('shopOrders', { $id: nr(), shopId: fid(p, 'shop'), customerId: 'c1', status, bestellnummer: nr(), summeCent: 1000, erstelltAt: inStd(-5), ...over }) const zettel = (p, over = {}) => aw.seed('newsPosts', { $id: nr(), customerId: 'c1', projectId: p, type: 'note', title: 'Hallo', text: 'Text', paper: 'sun', duration: 'open', visibleFrom: inStd(-48), visibleUntil: null, takenDownAt: null, createdAt: inStd(-48), updatedAt: inStd(-48), ...over }) const anfrage = (p, status, von, bis, over = {}) => aw.seed('ukAnfragen', { $id: nr(), featureId: fid(p, 'unterkunft'), customerId: 'c1', projectId: p, unterkunftId: 'u1', nummer: nr(), von, bis, status, personen: 2, ...over }) function plan(woche) { const tage = {} for (let d = 1; d <= 7; d += 1) tage[d] = woche[d] ? { modus: 'zeiten', fenster: [woche[d]] } : { modus: 'zu', fenster: [] } return { id: 'p1', kurz: 'standard', name: 'Standard', haupt: true, tage } } const zeitenDaten = (woche) => ({ land: 'RP', plaene: [plan(woche)], feiertage: {}, ausnahmen: [], hinweise: [], kontakt: { an: false, telefon: '', adresse: '', whatsapp: null } }) const zeitenDok = (p, daten) => aw.seed('oeffnungszeiten', { $id: fid(p, 'zeiten'), customerId: 'c1', projectId: p, featureId: fid(p, 'zeiten'), version: 1, teamVersion: 0, daten: JSON.stringify(daten) }) const holen = async (cookie = c1) => (await get('/api/funktionen?kurzinfo=1', { cookie })) // so fragt „Meine“ (kurzinfo nur auf Anfrage) const holenOhne = async (pfad = '/api/funktionen', cookie = c1) => (await get(pfad, { cookie })) // Etagen, Einschalt-Ablauf, Module const info = (b, p, key) => b.projects.find((x) => x.projectId === p).features[key].kurzinfo const abfragen = (sammlung) => aw.listCalls(sammlung) beforeEach(async () => { await leere(...SAMMLUNGEN) resetZeitenCacheForTests() kurzinfoDeps.limitMs = 1500 zaehler = 0 seedFixtures(aw) aw.seed('subscriptions', subscriptionDoc('sub_h', { ...wartungDoc })) projekt('p1') config.kundenansichtNeu.mode = 'pilot' config.kundenansichtNeu.pilotEmails = ['k@example.invalid'] config.termine.mode = 'on' config.shop.mode = 'on' config.unterkunft.mode = 'on' config.zeiten.mode = 'pilot' config.zeiten.pilotEmails = ['*'] config.analyse.enabled = false einschaltenDeps.jetzt = () => JETZT setMailTransportForTests(mailSink()) mock.method(console, 'error', () => {}) mock.method(console, 'warn', () => {}) mock.method(console, 'log', () => {}) aw.resetListCalls() }) afterEach(() => { mock.restoreAll() einschaltenDeps.jetzt = () => new Date() }) /* ------------------------------------------------------------------------------------------ Terminplaner --- */ test('Terminplaner: Termine von heute (Berliner Tag), nächster nach jetzt; Sperren, Abgesagte, andere Tage und andere Funktionen zählen nicht', async () => { funktion('p1', 'termine'); projekt('p2'); funktion('p2', 'termine') buchung('p1', { status: 'erledigt', startsAt: '2026-10-17T06:00:00.000Z' }) // 08:00 Berlin, schon vorbei, zählt buchung('p1', { startsAt: '2026-10-17T08:30:00.000Z' }) // 10:30, der nächste buchung('p1', { startsAt: '2026-10-17T12:00:00.000Z' }) // 14:00 buchung('p1', { status: 'abgesagt', startsAt: '2026-10-17T09:00:00.000Z' }) buchung('p1', { quelle: 'sperre', startsAt: '2026-10-17T09:30:00.000Z' }) buchung('p1', { status: 'angefragt', startsAt: '2026-10-17T10:00:00.000Z' }) buchung('p1', { startsAt: '2026-10-18T08:00:00.000Z' }) // morgen buchung('p1', { startsAt: '2026-10-16T21:30:00.000Z' }) // 23:30 Berlin, gestern buchung('p2', { startsAt: '2026-10-17T07:00:00.000Z' }) // andere Website const b = await holen() assert.equal(info(b, 'p1', 'termine'), '3 Termine heute · nächster 10:30') assert.equal(info(b, 'p2', 'termine'), '1 Termin heute') }) test('Terminplaner: ein Termin, alle vorbei, keiner', async () => { funktion('p1', 'termine') assert.equal(info(await holen(), 'p1', 'termine'), 'Heute keine Termine') buchung('p1', { startsAt: '2026-10-17T11:00:00.000Z' }) assert.equal(info(await holen(), 'p1', 'termine'), '1 Termin heute · nächster 13:00') await leere('termineBookings') buchung('p1', { status: 'erledigt', startsAt: '2026-10-17T05:00:00.000Z' }) buchung('p1', { status: 'erledigt', startsAt: '2026-10-17T06:00:00.000Z' }) assert.equal(info(await holen(), 'p1', 'termine'), '2 Termine heute') }) test('Terminplaner: der Tag ist der Berliner Tag, auch kurz nach Mitternacht (UTC liegt noch beim Vortag)', async () => { funktion('p1', 'termine') einschaltenDeps.jetzt = () => new Date('2026-10-17T22:30:00.000Z') // 00:30 Berlin am 18.10. buchung('p1', { startsAt: '2026-10-17T21:59:00.000Z' }) // 23:59 am 17.10., nicht heute buchung('p1', { startsAt: '2026-10-17T22:15:00.000Z' }) // 00:15 am 18.10., zählt, vorbei buchung('p1', { startsAt: '2026-10-18T06:00:00.000Z' }) // 08:00, nächster buchung('p1', { startsAt: '2026-10-18T22:00:00.000Z' }) // 00:00 am 19.10., nicht heute assert.equal(info(await holen(), 'p1', 'termine'), '2 Termine heute · nächster 08:00') }) /* ------------------------------------------------------------------------------------------ Webshop --- */ test('Webshop: neue (bezahlte, noch nicht bearbeitete) Bestellungen dieses Shops', async () => { funktion('p1', 'shop'); projekt('p2'); funktion('p2', 'shop') assert.equal(info(await holen(), 'p1', 'shop'), 'Keine offenen Bestellungen') bestellung('p1', 'bezahlt'); bestellung('p1', 'bezahlt') bestellung('p1', 'versendet'); bestellung('p1', 'offen'); bestellung('p1', 'abgeschlossen'); bestellung('p1', 'storniert') bestellung('p2', 'bezahlt') let b = await holen() assert.equal(info(b, 'p1', 'shop'), '2 neue Bestellungen') assert.equal(info(b, 'p2', 'shop'), '1 neue Bestellung') }) /* ------------------------------------------------------------------------------------------ News --- */ test('News-Pinnwand: sichtbare Beiträge (nicht abgenommen, begonnen, nicht abgelaufen) dieser Website', async () => { funktion('p1', 'news'); projekt('p2'); funktion('p2', 'news') assert.equal(info(await holen(), 'p1', 'news'), 'Keine Beiträge sichtbar') zettel('p1') // ohne Ende zettel('p1', { visibleFrom: inStd(-24), visibleUntil: inStd(72) }) zettel('p1', { visibleFrom: inStd(-240), visibleUntil: inStd(1) }) zettel('p1', { visibleFrom: inStd(-120), visibleUntil: inStd(480) }) zettel('p1', { visibleFrom: inStd(-240), visibleUntil: inStd(-1) }) // abgelaufen zettel('p1', { takenDownAt: inStd(-2) }) // abgenommen zettel('p1', { visibleFrom: inStd(24) }) // beginnt erst morgen zettel('p2') const b = await holen() assert.equal(info(b, 'p1', 'news'), '4 Beiträge sichtbar') assert.equal(info(b, 'p2', 'news'), '1 Beitrag sichtbar') }) /* ------------------------------------------------------------------------------------------ Buchungskalender --- */ test('Buchungskalender: offene Anfragen, sonst die nächste Anreise, sonst keine offenen Anfragen', async () => { funktion('p1', 'unterkunft'); projekt('p2'); funktion('p2', 'unterkunft') assert.equal(info(await holen(), 'p1', 'unterkunft'), 'Keine offenen Anfragen') anfrage('p1', 'gebucht', '2026-11-02', '2026-11-05') anfrage('p1', 'bezahlt', '2026-10-24', '2026-10-27') anfrage('p1', 'gebucht', '2026-10-01', '2026-10-05') // vorbei anfrage('p1', 'gebucht', '2026-10-15', '2026-10-19') // läuft schon: keine Anreise mehr anfrage('p1', 'freigegeben', '2026-10-20', '2026-10-22') // noch nicht gebucht anfrage('p1', 'abgelehnt', '2026-10-18', '2026-10-20') assert.equal(info(await holen(), 'p1', 'unterkunft'), 'Nächste Anreise 24.10.') anfrage('p1', 'neu', '2026-12-01', '2026-12-03') anfrage('p2', 'neu', '2026-12-01', '2026-12-03') anfrage('p2', 'neu', '2026-12-05', '2026-12-07') const b = await holen() assert.equal(info(b, 'p1', 'unterkunft'), '1 offene Anfrage') assert.equal(info(b, 'p2', 'unterkunft'), '2 offene Anfragen') }) test('Buchungskalender: Anreise heute und morgen werden ausgeschrieben', async () => { funktion('p1', 'unterkunft') anfrage('p1', 'gebucht', '2026-10-18', '2026-10-20') assert.equal(info(await holen(), 'p1', 'unterkunft'), 'Anreise morgen') anfrage('p1', 'bezahlt', '2026-10-17', '2026-10-19') assert.equal(info(await holen(), 'p1', 'unterkunft'), 'Anreise heute') }) /* ------------------------------------------------------------------------------------------ Öffnungszeiten --- */ test('Öffnungszeiten live: der Status der Website von jetzt (ohne Zusatzabfrage), ohne Zeiten ein Hinweis', async () => { funktion('p1', 'zeiten') assert.equal(info(await holen(), 'p1', 'zeiten'), 'Noch keine Öffnungszeiten eingetragen') zeitenDok('p1', zeitenDaten({ 6: ['09:00', '18:00'], 1: ['09:00', '18:00'] })) resetZeitenCacheForTests() aw.resetListCalls() assert.equal(info(await holen(), 'p1', 'zeiten'), 'Jetzt geöffnet · schließt um 18 Uhr') einschaltenDeps.jetzt = () => new Date('2026-10-17T18:00:00.000Z') // Samstag 20:00 assert.equal(info(await holen(), 'p1', 'zeiten'), 'Geschlossen · öffnet Montag um 9 Uhr') }) /* ------------------------------------------------------------------------------------------ Regeln --- */ test('nur aktive Funktionen bekommen einen Text und eine Abfrage; alle anderen null', async () => { for (const [key, state] of [['news', 'setup'], ['termine', 'ordered'], ['shop', 'disabled'], ['unterkunft', 'off'], ['zeiten', 'setup']]) funktion('p1', key, state) buchung('p1', { startsAt: '2026-10-17T08:30:00.000Z' }) bestellung('p1', 'bezahlt') zettel('p1') anfrage('p1', 'neu', '2026-12-01', '2026-12-03') zeitenDok('p1', zeitenDaten({ 6: ['09:00', '18:00'] })) aw.resetListCalls() const b = await holen() for (const key of NAMEN) assert.equal(info(b, 'p1', key), null, key) for (const key of NAMEN) assert.ok(Object.hasOwn(b.projects[0].features[key], 'kurzinfo'), `${key}: Feld da`) for (const sammlung of ['termineBookings', 'shopOrders', 'newsPosts', 'ukAnfragen']) assert.equal(abfragen(sammlung).length, 0, sammlung) }) test('eine kleine feste Zahl schmaler Abfragen je aktiver Funktion: limit gesetzt, nur nötige Felder, nur Filter dieser Funktion', async () => { alleAktiv('p1') const gesamt = () => ['termineBookings', 'shopOrders', 'newsPosts', 'ukAnfragen'].map((s) => [s, abfragen(s)]) aw.resetListCalls() await holen() const [termine, shop, news, uk] = gesamt().map(([, a]) => a) assert.equal(termine.length, 1) assert.equal(shop.length, 1) assert.equal(news.length, 1) assert.equal(uk.length, 2) const hat = (aufruf, methode) => aufruf.queries.find((q) => q.method === methode) for (const aufruf of [...termine, ...news, ...uk]) { const limit = hat(aufruf, 'limit') assert.ok(limit && limit.values[0] <= 200, `limit ${JSON.stringify(limit)}`) } assert.deepEqual(hat(shop[0], 'limit').values, [1]) // nur die Anzahl (total) assert.ok(hat(termine[0], 'select') && hat(news[0], 'select')) assert.deepEqual(hat(termine[0], 'equal').values, ['f_p1_termine']) assert.deepEqual(hat(shop[0], 'equal').values, ['f_p1_shop']) assert.deepEqual(hat(news[0], 'equal').values, ['p1']) for (const aufruf of uk) assert.deepEqual(hat(aufruf, 'equal').values, ['f_p1_unterkunft']) assert.equal(aw.listCalls('oeffnungszeiten').length <= 1, true) // Zeiten: höchstens das Lesen für zeitenStatus, nichts zusätzlich }) test('mehrere Websites: jede bekommt ihre eigene Zeile, die Abfragen laufen für alle gleichzeitig', async () => { alleAktiv('p1'); projekt('p2'); alleAktiv('p2'); projekt('p3'); funktion('p3', 'termine') buchung('p1', { startsAt: '2026-10-17T08:30:00.000Z' }) buchung('p3', { startsAt: '2026-10-17T07:00:00.000Z' }) buchung('p3', { startsAt: '2026-10-17T13:00:00.000Z' }) const b = await holen() assert.deepEqual(['p1', 'p2', 'p3'].map((p) => info(b, p, 'termine')).sort(), ['1 Termin heute · nächster 10:30', '2 Termine heute · nächster 15:00', 'Heute keine Termine'].sort()) assert.equal(info(b, 'p3', 'shop'), null) }) test('Fehler einer Einzelabfrage ergibt null für diese Funktion; der Rest der Liste bleibt', async () => { alleAktiv('p1') buchung('p1', { startsAt: '2026-10-17T08:30:00.000Z' }) bestellung('p1', 'bezahlt') zettel('p1') aw.failNext({ collection: 'termineBookings', method: 'GET', status: 500, message: 'geheimer Gästename im Fehlertext' }) const b = await holen() assert.equal(info(b, 'p1', 'termine'), null) assert.equal(info(b, 'p1', 'shop'), '1 neue Bestellung') assert.equal(info(b, 'p1', 'news'), '1 Beitrag sichtbar') assert.equal(b.modell, 'abo') assert.ok(Array.isArray(b.katalog)) const log = console.error.mock.calls.map((c) => c.arguments.join(' ')).join('\n') assert.match(log, /Kurzinfo \(termine\)/) assert.doesNotMatch(log, /geheimer/) // ein Teilfehler im Buchungskalender (eine der zwei Abfragen) ergibt null, nie eine halbe Aussage anfrage('p1', 'neu', '2026-12-01', '2026-12-03') aw.failNext({ collection: 'ukAnfragen', method: 'GET', status: 500 }) assert.equal(info(await holen(), 'p1', 'unterkunft'), null) }) test('Zeitüberschreitung ergibt null, ohne die Liste aufzuhalten', async () => { alleAktiv('p1') bestellung('p1', 'bezahlt'); zettel('p1') kurzinfoDeps.limitMs = 60 const echt = globalThis.fetch mock.method(globalThis, 'fetch', async (url, ...rest) => { if (String(url).includes('/shopOrders/')) await new Promise((r) => setTimeout(r, 600)) return echt(url, ...rest) }) const start = Date.now() const b = await holen() assert.ok(Date.now() - start < 550, `Antwort dauerte ${Date.now() - start} ms`) assert.equal(info(b, 'p1', 'shop'), null) assert.equal(info(b, 'p1', 'news'), '1 Beitrag sichtbar') await new Promise((r) => setTimeout(r, 700)) // die späte Antwort richtet nichts mehr an }) test('ohne neue Ansicht: keine Kurzinfo in der Antwort, keine einzige Abfrage dafür; sonst ist die neue Antwort die alte plus ergänzte Felder', async () => { alleAktiv('p1') buchung('p1', { startsAt: '2026-10-17T08:30:00.000Z' }); bestellung('p1', 'bezahlt'); zettel('p1'); anfrage('p1', 'neu', '2026-12-01', '2026-12-03') config.kundenansichtNeu.mode = 'off' aw.resetListCalls() const alt = await holen() // auch mit ?kurzinfo=1 ändert die alte Ansicht nichts assert.doesNotMatch(JSON.stringify(alt), /kurzinfo/) for (const sammlung of ['termineBookings', 'shopOrders', 'newsPosts', 'ukAnfragen']) assert.equal(abfragen(sammlung).length, 0, sammlung) assert.deepEqual(await holenOhne(), alt) config.kundenansichtNeu.mode = 'pilot' config.kundenansichtNeu.pilotEmails = ['k2@example.invalid'] // Kunde nicht in der Liste assert.deepEqual(await holen(), alt) config.kundenansichtNeu.pilotEmails = ['k@example.invalid'] const neu = await holen() for (const key of ['modell', 'kundenart', 'abo', 'katalog']) delete neu[key] for (const p of neu.projects) for (const f of Object.values(p.features)) { delete f.kurzinfo; delete f.anfrageStatus } assert.deepEqual(neu, alt) }) /* ------------------------------------------------------------------------------------ nur auf Anfrage --- */ test('Kurzinfo nur auf Anfrage: ohne ?kurzinfo=1 fehlt das Feld überall und es gibt keine einzige Abfrage dafür; der Rest der Antwort ist gleich', async () => { alleAktiv('p1'); projekt('p2'); funktion('p2', 'termine'); funktion('p2', 'news', 'setup') buchung('p1', { startsAt: '2026-10-17T08:30:00.000Z' }); bestellung('p1', 'bezahlt'); zettel('p1'); anfrage('p1', 'neu', '2026-12-01', '2026-12-03') zeitenDok('p1', zeitenDaten({ 6: ['09:00', '18:00'] })) const sammlungen = ['termineBookings', 'shopOrders', 'newsPosts', 'ukAnfragen'] for (const pfad of ['/api/funktionen', '/api/funktionen?kurzinfo=0', '/api/funktionen?kurzinfo=', '/api/funktionen?kurzinfo=true', '/api/funktionen?kurzinfo=1&kurzinfo=0', '/api/funktionen?kurzinfo[]=1', '/api/funktionen?x=1']) { aw.resetListCalls() const ohne = await holenOhne(pfad) assert.doesNotMatch(JSON.stringify(ohne), /kurzinfo/, pfad) assert.equal(ohne.modell, 'abo', pfad) // die neue Ansicht bekommt sonst alles wie bisher for (const sammlung of sammlungen) assert.equal(abfragen(sammlung).length, 0, `${pfad}: ${sammlung}`) } aw.resetListCalls() const mit = await holen() for (const sammlung of sammlungen) assert.ok(abfragen(sammlung).length > 0, sammlung) assert.equal(info(mit, 'p1', 'termine'), '1 Termin heute · nächster 10:30') assert.equal(info(mit, 'p2', 'news'), null) // mit Anfrage haben auch Funktionen ohne Text das Feld (null) const ohne = await holenOhne() for (const p of mit.projects) for (const f of Object.values(p.features)) delete f.kurzinfo assert.deepEqual(ohne, mit) }) test('die Kurzinfo ist ein kurzer Text ohne Preis, Mail-Adresse oder Namen von Gästen', async () => { alleAktiv('p1') buchung('p1', { startsAt: '2026-10-17T08:30:00.000Z', gastName: 'Erika Beispiel', gastEmail: 'erika@example.invalid', preisCent: 4500 }) bestellung('p1', 'bezahlt', { kaeuferEmail: 'kauf@example.invalid', summeCent: 9900 }) zettel('p1', { title: 'Geheimer Titel' }) anfrage('p1', 'neu', '2026-12-01', '2026-12-03', { gast: JSON.stringify({ name: 'Max Gast' }) }) const b = await holen() const texte = NAMEN.map((k) => info(b, 'p1', k)) for (const t of texte) { assert.equal(typeof t, 'string') assert.ok(t.length > 0 && t.length <= 60, t) assert.doesNotMatch(t, /€|Euro|@|Erika|Max|Geheimer|\d+,\d\d/) } })