oeffentlich.js baut die Antwort für das Website-Skript (Pläne mit Woche, 60 Tagen
und Statuswechseln, Hinweise, Kontakt-Leiste, JSON-LD, Firma, Team) und merkt sie
30 Sekunden je siteKey. Ungültige Schlüssel und ausgeschalteter Schalter lösen
weder eine Abfrage noch einen Cache-Eintrag aus; die Funktion antwortet schon in
setup. findActiveBySiteKey nimmt dafür { states } entgegen (Vorgabe wie bisher
nur active).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
540 lines
29 KiB
JavaScript
540 lines
29 KiB
JavaScript
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 { useTestEnv } from './support/testEnv.js'
|
|
import { ZEITEN_DATETIMES } from '../server/services/zeiten/schema.js'
|
|
|
|
// Oeffentliche Antwort der Oeffnungszeiten (server/services/zeiten/oeffentlich.js, Spezifikation 2026-10-04-oeffnungszeiten-live-design.md,
|
|
// Abschnitt 4.1): baueOeffentlicheAntwort ist rein, oeffentlicheZeitenAntwort prueft Schalter und Schluessel, holt Funktion, Kunde und
|
|
// Zeiten aus dem Appwrite-Nachbau und legt die Antwort 30 Sekunden im Speicher ab.
|
|
const aw = await startAppwriteMock({
|
|
schema: {
|
|
portalFeatures: schemaKeys('portalFeatures'),
|
|
oeffnungszeiten: schemaKeys('oeffnungszeiten'),
|
|
zeitenErinnerungen: schemaKeys('zeitenErinnerungen'),
|
|
},
|
|
datetimes: ZEITEN_DATETIMES,
|
|
defaultLimit: 25,
|
|
})
|
|
after(() => aw.close())
|
|
useTestEnv({ appwriteUrl: aw.url, stripeUrl: 'http://127.0.0.1:1' })
|
|
|
|
const { config } = await import('../server/config.js')
|
|
const { deleteDocument } = await import('../server/services/appwriteAdmin.js')
|
|
const { zeitenCache, resetZeitenCacheForTests } = await import('../server/services/zeiten/cache.js')
|
|
const { speichereZeiten, speichereTeam } = await import('../server/services/zeiten/speicher.js')
|
|
const { baueOeffentlicheAntwort, oeffentlicheZeitenAntwort } = await import('../server/services/zeiten/oeffentlich.js')
|
|
|
|
const JETZT = new Date('2026-10-05T10:00:00.000Z') // Berlin: Montag 2026-10-05 12:00
|
|
const SITE_KEY = 'abcdabcdabcdabcd'
|
|
const KUNDE = 'c_kunde01'
|
|
const PROJEKT = 'p_projekt01'
|
|
const FEATURE_ID = 'f_p_projekt01_zeiten'
|
|
const P_STANDARD = 'p_aaaaaaa1'
|
|
const P_KUECHE = 'p_aaaaaaa2'
|
|
const ADRESSE = 'Hauptstraße 1, 67722 Winnweiler'
|
|
const STARTTEXT = 'Hallo, ich habe eine Frage & möchte einen Termin!'
|
|
const MODUS_VORHER = config.zeiten.mode
|
|
|
|
const FEATURE = { $id: FEATURE_ID, customerId: KUNDE, projectId: PROJEKT, siteKey: SITE_KEY, featureKey: 'zeiten', state: 'active', enabled: true }
|
|
|
|
const tage = (woche = {}) => {
|
|
const t = {}
|
|
for (let d = 1; d <= 7; d += 1) t[d] = woche[d] ? { modus: 'zeiten', fenster: [woche[d]] } : { modus: 'zu', fenster: [] }
|
|
return t
|
|
}
|
|
|
|
const eingabe = (extra = {}) => ({
|
|
land: 'RP',
|
|
plaene: [
|
|
{ id: P_STANDARD, kurz: 'standard', name: 'Standard', haupt: true, tage: tage({ 1: ['09:00', '18:00'], 2: ['09:00', '18:00'], 3: ['09:00', '18:00'], 4: ['09:00', '18:00'], 5: ['09:00', '18:00'], 6: ['09:00', '12:00'] }) },
|
|
{ id: P_KUECHE, kurz: 'kueche', name: 'Küche', haupt: false, tage: tage({ 1: ['11:00', '14:00'] }) },
|
|
],
|
|
feiertage: {},
|
|
ausnahmen: [{ id: 'x_urlaub01', art: 'urlaub', von: '2026-10-12', bis: '2026-10-16', text: 'Betriebsurlaub', plan: null }],
|
|
hinweise: [
|
|
{ id: 'h_dauer001', art: 'info', text: 'Wir sind für Euch da.', knopf: null }, // ohne von/bis: ohne Ende
|
|
{ id: 'h_aktion01', art: 'aktion', text: 'Herbstaktion', knopf: { art: 'link', ziel: 'https://bäckerei.example/aktion', text: 'Zur Aktion' }, von: '2026-10-10', bis: '2026-10-20' },
|
|
{ id: 'h_spaet001', art: 'info', text: 'Weihnachtsöffnung', knopf: null, von: '2026-12-01', bis: '2026-12-24' }, // beginnt erst in 57 Tagen
|
|
{ id: 'h_vorbei01', art: 'info', text: 'Alt', knopf: null, von: '2026-09-01', bis: '2026-09-02' },
|
|
],
|
|
kontakt: {
|
|
an: true,
|
|
telefon: '0171 1234567',
|
|
adresse: ADRESSE,
|
|
whatsapp: { nummer: '+49 171 1234567', starttext: STARTTEXT, bestaetigtAt: '2026-10-01T08:00:00.000Z' },
|
|
},
|
|
...extra,
|
|
})
|
|
|
|
/** Gespeicherte, normalisierte Daten wie in Appwrite (Weg ueber speichereZeiten, wie im Betrieb). */
|
|
async function gespeicherteDaten(extra = {}) {
|
|
return (await speichereZeiten({ feature: FEATURE, eingabe: eingabe(extra), version: 0, by: 'kunde', now: JETZT })).daten
|
|
}
|
|
|
|
const TEAM = { navSelektor: '#kopf', kontaktleisteAus: false, hinweisleisteAus: true }
|
|
const FIRMA = { name: 'Bäckerei Sonnenschein' } // der Aufrufer liefert den Namen, die Adresse kommt aus den Kontaktangaben der Daten
|
|
|
|
const baue = (daten, extra = {}) => baueOeffentlicheAntwort({ daten, team: TEAM, firma: FIRMA, now: JETZT, ...extra })
|
|
|
|
/** Funktion, Kunde und Projekt des Normalfalls in den Nachbau legen. */
|
|
function legeAn({ feature = {}, kunde = { name: 'Frau Sonne', companyName: 'Bäckerei Sonnenschein' }, projekt = { projectName: 'Sonnenschein-Website' } } = {}) {
|
|
aw.seed('portalFeatures', { ...FEATURE, ...feature })
|
|
if (kunde) aw.seed('customers', { $id: KUNDE, ...kunde })
|
|
if (projekt) aw.seed('websiteProjects', { $id: PROJEKT, customerId: KUNDE, ...projekt })
|
|
}
|
|
|
|
const listen = () => aw.listCalls('portalFeatures').length
|
|
|
|
beforeEach(async () => {
|
|
for (const collection of ['portalFeatures', 'oeffnungszeiten', 'zeitenErinnerungen', 'customers', 'websiteProjects']) {
|
|
for (const id of [...aw.docs(collection).keys()]) await deleteDocument(collection, id)
|
|
}
|
|
config.zeiten.mode = 'on'
|
|
resetZeitenCacheForTests()
|
|
aw.resetListCalls()
|
|
mock.method(console, 'error', () => {})
|
|
mock.method(console, 'warn', () => {})
|
|
})
|
|
afterEach(() => {
|
|
config.zeiten.mode = MODUS_VORHER
|
|
mock.restoreAll()
|
|
})
|
|
|
|
/** Alle Schluessel und Zeichenketten einer Antwort (rekursiv). */
|
|
function durchsuche(wert, schluessel = new Set(), werte = []) {
|
|
if (Array.isArray(wert)) wert.forEach((e) => durchsuche(e, schluessel, werte))
|
|
else if (wert && typeof wert === 'object') {
|
|
for (const [k, v] of Object.entries(wert)) {
|
|
schluessel.add(k)
|
|
durchsuche(v, schluessel, werte)
|
|
}
|
|
} else if (typeof wert === 'string') werte.push(wert)
|
|
return { schluessel, werte }
|
|
}
|
|
|
|
// ---------------------------------------------------------------- baueOeffentlicheAntwort (rein)
|
|
|
|
test('Form: aktiv, stand, plaene, hinweise, kontakt, jsonld, firma, team (nach Spec 4.1)', async () => {
|
|
const a = baue(await gespeicherteDaten())
|
|
assert.deepEqual(Object.keys(a), ['aktiv', 'stand', 'plaene', 'hinweise', 'kontakt', 'jsonld', 'firma', 'team'])
|
|
assert.equal(a.aktiv, true)
|
|
assert.equal(a.stand, JETZT.toISOString())
|
|
assert.deepEqual(a.firma, { name: 'Bäckerei Sonnenschein', adresse: ADRESSE })
|
|
assert.deepEqual(a.team, TEAM)
|
|
assert.equal(JSON.parse(JSON.stringify(a)).plaene.length, 2, 'ist JSON-tauglich')
|
|
})
|
|
|
|
test('Plaene: kurz, name, haupt, woche (7 Tage), tage (60 Tage ab heute), wechsel (nur ab, offen, text, kurz)', async () => {
|
|
const a = baue(await gespeicherteDaten())
|
|
const [standard, kueche] = a.plaene
|
|
assert.deepEqual(Object.keys(standard), ['kurz', 'name', 'haupt', 'woche', 'tage', 'wechsel'])
|
|
assert.deepEqual([standard.kurz, standard.name, standard.haupt], ['standard', 'Standard', true])
|
|
assert.deepEqual([kueche.kurz, kueche.name, kueche.haupt], ['kueche', 'Küche', false])
|
|
|
|
assert.deepEqual(standard.woche.map((t) => t.tag), [1, 2, 3, 4, 5, 6, 7])
|
|
assert.deepEqual(standard.woche[0], { tag: 1, modus: 'zeiten', fenster: [['09:00', '18:00']] })
|
|
assert.deepEqual(standard.woche[6], { tag: 7, modus: 'zu', fenster: [] })
|
|
assert.deepEqual(kueche.woche[0].fenster, [['11:00', '14:00']], 'jeder Plan hat seine eigene Woche')
|
|
|
|
assert.equal(standard.tage.length, 60)
|
|
assert.equal(standard.tage[0].datum, '2026-10-05', 'heute (Berliner Tag)')
|
|
assert.equal(standard.tage[59].datum, '2026-12-03')
|
|
assert.deepEqual(Object.keys(standard.tage[0]), ['datum', 'art', 'name', 'modus', 'fenster'])
|
|
const urlaub = standard.tage.find((t) => t.datum === '2026-10-12')
|
|
assert.deepEqual([urlaub.art, urlaub.name, urlaub.modus], ['urlaub', 'Betriebsurlaub', 'zu'])
|
|
assert.equal(standard.tage.find((t) => t.datum === '2026-10-03'), undefined, 'gestern ist nicht dabei')
|
|
assert.equal(standard.tage.find((t) => t.datum === '2026-11-01')?.art, 'feiertag', 'Allerheiligen (RP)')
|
|
|
|
assert.ok(standard.wechsel.length >= 8, 'mindestens je Tag ein Wechsel')
|
|
assert.deepEqual(Object.keys(standard.wechsel[0]), ['ab', 'offen', 'text', 'kurz'])
|
|
assert.equal(typeof standard.wechsel[0].ab, 'number')
|
|
assert.equal(standard.wechsel[0].offen, true, 'Montag 12:00 ist offen')
|
|
assert.match(standard.wechsel[0].text, /geöffnet/i)
|
|
assert.ok(standard.wechsel.every((w, i, l) => i === 0 || w.ab > l[i - 1].ab), 'aufsteigend')
|
|
assert.ok(standard.wechsel.at(-1).ab < JETZT.getTime() + 8 * 24 * 3600 * 1000 + 3600 * 1000, 'hoechstens 8 Tage')
|
|
assert.notDeepEqual(kueche.wechsel, standard.wechsel, 'Wechsel je Plan')
|
|
})
|
|
|
|
test('Hinweise: gueltige und solche mit Beginn in 8 Tagen, ohne IDs; offene Enden bleiben Zahlen', async () => {
|
|
const a = baue(await gespeicherteDaten())
|
|
assert.deepEqual(a.hinweise.map((h) => h.text), ['Wir sind für Euch da.', 'Herbstaktion'], 'weder der ferne noch der vergangene')
|
|
assert.deepEqual(Object.keys(a.hinweise[0]), ['art', 'text', 'knopf', 'ab', 'bis', 'rang'])
|
|
assert.deepEqual([a.hinweise[0].ab, a.hinweise[0].bis, a.hinweise[0].rang, a.hinweise[0].knopf], [0, 8.64e15, 1, null])
|
|
const aktion = a.hinweise[1]
|
|
assert.equal(aktion.art, 'aktion')
|
|
assert.equal(aktion.rang, 2)
|
|
assert.deepEqual(aktion.knopf, { art: 'link', ziel: 'https://xn--bckerei-5wa.example/aktion', text: 'Zur Aktion' })
|
|
assert.equal(aktion.ab, Date.parse('2026-10-09T22:00:00.000Z'), 'Beginn 10.10. 00:00 Berliner Zeit (Sommerzeit)')
|
|
assert.equal(aktion.bis, Date.parse('2026-10-20T22:00:00.000Z'), 'Ende 20.10. 24:00 = 21.10. 00:00')
|
|
assert.equal(JSON.parse(JSON.stringify(a.hinweise))[0].bis, 8.64e15)
|
|
})
|
|
|
|
test('JSON-LD: Wochenzeiten und Sonderzeiten des Hauptplans samt neuem Block', async () => {
|
|
const a = baue(await gespeicherteDaten())
|
|
const { openingHoursSpecification: woche, specialOpeningHoursSpecification: sonder, neuerBlock } = a.jsonld
|
|
assert.deepEqual(woche.find((e) => e.dayOfWeek === 'Monday'), { '@type': 'OpeningHoursSpecification', dayOfWeek: 'Monday', opens: '09:00', closes: '18:00' })
|
|
assert.equal(woche.find((e) => e.dayOfWeek === 'Sunday'), undefined)
|
|
assert.deepEqual(
|
|
sonder.find((e) => e.validFrom === '2026-10-12'),
|
|
{ '@type': 'OpeningHoursSpecification', validFrom: '2026-10-12', validThrough: '2026-10-16', opens: '00:00', closes: '00:00' },
|
|
)
|
|
assert.equal(neuerBlock['@context'], 'https://schema.org', 'das https-Literal liefert der Server, das Skript hat keins')
|
|
assert.equal(neuerBlock['@type'], 'LocalBusiness')
|
|
assert.deepEqual(neuerBlock.openingHoursSpecification, woche)
|
|
assert.ok(!('name' in neuerBlock) && !('address' in neuerBlock) && !('url' in neuerBlock))
|
|
})
|
|
|
|
test('JSON-LD folgt dem Hauptplan, auch wenn er nicht der erste ist', async () => {
|
|
const daten = await gespeicherteDaten()
|
|
daten.plaene.reverse()
|
|
const a = baue(daten)
|
|
assert.deepEqual(a.plaene.map((p) => p.kurz), ['kueche', 'standard'])
|
|
assert.equal(a.jsonld.openingHoursSpecification.find((e) => e.dayOfWeek === 'Monday').closes, '18:00')
|
|
})
|
|
|
|
test('vor dem ersten Speichern: aktiv true, plaene leer, kein jsonld', () => {
|
|
for (const daten of [null, undefined, {}, { plaene: [] }, 'kaputt']) {
|
|
const a = baue(daten)
|
|
assert.equal(a.aktiv, true)
|
|
assert.deepEqual(a.plaene, [])
|
|
assert.deepEqual(a.hinweise, [])
|
|
assert.equal(a.kontakt, null)
|
|
assert.equal(a.jsonld, null, 'null statt leerer Listen: ein leeres JSON-LD wuerde vorhandenes im Browser ueberschreiben')
|
|
assert.deepEqual(a.firma, { name: 'Bäckerei Sonnenschein', adresse: null })
|
|
assert.deepEqual(a.team, TEAM, 'Team-Einstellungen gelten auch ohne Zeiten')
|
|
}
|
|
})
|
|
|
|
test('ohne Hauptplan kein JSON-LD, die Plaene bleiben', async () => {
|
|
const daten = await gespeicherteDaten()
|
|
for (const p of daten.plaene) p.haupt = false
|
|
const a = baue(daten)
|
|
assert.equal(a.jsonld, null)
|
|
assert.deepEqual(a.plaene.map((p) => p.haupt), [false, false])
|
|
})
|
|
|
|
test('Kontakt-Leiste mit Teildaten', () => {
|
|
const kontakt = (extra) => ({ an: true, telefon: '', adresse: '', whatsapp: null, ...extra })
|
|
const nur = baue({ plaene: [], kontakt: kontakt({ telefon: '491711234567' }) }).kontakt
|
|
assert.deepEqual(nur, { telefon: '491711234567', routeUrl: null, whatsappUrl: null })
|
|
|
|
const adresse = baue({ plaene: [], kontakt: kontakt({ adresse: ADRESSE }) }).kontakt
|
|
assert.deepEqual(adresse, { telefon: null, routeUrl: `https://www.google.com/maps/dir/?api=1&destination=${encodeURIComponent(ADRESSE)}`, whatsappUrl: null })
|
|
|
|
const leer = baue({ plaene: [], kontakt: kontakt({}) }).kontakt
|
|
assert.equal(leer, null, 'an: true ohne Angaben ergibt keine Leiste')
|
|
assert.equal(baue({ plaene: [], kontakt: kontakt({ telefon: '491711234567', an: false }) }).kontakt, null, 'ausgeschaltet')
|
|
assert.equal(baue({ plaene: [], kontakt: kontakt({ telefon: ' ', adresse: ' ' }) }).kontakt, null, 'nur Leerraum')
|
|
})
|
|
|
|
test('routeUrl und whatsappUrl', () => {
|
|
const daten = {
|
|
plaene: [],
|
|
kontakt: { an: true, telefon: '491711234567', adresse: ADRESSE, whatsapp: { nummer: '491711234567', starttext: STARTTEXT, bestaetigtAt: '2026-10-01T08:00:00.000Z' } },
|
|
}
|
|
const k = baue(daten).kontakt
|
|
assert.equal(k.telefon, '491711234567')
|
|
assert.equal(k.routeUrl, 'https://www.google.com/maps/dir/?api=1&destination=' + encodeURIComponent(ADRESSE))
|
|
assert.equal(k.whatsappUrl, 'https://wa.me/491711234567?text=' + encodeURIComponent(STARTTEXT))
|
|
assert.ok(!k.whatsappUrl.includes(' ') && !k.whatsappUrl.includes('&möchte'), 'Starttext ist kodiert')
|
|
assert.deepEqual(Object.keys(k), ['telefon', 'routeUrl', 'whatsappUrl'])
|
|
})
|
|
|
|
test('WhatsApp nur mit bestaetigtAt; ohne Starttext bleibt der Text leer', () => {
|
|
const kontakt = (whatsapp, extra = {}) => ({ plaene: [], kontakt: { an: true, telefon: '', adresse: '', whatsapp, ...extra } })
|
|
assert.equal(baue(kontakt({ nummer: '491711234567', starttext: 'Hallo', bestaetigtAt: null })).kontakt, null, 'unbestaetigt und sonst nichts: keine Leiste')
|
|
assert.equal(baue(kontakt({ nummer: '491711234567', starttext: 'Hallo', bestaetigtAt: '' })).kontakt, null)
|
|
const mit = baue(kontakt({ nummer: '491711234567', starttext: 'Hallo', bestaetigtAt: null }, { telefon: '491701111111' })).kontakt
|
|
assert.deepEqual(mit, { telefon: '491701111111', routeUrl: null, whatsappUrl: null })
|
|
const ohneText = baue(kontakt({ nummer: '491711234567', starttext: '', bestaetigtAt: '2026-10-01T08:00:00.000Z' })).kontakt
|
|
assert.equal(ohneText.whatsappUrl, 'https://wa.me/491711234567?text=')
|
|
})
|
|
|
|
test('Telefonnummern gehen nur als Ziffern hinaus (kein tel:-Einschleusen aus beschaedigten Daten)', () => {
|
|
// mit gueltiger Adresse, damit die Leiste bestehen bleibt und die Telefonfelder einzeln zu sehen sind
|
|
const k = (telefon, nummer = '491711234567') =>
|
|
baue({ plaene: [], kontakt: { an: true, telefon, adresse: ADRESSE, whatsapp: { nummer, starttext: '', bestaetigtAt: '2026-10-01T08:00:00.000Z' } } }).kontakt
|
|
assert.equal(k('491711234567; rm', '49171 123').telefon, null)
|
|
assert.equal(k('491711234567; rm', '49171 123').whatsappUrl, null)
|
|
assert.equal(k('+491711234567').telefon, null, 'gespeichert werden nur Ziffern')
|
|
assert.equal(k(491711234567).telefon, null, 'Zahl statt Text')
|
|
assert.equal(k('12345').telefon, null, 'zu kurz fuer eine Nummer mit Vorwahl')
|
|
assert.equal(k('0171123456').telefon, null, 'fuehrende 0: gespeichert wird die Vorwahl 49')
|
|
assert.equal(k('4917112345678901234').telefon, null, 'laenger als 15 Ziffern')
|
|
assert.equal(k('491711234567').telefon, '491711234567')
|
|
assert.equal(k('491711234567\n').telefon, null, 'auch kein Zeilenumbruch hinten')
|
|
})
|
|
|
|
test('ein einzelnes Surrogat in Starttext oder Adresse bringt die Antwort nicht zu Fall', () => {
|
|
const k = baue({
|
|
plaene: [],
|
|
kontakt: { an: true, telefon: '', adresse: 'Hauptstr. \ud800 1', whatsapp: { nummer: '491711234567', starttext: 'Hallo \udc00!', bestaetigtAt: '2026-10-01T08:00:00.000Z' } },
|
|
}).kontakt
|
|
assert.equal(k.whatsappUrl, 'https://wa.me/491711234567?text=Hallo%20%EF%BF%BD!')
|
|
assert.equal(k.routeUrl, 'https://www.google.com/maps/dir/?api=1&destination=Hauptstr.%20%EF%BF%BD%201')
|
|
})
|
|
|
|
test('firma: Name vom Aufrufer, Adresse = kontakt.adresse (getrimmt; leer, fehlend oder kein Text = null)', () => {
|
|
const firma = (kontakt, name = 'X') => baue({ plaene: [], kontakt }, { firma: { name } }).firma
|
|
assert.deepEqual(firma({ an: true, telefon: '', adresse: ` ${ADRESSE} `, whatsapp: null }), { name: 'X', adresse: ADRESSE })
|
|
assert.deepEqual(firma({ an: false, telefon: '', adresse: ADRESSE, whatsapp: null }), { name: 'X', adresse: ADRESSE }, 'unabhaengig von der Kontakt-Leiste (Spec 4.1)')
|
|
for (const kontakt of [{ adresse: ' ' }, { adresse: 5 }, {}, null, 'x']) assert.equal(firma(kontakt).adresse, null, JSON.stringify(kontakt))
|
|
assert.deepEqual(baue(null, { firma: undefined }).firma, { name: '', adresse: null })
|
|
assert.deepEqual(baue(null, { firma: { name: 5 } }).firma, { name: '', adresse: null }, 'kein Text = leer')
|
|
assert.deepEqual(baue(null, { firma: { name: ' Büro ' } }).firma, { name: 'Büro', adresse: null })
|
|
})
|
|
|
|
test('team: Vorgabe bei fehlenden oder kaputten Angaben, keine fremden Schluessel', () => {
|
|
const vorgabe = { navSelektor: null, kontaktleisteAus: false, hinweisleisteAus: false }
|
|
for (const team of [undefined, null, {}, 'x', [], { navSelektor: '', kontaktleisteAus: 'ja', hinweisleisteAus: 1 }]) {
|
|
assert.deepEqual(baue(null, { team }).team, vorgabe, JSON.stringify(team))
|
|
}
|
|
assert.deepEqual(baue(null, { team: { navSelektor: 'header', kontaktleisteAus: true, hinweisleisteAus: false, intern: 'x' } }).team, { navSelektor: 'header', kontaktleisteAus: true, hinweisleisteAus: false })
|
|
})
|
|
|
|
test('kaputte gespeicherte Daten werfen nicht', () => {
|
|
for (const daten of [
|
|
{ plaene: 'x', hinweise: {}, kontakt: 'x' },
|
|
{ plaene: [null, 5, { kurz: 'a' }, { kurz: 'b', haupt: true, tage: 'x' }, { name: 'ohne kurz' }], hinweise: [null, { text: 'x' }], kontakt: [] },
|
|
{ plaene: [{ kurz: 'a', name: 'A', haupt: true, tage: { 1: { modus: 'hm', fenster: 'x' } } }], ausnahmen: 7, feiertage: [], land: 'XX' },
|
|
]) {
|
|
const a = baue(daten)
|
|
assert.equal(a.aktiv, true)
|
|
assert.ok(Array.isArray(a.plaene) && Array.isArray(a.hinweise))
|
|
JSON.stringify(a)
|
|
}
|
|
const a = baue({ plaene: [{ kurz: 'a', name: 'A', haupt: true, tage: { 1: { modus: 'hm' } } }, { name: 'ohne kurz' }, null] })
|
|
assert.deepEqual(a.plaene.map((p) => p.kurz), ['a'], 'Plaene ohne Kurznamen koennen nicht angesprochen werden und fallen weg')
|
|
assert.deepEqual(a.plaene[0].woche[0], { tag: 1, modus: 'zu', fenster: [] }, 'unbekannter Modus = zu')
|
|
})
|
|
|
|
test('ungueltiges now ist ein Programmierfehler', () => {
|
|
for (const now of [undefined, null, 'heute', new Date('x'), 1]) assert.throws(() => baue(null, { now }), TypeError)
|
|
})
|
|
|
|
// ---------------------------------------------------------------- oeffentlicheZeitenAntwort
|
|
|
|
test('Schalter aus: aktiv false, ohne Abfrage und ohne Cache-Eintrag', async () => {
|
|
legeAn()
|
|
config.zeiten.mode = 'off'
|
|
assert.deepEqual(await oeffentlicheZeitenAntwort(SITE_KEY, JETZT), { aktiv: false })
|
|
assert.equal(listen(), 0)
|
|
assert.equal(zeitenCache.groesse(), 0)
|
|
config.zeiten.mode = 'pilot'
|
|
assert.equal((await oeffentlicheZeitenAntwort(SITE_KEY, JETZT)).aktiv, true, 'pilot und on beantworten')
|
|
})
|
|
|
|
test('ungültiger Schlüssel: keine Abfrage, kein Cache-Eintrag', async () => {
|
|
legeAn()
|
|
for (const key of ['', 'ABCDABCDABCDABCD', 'abcdabcdabcdabc', 'abcdabcdabcdabcde', 'abcdabcdabcdabc1', 'abcdabcdabcdabc0', 'abcdabcdabcdabc!', 'abcd abcdabcdabc', undefined, null, 42, {}, [SITE_KEY], { toString: () => SITE_KEY }]) {
|
|
assert.deepEqual(await oeffentlicheZeitenAntwort(key, JETZT), { aktiv: false }, String(key))
|
|
}
|
|
assert.equal(aw.listCalls().length, 0, 'keine einzige Listenabfrage an Appwrite')
|
|
assert.equal(zeitenCache.groesse(), 0)
|
|
})
|
|
|
|
test('unbekannter, gueltig geformter Schluessel: aktiv false, wird gemerkt', async () => {
|
|
legeAn()
|
|
assert.deepEqual(await oeffentlicheZeitenAntwort('zzzzzzzzzzzzzzzz', JETZT), { aktiv: false })
|
|
assert.equal(listen(), 1)
|
|
assert.equal(zeitenCache.groesse(), 1)
|
|
assert.deepEqual(await oeffentlicheZeitenAntwort('zzzzzzzzzzzzzzzz', new Date(JETZT.getTime() + 29000)), { aktiv: false })
|
|
assert.equal(listen(), 1, 'auch das Unbekannte kommt aus dem Speicher')
|
|
})
|
|
|
|
test('Zustand: setup und active liefern dasselbe, off, ordered und disabled nur aktiv false', async () => {
|
|
legeAn()
|
|
await gespeicherteDaten()
|
|
const active = await oeffentlicheZeitenAntwort(SITE_KEY, JETZT)
|
|
assert.equal(active.aktiv, true)
|
|
|
|
for (const state of ['setup']) {
|
|
legeAn({ feature: { state, enabled: false } })
|
|
resetZeitenCacheForTests()
|
|
assert.deepEqual(await oeffentlicheZeitenAntwort(SITE_KEY, JETZT), active, state)
|
|
}
|
|
for (const state of ['off', 'ordered', 'disabled']) {
|
|
legeAn({ feature: { state, enabled: false } })
|
|
resetZeitenCacheForTests()
|
|
assert.deepEqual(await oeffentlicheZeitenAntwort(SITE_KEY, JETZT), { aktiv: false }, state)
|
|
}
|
|
})
|
|
|
|
test('nur die Funktion zeiten: der Schluessel von News oder Termine gibt aktiv false', async () => {
|
|
legeAn()
|
|
for (const featureKey of ['news', 'termine']) {
|
|
aw.seed('portalFeatures', { ...FEATURE, $id: `f_p_projekt01_${featureKey}`, featureKey, siteKey: 'nnnnnnnnnnnnnnnn' })
|
|
resetZeitenCacheForTests()
|
|
assert.deepEqual(await oeffentlicheZeitenAntwort('nnnnnnnnnnnnnnnn', JETZT), { aktiv: false }, featureKey)
|
|
await deleteDocument('portalFeatures', `f_p_projekt01_${featureKey}`)
|
|
}
|
|
})
|
|
|
|
test('Kunde gelöscht ⇒ aktiv false', async () => {
|
|
legeAn({ kunde: null })
|
|
await gespeicherteDaten()
|
|
assert.deepEqual(await oeffentlicheZeitenAntwort(SITE_KEY, JETZT), { aktiv: false })
|
|
assert.equal(zeitenCache.groesse(), 1, 'auch das wird gemerkt')
|
|
})
|
|
|
|
test('Funktion ohne Kunden-ID ⇒ aktiv false', async () => {
|
|
legeAn({ feature: { customerId: '' } })
|
|
assert.deepEqual(await oeffentlicheZeitenAntwort(SITE_KEY, JETZT), { aktiv: false })
|
|
})
|
|
|
|
test('vor dem ersten Speichern (ohne Dokument): aktiv true, plaene leer, kein jsonld, Standard-Team', async () => {
|
|
legeAn()
|
|
const a = await oeffentlicheZeitenAntwort(SITE_KEY, JETZT)
|
|
assert.equal(a.aktiv, true)
|
|
assert.deepEqual(a.plaene, [])
|
|
assert.equal(a.jsonld, null)
|
|
assert.equal(a.kontakt, null)
|
|
assert.deepEqual(a.team, { navSelektor: null, kontaktleisteAus: false, hinweisleisteAus: false })
|
|
assert.deepEqual(a.firma, { name: 'Bäckerei Sonnenschein', adresse: null })
|
|
})
|
|
|
|
test('nur Team-Einstellungen gespeichert (Dokument ohne daten): Team gilt, plaene leer', async () => {
|
|
legeAn()
|
|
await speichereTeam({ feature: FEATURE, eingabe: { navSelektor: '#kopf', kontaktleisteAus: true, hinweisleisteAus: false }, teamVersion: 0, now: JETZT })
|
|
const a = await oeffentlicheZeitenAntwort(SITE_KEY, JETZT)
|
|
assert.deepEqual(a.plaene, [])
|
|
assert.deepEqual(a.team, { navSelektor: '#kopf', kontaktleisteAus: true, hinweisleisteAus: false })
|
|
})
|
|
|
|
test('unlesbare daten gelten wie vor dem ersten Speichern', async () => {
|
|
legeAn()
|
|
aw.seed('oeffnungszeiten', { $id: FEATURE_ID, customerId: KUNDE, projectId: PROJEKT, featureId: FEATURE_ID, daten: '{kaputt', version: 3, teamVersion: 0 })
|
|
const a = await oeffentlicheZeitenAntwort(SITE_KEY, JETZT)
|
|
assert.equal(a.aktiv, true)
|
|
assert.deepEqual(a.plaene, [])
|
|
})
|
|
|
|
test('volle Antwort aus gespeicherten Daten: Pläne, Hinweise, Kontakt, firma.adresse und Team', async () => {
|
|
legeAn()
|
|
await gespeicherteDaten()
|
|
await speichereTeam({ feature: FEATURE, eingabe: TEAM, teamVersion: 0, now: JETZT })
|
|
const a = await oeffentlicheZeitenAntwort(SITE_KEY, JETZT)
|
|
assert.deepEqual(Object.keys(a), ['aktiv', 'stand', 'plaene', 'hinweise', 'kontakt', 'jsonld', 'firma', 'team'])
|
|
assert.equal(a.stand, JETZT.toISOString())
|
|
assert.deepEqual(a.plaene.map((p) => p.kurz), ['standard', 'kueche'])
|
|
assert.equal(a.hinweise.length, 2)
|
|
assert.deepEqual(a.kontakt, {
|
|
telefon: '491711234567',
|
|
routeUrl: 'https://www.google.com/maps/dir/?api=1&destination=' + encodeURIComponent(ADRESSE),
|
|
whatsappUrl: 'https://wa.me/491711234567?text=' + encodeURIComponent(STARTTEXT),
|
|
})
|
|
assert.deepEqual(a.firma, { name: 'Bäckerei Sonnenschein', adresse: ADRESSE })
|
|
assert.deepEqual(a.team, TEAM)
|
|
assert.equal(a.jsonld.openingHoursSpecification.length, 6)
|
|
})
|
|
|
|
test('firma ohne Firmenname fällt auf den Kundennamen, dann auf den Projektnamen, sonst leer', async () => {
|
|
const name = async (kunde, projekt) => {
|
|
for (const c of ['portalFeatures', 'customers', 'websiteProjects']) for (const id of [...aw.docs(c).keys()]) await deleteDocument(c, id)
|
|
legeAn({ kunde, projekt })
|
|
resetZeitenCacheForTests()
|
|
const a = await oeffentlicheZeitenAntwort(SITE_KEY, JETZT)
|
|
assert.equal(a.aktiv, true)
|
|
return a.firma.name
|
|
}
|
|
assert.equal(await name({ name: 'Frau Sonne', companyName: 'Bäckerei Sonnenschein' }, { projectName: 'P' }), 'Bäckerei Sonnenschein')
|
|
assert.equal(await name({ name: 'Frau Sonne', companyName: '' }, { projectName: 'P' }), 'Frau Sonne')
|
|
assert.equal(await name({ name: 'Frau Sonne' }, { projectName: 'P' }), 'Frau Sonne')
|
|
assert.equal(await name({ name: ' ', companyName: ' ' }, { projectName: 'Sonnenschein-Website' }), 'Sonnenschein-Website')
|
|
assert.equal(await name({ email: 'x@y.de' }, { projectName: 'Sonnenschein-Website' }), 'Sonnenschein-Website')
|
|
assert.equal(await name({ email: 'x@y.de' }, null), '', 'weder Kundenname noch Projekt: leer, nie ein Fehler')
|
|
assert.equal(await name({ email: 'x@y.de' }, { projectName: '' }), '')
|
|
})
|
|
|
|
test('zweiter Abruf innerhalb von 30 s aus dem Cache', async () => {
|
|
legeAn()
|
|
await gespeicherteDaten()
|
|
const erste = await oeffentlicheZeitenAntwort(SITE_KEY, JETZT)
|
|
assert.equal(listen(), 1)
|
|
|
|
// Aenderungen in Appwrite bleiben bis zum Ablauf unsichtbar
|
|
aw.seed('portalFeatures', { ...FEATURE, state: 'disabled', enabled: false })
|
|
const zweite = await oeffentlicheZeitenAntwort(SITE_KEY, new Date(JETZT.getTime() + 29999))
|
|
assert.equal(listen(), 1, 'keine neue Abfrage')
|
|
assert.deepEqual(zweite, erste)
|
|
assert.equal(zweite.stand, JETZT.toISOString(), 'stand ist der Zeitpunkt des Bauens')
|
|
|
|
const dritte = await oeffentlicheZeitenAntwort(SITE_KEY, new Date(JETZT.getTime() + 30000))
|
|
assert.equal(listen(), 2, 'nach 30 s wird neu gebaut')
|
|
assert.deepEqual(dritte, { aktiv: false })
|
|
})
|
|
|
|
test('der Cache gilt je Schluessel', async () => {
|
|
legeAn()
|
|
aw.seed('portalFeatures', { ...FEATURE, $id: 'f_p_projekt02_zeiten', projectId: 'p_projekt02', siteKey: 'wxyzwxyzwxyzwxyz' })
|
|
assert.equal((await oeffentlicheZeitenAntwort(SITE_KEY, JETZT)).aktiv, true)
|
|
assert.equal((await oeffentlicheZeitenAntwort('wxyzwxyzwxyzwxyz', JETZT)).aktiv, true)
|
|
assert.equal(listen(), 2)
|
|
assert.equal(zeitenCache.groesse(), 2)
|
|
})
|
|
|
|
test('Speichern verwirft die Antwort: die naechste Anfrage sieht die neuen Zeiten sofort', async () => {
|
|
legeAn()
|
|
const daten = await gespeicherteDaten()
|
|
const vorher = await oeffentlicheZeitenAntwort(SITE_KEY, JETZT)
|
|
assert.equal(vorher.plaene[0].name, 'Standard')
|
|
|
|
const neu = { ...daten, plaene: daten.plaene.map((p) => (p.haupt ? { ...p, name: 'Hauptzeiten' } : p)) }
|
|
await speichereZeiten({ feature: FEATURE, eingabe: neu, version: 1, by: 'kunde', now: JETZT })
|
|
const nachher = await oeffentlicheZeitenAntwort(SITE_KEY, new Date(JETZT.getTime() + 1000))
|
|
assert.equal(nachher.plaene[0].name, 'Hauptzeiten')
|
|
})
|
|
|
|
test('keine internen IDs in der Antwort', async () => {
|
|
legeAn()
|
|
await gespeicherteDaten()
|
|
await speichereTeam({ feature: FEATURE, eingabe: TEAM, teamVersion: 0, now: JETZT })
|
|
const a = await oeffentlicheZeitenAntwort(SITE_KEY, JETZT)
|
|
const { schluessel, werte } = durchsuche(a)
|
|
for (const verboten of ['$id', '$collectionId', '$databaseId', 'customerId', 'projectId', 'featureId', 'siteKey', 'id', 'version', 'teamVersion', 'updatedBy', 'ersatzHash', 'email']) {
|
|
assert.ok(!schluessel.has(verboten), `Schluessel ${verboten}`)
|
|
}
|
|
const text = JSON.stringify(a)
|
|
for (const intern of [FEATURE_ID, KUNDE, PROJEKT, SITE_KEY, P_STANDARD, P_KUECHE, 'x_urlaub01', 'h_aktion01', 'h_dauer001', 'Frau Sonne']) {
|
|
assert.ok(!text.includes(intern), `Wert ${intern}`)
|
|
}
|
|
assert.ok(werte.length > 20)
|
|
})
|
|
|
|
test('Appwrite-Fehler gehen an den Aufrufer, ohne dass etwas gemerkt wird', async () => {
|
|
legeAn()
|
|
await gespeicherteDaten()
|
|
aw.failNext({ collection: 'portalFeatures', status: 500, message: 'kaputt' })
|
|
await assert.rejects(() => oeffentlicheZeitenAntwort(SITE_KEY, JETZT), { status: 500 })
|
|
assert.equal(zeitenCache.groesse(), 0)
|
|
aw.failNext({ collection: 'customers', status: 500, message: 'kaputt' })
|
|
await assert.rejects(() => oeffentlicheZeitenAntwort(SITE_KEY, JETZT), { status: 500 })
|
|
aw.failNext({ collection: 'oeffnungszeiten', status: 500, message: 'kaputt' })
|
|
await assert.rejects(() => oeffentlicheZeitenAntwort(SITE_KEY, JETZT), { status: 500 })
|
|
assert.equal(zeitenCache.groesse(), 0)
|
|
assert.equal((await oeffentlicheZeitenAntwort(SITE_KEY, JETZT)).aktiv, true, 'danach geht es wieder')
|
|
})
|
|
|
|
test('ohne now rechnet die Funktion mit der aktuellen Zeit', async () => {
|
|
legeAn()
|
|
await gespeicherteDaten()
|
|
const vorher = Date.now()
|
|
const a = await oeffentlicheZeitenAntwort(SITE_KEY)
|
|
assert.ok(Date.parse(a.stand) >= vorher && Date.parse(a.stand) <= Date.now())
|
|
})
|
|
|
|
test('die gemerkte Antwort ist gegen Veraenderung geschuetzt', async () => {
|
|
legeAn()
|
|
await gespeicherteDaten()
|
|
const a = await oeffentlicheZeitenAntwort(SITE_KEY, JETZT)
|
|
assert.throws(() => {
|
|
a.plaene[0].name = 'gehackt'
|
|
}, TypeError)
|
|
assert.throws(() => {
|
|
a.firma.name = 'gehackt'
|
|
}, TypeError)
|
|
assert.equal((await oeffentlicheZeitenAntwort(SITE_KEY, JETZT)).plaene[0].name, 'Standard')
|
|
})
|