Files
ClaudeandClaude Opus 5.5 097036c9a5 Öffnungszeiten: Speicher mit Versionen und Cache
LRU-Speicher mit Ablaufzeit (lru.js), Cache-Instanz der öffentlichen Antwort
(cache.js: 500 Einträge, 30 s) und Appwrite-Speicher mit getrennten Versionen
für Kundendaten und Team-Einstellungen (speicher.js), alle Schreibzugriffe
unter der Sperre zeiten:<projectId>.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 00:50:41 +00:00

525 lines
26 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'
// Speicher der Oeffnungszeiten (server/services/zeiten/speicher.js): Dokument je Funktion (ID = ID des portalFeatures-
// Dokuments), getrennte Versionen fuer Kundendaten (version) und Team-Einstellungen (teamVersion), alles unter der Sperre
// zeiten:<projectId>. Nachbau von Appwrite mit Schema der beiden Zeiten-Collections und Standard-Limit 25 wie in Produktion.
const aw = await startAppwriteMock({
schema: {
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 { withLocks } = await import('../server/services/keyedLock.js')
const { zeitenCache, resetZeitenCacheForTests } = await import('../server/services/zeiten/cache.js')
const S = await import('../server/services/zeiten/speicher.js')
const { ladeZeiten, speichereZeiten, speichereTeam, setzeErsatzHash, loescheZeiten, loescheZeitenVonKunde } = S
const COL = config.collections.oeffnungszeiten
const MERKER = config.collections.zeitenErinnerungen
const JETZT = new Date('2026-10-05T10:00:00.000Z') // Berlin: Montag 2026-10-05 12:00
const P1 = 'p_aaaaaaa1'
const P2 = 'p_aaaaaaa2'
const FEATURE = {
$id: 'f_p_projekt01_zeiten',
customerId: 'c_kunde01',
projectId: 'p_projekt01',
siteKey: 'abcdabcdabcdabcd',
featureKey: 'zeiten',
state: 'active',
}
const ANDERES = {
$id: 'f_p_projekt02_zeiten',
customerId: 'c_kunde02',
projectId: 'p_projekt02',
siteKey: 'wxyzwxyzwxyzwxyz',
featureKey: 'zeiten',
state: 'active',
}
function tage(woche = {}) {
const t = {}
for (let d = 1; d <= 7; d += 1) {
const w = woche[d]
t[d] = w ? { modus: 'zeiten', fenster: [w] } : { modus: 'zu', fenster: [] }
}
return t
}
function plan(extra = {}) {
return { id: P1, kurz: 'standard', name: 'Standard', haupt: true, tage: tage({ 1: ['09:00', '18:00'] }), ...extra }
}
function eingabe(extra = {}) {
return { land: 'RP', plaene: [plan()], feiertage: {}, ausnahmen: [], hinweise: [], kontakt: { an: false, telefon: '', adresse: '', whatsapp: null }, ...extra }
}
const dokument = (id = FEATURE.$id) => aw.docs('oeffnungszeiten').get(id)
const warte = (ms) => new Promise((resolve) => setTimeout(resolve, ms))
let log
beforeEach(async () => {
for (const collection of ['oeffnungszeiten', 'zeitenErinnerungen']) {
for (const id of [...aw.docs(collection).keys()]) await deleteDocument(collection, id)
}
aw.resetListCalls()
resetZeitenCacheForTests()
log = { error: mock.method(console, 'error', () => {}), warn: mock.method(console, 'warn', () => {}) }
})
afterEach(() => mock.restoreAll())
// ---------------------------------------------------------------- Zeiten speichern
test('erstes Speichern legt das Dokument mit version 1 an', async () => {
const erg = await speichereZeiten({ feature: FEATURE, eingabe: eingabe(), version: 0, by: 'kunde', now: JETZT })
assert.equal(erg.version, 1)
assert.equal(erg.daten.plaene[0].kurz, 'standard')
assert.equal(erg.daten.plaene[0].tage[1].fenster[0][0], '09:00')
const doc = dokument()
assert.ok(doc, 'Dokument-ID = ID des portalFeatures-Dokuments')
assert.equal(doc.customerId, FEATURE.customerId)
assert.equal(doc.projectId, FEATURE.projectId)
assert.equal(doc.featureId, FEATURE.$id)
assert.equal(doc.version, 1)
assert.equal(doc.teamVersion, 0)
assert.equal(doc.updatedBy, 'kunde')
assert.equal(new Date(doc.updatedAt).getTime(), JETZT.getTime())
assert.deepEqual(JSON.parse(doc.daten), erg.daten, 'gespeichert wird genau das normalisierte Ergebnis')
})
test('jedes weitere Speichern zählt die Version um eins hoch und ersetzt die Daten', async () => {
await speichereZeiten({ feature: FEATURE, eingabe: eingabe(), version: 0, by: 'kunde', now: JETZT })
const zweite = await speichereZeiten({
feature: FEATURE,
eingabe: eingabe({ plaene: [plan({ name: 'Neu' })] }),
version: 1,
by: 'team:st_1',
now: JETZT,
})
assert.equal(zweite.version, 2)
assert.equal(dokument().version, 2)
assert.equal(JSON.parse(dokument().daten).plaene[0].name, 'Neu')
assert.equal(dokument().updatedBy, 'team:st_1')
assert.equal(aw.docs('oeffnungszeiten').size, 1)
})
test('ohne Angabe von by steht kunde im Dokument', async () => {
await speichereZeiten({ feature: FEATURE, eingabe: eingabe(), version: 0, now: JETZT })
assert.equal(dokument().updatedBy, 'kunde')
})
test('veraltete Version ⇒ 409 ZEITEN_VERALTET und nichts wird geschrieben', async () => {
await speichereZeiten({ feature: FEATURE, eingabe: eingabe(), version: 0, by: 'kunde', now: JETZT })
await speichereZeiten({ feature: FEATURE, eingabe: eingabe({ plaene: [plan({ name: 'Zwei' })] }), version: 1, by: 'kunde', now: JETZT })
const vorher = JSON.stringify(dokument())
for (const version of [0, 1, 3, undefined, null, '2', 2.5, -1]) {
await assert.rejects(
() => speichereZeiten({ feature: FEATURE, eingabe: eingabe({ plaene: [plan({ name: 'Drei' })] }), version, by: 'kunde', now: JETZT }),
(err) => err.status === 409 && err.code === 'ZEITEN_VERALTET' && /neu laden/i.test(err.message),
`version ${String(version)}`,
)
}
assert.equal(JSON.stringify(dokument()), vorher)
})
test('erstes Speichern mit version 1 (es gibt noch nichts) ⇒ 409, kein Dokument', async () => {
await assert.rejects(
() => speichereZeiten({ feature: FEATURE, eingabe: eingabe(), version: 1, by: 'kunde', now: JETZT }),
(err) => err.status === 409 && err.code === 'ZEITEN_VERALTET',
)
assert.equal(aw.docs('oeffnungszeiten').size, 0)
})
test('Prüffehler ⇒ 400 mit fehler, es wird nichts geschrieben', async () => {
await assert.rejects(
() => speichereZeiten({ feature: FEATURE, eingabe: eingabe({ land: 'XX' }), version: 0, by: 'kunde', now: JETZT }),
(err) => err.status === 400 && typeof err.fehler?.land === 'string' && err.code !== 'ZEITEN_VERALTET',
)
assert.equal(aw.docs('oeffnungszeiten').size, 0)
// auch auf einem vorhandenen Dokument bleibt der Stand unberührt
await speichereZeiten({ feature: FEATURE, eingabe: eingabe(), version: 0, by: 'kunde', now: JETZT })
const vorher = JSON.stringify(dokument())
await assert.rejects(
() => speichereZeiten({ feature: FEATURE, eingabe: eingabe({ plaene: [] }), version: 1, by: 'kunde', now: JETZT }),
(err) => err.status === 400 && typeof err.fehler?.plaene === 'string',
)
assert.equal(JSON.stringify(dokument()), vorher)
})
test('Eingabe, die kein Objekt ist ⇒ 400', async () => {
for (const kaputt of [null, undefined, 'x', 5, [], [eingabe()]]) {
await assert.rejects(
() => speichereZeiten({ feature: FEATURE, eingabe: kaputt, version: 0, by: 'kunde', now: JETZT }),
(err) => err.status === 400 && typeof err.fehler?.daten === 'string',
)
}
assert.equal(aw.docs('oeffnungszeiten').size, 0)
})
test('ist die Version veraltet und die Eingabe falsch, gewinnt 409 (der Kunde lädt ohnehin neu)', async () => {
await speichereZeiten({ feature: FEATURE, eingabe: eingabe(), version: 0, by: 'kunde', now: JETZT })
await assert.rejects(
() => speichereZeiten({ feature: FEATURE, eingabe: eingabe({ land: 'XX' }), version: 0, by: 'kunde', now: JETZT }),
(err) => err.status === 409 && err.code === 'ZEITEN_VERALTET',
)
})
test('die gespeicherten Daten gehen als bisher in die Prüfung (Kurznamen bleiben)', async () => {
const kueche = plan({ id: P1, name: 'Küche', kurz: 'egal' })
const erste = await speichereZeiten({ feature: FEATURE, eingabe: eingabe({ plaene: [kueche] }), version: 0, by: 'kunde', now: JETZT })
assert.equal(erste.daten.plaene[0].kurz, 'kueche')
const zweite = await speichereZeiten({
feature: FEATURE,
eingabe: eingabe({ plaene: [plan({ id: P1, name: 'Mittag', kurz: 'x' }), plan({ id: P2, name: 'Küche', haupt: false })] }),
version: 1,
by: 'kunde',
now: JETZT,
})
assert.equal(zweite.daten.plaene[0].kurz, 'kueche', 'der Kurzname eines vorhandenen Plans ändert sich nie')
assert.equal(zweite.daten.plaene[1].kurz, 'kueche-2', 'und wird auch nicht neu vergeben')
})
test('Speichern leert den Cache-Eintrag des siteKeys', async () => {
zeitenCache.setze(FEATURE.siteKey, { aktiv: true }, 0)
zeitenCache.setze(ANDERES.siteKey, { aktiv: true }, 0)
await speichereZeiten({ feature: FEATURE, eingabe: eingabe(), version: 0, by: 'kunde', now: JETZT })
assert.equal(zeitenCache.hole(FEATURE.siteKey, 1), undefined)
assert.deepEqual(zeitenCache.hole(ANDERES.siteKey, 1), { aktiv: true }, 'andere Seiten bleiben im Cache')
})
test('scheitert das Schreiben in Appwrite, geht der Fehler durch, nichts ändert sich, der Cache wird trotzdem geleert', async () => {
await speichereZeiten({ feature: FEATURE, eingabe: eingabe(), version: 0, by: 'kunde', now: JETZT })
const vorher = JSON.stringify(dokument())
zeitenCache.setze(FEATURE.siteKey, { aktiv: true }, 0)
aw.failNext({ collection: 'oeffnungszeiten', method: 'PATCH', status: 500, message: 'kaputt' })
await assert.rejects(
() => speichereZeiten({ feature: FEATURE, eingabe: eingabe({ plaene: [plan({ name: 'Neu' })] }), version: 1, by: 'kunde', now: JETZT }),
(err) => err.status === 500,
)
assert.equal(JSON.stringify(dokument()), vorher)
assert.equal(zeitenCache.hole(FEATURE.siteKey, 1), undefined, 'unklar, was in der Datenbank steht')
// danach geht es mit derselben Version weiter
const erg = await speichereZeiten({ feature: FEATURE, eingabe: eingabe({ plaene: [plan({ name: 'Neu' })] }), version: 1, by: 'kunde', now: JETZT })
assert.equal(erg.version, 2)
})
test('legt ein anderer Vorgang das Dokument zwischen Lesen und Anlegen an (Appwrite 409), ist das ebenfalls veraltet', async () => {
aw.failNext({ collection: 'oeffnungszeiten', method: 'POST', status: 409, message: 'Document with the requested ID already exists.' })
await assert.rejects(
() => speichereZeiten({ feature: FEATURE, eingabe: eingabe(), version: 0, by: 'kunde', now: JETZT }),
(err) => err.status === 409 && err.code === 'ZEITEN_VERALTET',
)
})
test('gleichzeitiges Speichern: genau einer gewinnt', async () => {
await speichereZeiten({ feature: FEATURE, eingabe: eingabe(), version: 0, by: 'kunde', now: JETZT })
const [a, b] = await Promise.allSettled([
speichereZeiten({ feature: FEATURE, eingabe: eingabe({ plaene: [plan({ name: 'Anna' })] }), version: 1, by: 'kunde', now: JETZT }),
speichereZeiten({ feature: FEATURE, eingabe: eingabe({ plaene: [plan({ name: 'Bernd' })] }), version: 1, by: 'kunde', now: JETZT }),
])
const gewinner = [a, b].filter((r) => r.status === 'fulfilled')
const verlierer = [a, b].filter((r) => r.status === 'rejected')
assert.equal(gewinner.length, 1)
assert.equal(verlierer.length, 1)
assert.equal(verlierer[0].reason.status, 409)
assert.equal(verlierer[0].reason.code, 'ZEITEN_VERALTET')
assert.equal(gewinner[0].value.version, 2)
assert.equal(dokument().version, 2)
assert.deepEqual(JSON.parse(dokument().daten), gewinner[0].value.daten, 'gespeichert ist das, was der Gewinner geschickt hat')
})
test('gleichzeitiges erstes Speichern: genau einer legt das Dokument an', async () => {
const ergebnisse = await Promise.allSettled([
speichereZeiten({ feature: FEATURE, eingabe: eingabe({ plaene: [plan({ name: 'Anna' })] }), version: 0, by: 'kunde', now: JETZT }),
speichereZeiten({ feature: FEATURE, eingabe: eingabe({ plaene: [plan({ name: 'Bernd' })] }), version: 0, by: 'kunde', now: JETZT }),
speichereZeiten({ feature: FEATURE, eingabe: eingabe({ plaene: [plan({ name: 'Cora' })] }), version: 0, by: 'kunde', now: JETZT }),
])
assert.equal(ergebnisse.filter((r) => r.status === 'fulfilled').length, 1)
assert.equal(ergebnisse.filter((r) => r.status === 'rejected' && r.reason.code === 'ZEITEN_VERALTET').length, 2)
assert.equal(dokument().version, 1)
assert.equal(aw.docs('oeffnungszeiten').size, 1)
})
// ---------------------------------------------------------------- Sperre
test('alle Schreibzugriffe warten auf die Sperre zeiten:<projectId>, andere Projekte nicht', async () => {
const schreiber = {
speichereZeiten: () => speichereZeiten({ feature: FEATURE, eingabe: eingabe({ plaene: [plan({ name: 'Neu' })] }), version: 1, by: 'kunde', now: JETZT }),
speichereTeam: () => speichereTeam({ feature: FEATURE, eingabe: { navSelektor: '#kopf' }, teamVersion: 0, by: 'team:st_1' }),
setzeErsatzHash: () => setzeErsatzHash(FEATURE, 'abc123'),
loescheZeiten: () => loescheZeiten(FEATURE.$id),
loescheZeitenVonKunde: () => loescheZeitenVonKunde(FEATURE.customerId),
}
for (const [name, starte] of Object.entries(schreiber)) {
// jede Runde beginnt mit genau einem Dokument der Funktion (version 1)
for (const id of [...aw.docs('oeffnungszeiten').keys()]) await deleteDocument('oeffnungszeiten', id)
await speichereZeiten({ feature: FEATURE, eingabe: eingabe(), version: 0, by: 'kunde', now: JETZT })
let freigeben
const gehalten = new Promise((resolve) => { freigeben = resolve })
const halter = withLocks([`zeiten:${FEATURE.projectId}`], () => gehalten)
let fertig = false
const lauf = starte().finally(() => { fertig = true })
try {
// ein anderes Projekt wird nicht aufgehalten
const fremd = await speichereZeiten({ feature: ANDERES, eingabe: eingabe(), version: 0, by: 'kunde', now: JETZT })
assert.equal(fremd.version, 1, name)
await warte(40)
assert.equal(fertig, false, `${name} muss auf die Sperre warten`)
} finally {
freigeben() // auch bei einem Fehlschlag, sonst bliebe die Sperre fuer alle folgenden Tests belegt
}
await halter
await lauf
assert.equal(fertig, true, name)
}
})
// ---------------------------------------------------------------- Team
test('Team-Einstellungen haben eine eigene Version', async () => {
await speichereZeiten({ feature: FEATURE, eingabe: eingabe(), version: 0, by: 'kunde', now: JETZT })
const erste = await speichereTeam({ feature: FEATURE, eingabe: { navSelektor: ' #kopf nav ', kontaktleisteAus: true }, teamVersion: 0, by: 'team:st_1' })
assert.deepEqual(erste, { team: { navSelektor: '#kopf nav', kontaktleisteAus: true, hinweisleisteAus: false }, teamVersion: 1 })
assert.equal(dokument().teamVersion, 1)
assert.equal(dokument().version, 1, 'die Version der Kundendaten bleibt')
assert.equal(dokument().updatedBy, 'team:st_1')
assert.deepEqual(JSON.parse(dokument().team), erste.team)
// der Kunde speichert weiter mit seiner Version, ohne dass die Team-Einstellung stört (und umgekehrt)
const kunde = await speichereZeiten({ feature: FEATURE, eingabe: eingabe({ plaene: [plan({ name: 'Neu' })] }), version: 1, by: 'kunde', now: JETZT })
assert.equal(kunde.version, 2)
assert.equal(dokument().teamVersion, 1)
assert.deepEqual(JSON.parse(dokument().team), erste.team, 'Kundenänderungen lassen das Team-Feld unberührt')
const zweite = await speichereTeam({ feature: FEATURE, eingabe: { navSelektor: null, hinweisleisteAus: true }, teamVersion: 1, by: 'team:st_2' })
assert.deepEqual(zweite, { team: { navSelektor: null, kontaktleisteAus: false, hinweisleisteAus: true }, teamVersion: 2 })
assert.equal(dokument().version, 2)
assert.equal(JSON.parse(dokument().daten).plaene[0].name, 'Neu', 'Team-Änderungen lassen die Kundendaten unberührt')
})
test('Team-Einstellungen: veraltete teamVersion ⇒ 409 ZEITEN_VERALTET, Prüffehler ⇒ 400', async () => {
await speichereTeam({ feature: FEATURE, eingabe: { navSelektor: '#a' }, teamVersion: 0, by: 'team:st_1' })
const vorher = JSON.stringify(dokument())
for (const teamVersion of [0, 2, undefined, null, '1']) {
await assert.rejects(
() => speichereTeam({ feature: FEATURE, eingabe: { navSelektor: '#b' }, teamVersion, by: 'team:st_1' }),
(err) => err.status === 409 && err.code === 'ZEITEN_VERALTET',
`teamVersion ${String(teamVersion)}`,
)
}
await assert.rejects(
() => speichereTeam({ feature: FEATURE, eingabe: { navSelektor: '<script>' }, teamVersion: 1, by: 'team:st_1' }),
(err) => err.status === 400 && typeof err.fehler?.navSelektor === 'string',
)
assert.equal(JSON.stringify(dokument()), vorher)
})
test('Team-Einstellungen vor den ersten Kundenzeiten legen das Dokument mit version 0 an; der Kunde speichert danach mit 0', async () => {
const team = await speichereTeam({ feature: FEATURE, eingabe: { navSelektor: '#kopf' }, teamVersion: 0, by: 'team:st_1' })
assert.equal(team.teamVersion, 1)
const doc = dokument()
assert.equal(doc.version, 0)
assert.equal(doc.customerId, FEATURE.customerId)
assert.equal(doc.projectId, FEATURE.projectId)
assert.equal(doc.featureId, FEATURE.$id)
assert.ok(!doc.daten, 'noch keine Kundendaten')
assert.deepEqual(await ladeZeiten(FEATURE.$id), {
daten: null,
version: 0,
team: team.team,
teamVersion: 1,
ersatzHash: '',
})
const kunde = await speichereZeiten({ feature: FEATURE, eingabe: eingabe(), version: 0, by: 'kunde', now: JETZT })
assert.equal(kunde.version, 1)
assert.equal(dokument().teamVersion, 1)
assert.equal(JSON.parse(dokument().team).navSelektor, '#kopf')
})
test('Team-Speichern leert den Cache-Eintrag des siteKeys', async () => {
zeitenCache.setze(FEATURE.siteKey, { aktiv: true }, 0)
await speichereTeam({ feature: FEATURE, eingabe: {}, teamVersion: 0, by: 'team:st_1' })
assert.equal(zeitenCache.hole(FEATURE.siteKey, 1), undefined)
})
// ---------------------------------------------------------------- Laden
test('ladeZeiten: ohne Dokument null', async () => {
assert.equal(await ladeZeiten(FEATURE.$id), null)
})
test('ladeZeiten liefert Daten, Version, Team, Team-Version und Ersatz-Hash', async () => {
const gespeichert = await speichereZeiten({ feature: FEATURE, eingabe: eingabe(), version: 0, by: 'kunde', now: JETZT })
assert.deepEqual(await ladeZeiten(FEATURE.$id), {
daten: gespeichert.daten,
version: 1,
team: { navSelektor: null, kontaktleisteAus: false, hinweisleisteAus: false },
teamVersion: 0,
ersatzHash: '',
})
await speichereTeam({ feature: FEATURE, eingabe: { navSelektor: '#kopf', hinweisleisteAus: true }, teamVersion: 0, by: 'team:st_1' })
await setzeErsatzHash(FEATURE, '0123456789abcdef')
assert.deepEqual(await ladeZeiten(FEATURE.$id), {
daten: gespeichert.daten,
version: 1,
team: { navSelektor: '#kopf', kontaktleisteAus: false, hinweisleisteAus: true },
teamVersion: 1,
ersatzHash: '0123456789abcdef',
})
})
test('ladeZeiten: unlesbare Daten ergeben daten null mit der Version des Dokuments und einer Logzeile ohne Inhalt', async () => {
aw.seed('oeffnungszeiten', { $id: FEATURE.$id, customerId: FEATURE.customerId, projectId: FEATURE.projectId, featureId: FEATURE.$id, version: 7, daten: '{geheim:kaputt', teamVersion: 2, team: '{"navSelektor":"<script>"}' })
const geladen = await ladeZeiten(FEATURE.$id)
assert.equal(geladen.daten, null)
assert.equal(geladen.version, 7, 'wer die kaputten Daten überschreiben will, speichert mit genau dieser Version')
assert.deepEqual(geladen.team, { navSelektor: null, kontaktleisteAus: false, hinweisleisteAus: false }, 'ein ungültiges Team-Feld fällt auf die Vorgabe zurück')
assert.equal(geladen.teamVersion, 2)
const text = log.error.mock.calls.map((aufruf) => aufruf.arguments.map(String).join(' ')).join('\n')
assert.ok(text.includes(FEATURE.$id))
assert.ok(!text.includes('geheim') && !text.includes('script'), 'der Inhalt gehört nicht ins Log')
const erg = await speichereZeiten({ feature: FEATURE, eingabe: eingabe(), version: 7, by: 'kunde', now: JETZT })
assert.equal(erg.version, 8)
})
test('ladeZeiten und Schreiber verlangen eine Funktions-ID', async () => {
for (const kaputt of ['', undefined, null, 5]) await assert.rejects(() => ladeZeiten(kaputt), TypeError)
await assert.rejects(() => speichereZeiten({ feature: { projectId: 'p' }, eingabe: eingabe(), version: 0, now: JETZT }), TypeError)
await assert.rejects(() => speichereZeiten({ feature: { $id: 'f_x' }, eingabe: eingabe(), version: 0, now: JETZT }), TypeError)
await assert.rejects(() => speichereTeam({ feature: undefined, eingabe: {}, teamVersion: 0 }), TypeError)
await assert.rejects(() => setzeErsatzHash(FEATURE, 5), TypeError)
await assert.rejects(() => setzeErsatzHash(FEATURE, 'x'.repeat(65)), TypeError)
await assert.rejects(() => loescheZeiten(''), TypeError)
await assert.rejects(() => loescheZeitenVonKunde(''), TypeError)
assert.equal(aw.docs('oeffnungszeiten').size, 0)
})
// ---------------------------------------------------------------- Ersatz-Hash
test('setzeErsatzHash schreibt nur den Hash: Version, Daten und Team bleiben', async () => {
await speichereZeiten({ feature: FEATURE, eingabe: eingabe(), version: 0, by: 'kunde', now: JETZT })
await speichereTeam({ feature: FEATURE, eingabe: { navSelektor: '#kopf' }, teamVersion: 0, by: 'team:st_1' })
const vorher = { ...dokument() }
zeitenCache.setze(FEATURE.siteKey, { aktiv: true }, 0)
await setzeErsatzHash(FEATURE, 'abcdef0123456789')
const nachher = dokument()
assert.equal(nachher.ersatzHash, 'abcdef0123456789')
for (const feld of ['version', 'daten', 'teamVersion', 'team', 'updatedAt', 'updatedBy', 'customerId', 'projectId', 'featureId']) {
assert.deepEqual(nachher[feld], vorher[feld], feld)
}
assert.equal(zeitenCache.hole(FEATURE.siteKey, 1), undefined)
await setzeErsatzHash(FEATURE, '')
assert.equal((await ladeZeiten(FEATURE.$id)).ersatzHash, '')
})
test('setzeErsatzHash ohne Dokument legt eines ohne Kundendaten an', async () => {
await setzeErsatzHash(FEATURE, 'abcdef0123456789')
const doc = dokument()
assert.equal(doc.ersatzHash, 'abcdef0123456789')
assert.equal(doc.version, 0)
assert.equal(doc.teamVersion, 0)
assert.equal(doc.customerId, FEATURE.customerId)
assert.equal((await ladeZeiten(FEATURE.$id)).daten, null)
})
// ---------------------------------------------------------------- Loeschen
const merker = (id, featureId, datum = '2026-12-24') => aw.seed('zeitenErinnerungen', { $id: id, featureId, datum, createdAt: '2026-12-01T08:00:00.000Z' })
function dokumentFuer(kunde, nr) {
const id = `f_${kunde}_${nr}`
aw.seed('oeffnungszeiten', { $id: id, customerId: kunde, projectId: `p_${kunde}_${nr}`, featureId: id, version: 1, daten: '{}', teamVersion: 0, ersatzHash: '' })
return id
}
test('loescheZeiten löscht Dokument und Merker der Funktion, sonst nichts', async () => {
await speichereZeiten({ feature: FEATURE, eingabe: eingabe(), version: 0, by: 'kunde', now: JETZT })
await speichereZeiten({ feature: ANDERES, eingabe: eingabe(), version: 0, by: 'kunde', now: JETZT })
for (let i = 0; i < 30; i += 1) merker(`m_a_${i}`, FEATURE.$id, `2026-12-${String(i % 28 + 1).padStart(2, '0')}`)
merker('m_b_1', ANDERES.$id)
zeitenCache.setze(FEATURE.siteKey, { aktiv: true }, 0)
assert.equal(await loescheZeiten(FEATURE.$id), true)
assert.equal(dokument(), undefined)
assert.ok(dokument(ANDERES.$id))
assert.deepEqual([...aw.docs('zeitenErinnerungen').keys()], ['m_b_1'], 'alle 30 Merker der Funktion sind weg (mehr als eine Seite des Standard-Limits)')
assert.equal(zeitenCache.hole(FEATURE.siteKey, 1), undefined)
assert.equal(await ladeZeiten(FEATURE.$id), null)
// zweiter Aufruf: nichts mehr da, kein Fehler
assert.equal(await loescheZeiten(FEATURE.$id), false)
})
test('loescheZeiten räumt Merker auch auf, wenn es das Dokument schon nicht mehr gibt', async () => {
merker('m_1', FEATURE.$id)
assert.equal(await loescheZeiten(FEATURE.$id), false)
assert.equal(aw.docs('zeitenErinnerungen').size, 0)
})
test('loescheZeitenVonKunde löscht Daten und Merker', async () => {
const eigene = [dokumentFuer('c_a', 1), dokumentFuer('c_a', 2), dokumentFuer('c_a', 3)]
const fremd = dokumentFuer('c_b', 1)
merker('m_a1', eigene[0])
merker('m_a2', eigene[0], '2026-12-25')
merker('m_a3', eigene[2])
merker('m_b1', fremd)
zeitenCache.setze(FEATURE.siteKey, { aktiv: true }, 0)
assert.equal(await loescheZeitenVonKunde('c_a'), 3)
assert.deepEqual([...aw.docs('oeffnungszeiten').keys()], [fremd])
assert.deepEqual([...aw.docs('zeitenErinnerungen').keys()], ['m_b1'])
assert.equal(zeitenCache.hole(FEATURE.siteKey, 1), undefined)
assert.equal(await loescheZeitenVonKunde('c_a'), 0, 'zweiter Aufruf: nichts mehr da')
assert.equal(await loescheZeitenVonKunde('c_unbekannt'), 0)
})
test('loescheZeitenVonKunde blättert durch alle Seiten (mehr als das Standard-Limit)', async () => {
const ids = []
for (let i = 0; i < 130; i += 1) ids.push(dokumentFuer('c_viel', i))
const fremd = dokumentFuer('c_anders', 1)
for (let i = 0; i < 130; i += 1) merker(`m_${i}`, ids[i], '2026-12-24')
merker('m_fremd', fremd)
assert.equal(await loescheZeitenVonKunde('c_viel'), 130)
assert.deepEqual([...aw.docs('oeffnungszeiten').keys()], [fremd])
assert.deepEqual([...aw.docs('zeitenErinnerungen').keys()], ['m_fremd'])
assert.ok(aw.listCalls('oeffnungszeiten').length >= 2, 'mehrere Seiten abgefragt')
})
test('loescheZeitenVonKunde: scheitert ein Dokument, werden die anderen trotzdem gelöscht und der Fehler kommt am Ende', async () => {
const ids = [dokumentFuer('c_a', 1), dokumentFuer('c_a', 2), dokumentFuer('c_a', 3)]
aw.failNext({ collection: 'oeffnungszeiten', method: 'DELETE', status: 500, message: 'kaputt' })
await assert.rejects(() => loescheZeitenVonKunde('c_a'), (err) => err.status === 500)
assert.equal(aw.docs('oeffnungszeiten').size, 1, 'zwei von drei sind weg')
assert.equal(await loescheZeitenVonKunde('c_a'), 1, 'ein neuer Versuch räumt den Rest')
assert.equal(aw.docs('oeffnungszeiten').size, 0)
assert.equal(ids.length, 3)
})