Neu: server/services/termine/entwuerfe.js. Ein Terminplaner im Zustand off ist ein Entwurf (Fragebogen, Einstellung). loescheEntwuerfe(now) im Tagesjob (nach den abgeschalteten Funktionen, nur im vollen Lauf): letzte Aenderung = juengerer Zeitpunkt aus $updatedAt des Funktionsdokuments und termineConfig.updatedAt, aelter als 90 Tage => Einstellung weg, das Funktionsdokument nur, wenn es nie in Betrieb oder bezahlt war (sonst bleibt es wegen des siteKeys; ein von E8 zurueckgesetztes Dokument nie). Vor jedem Loeschen unter feature-order, feature-admin und termine-cfg frisch lesen (noch off, Frist, keine Buchung). Seiten zu 100 per cursorAfter, hoechstens 2000 geprueft und 100 geloescht je Lauf, der Rest folgt an Folgetagen; Einzelfehler zaehlen in fehler. Tagesbericht: Segment "Entwuerfe n geloescht (m offen)". loescheEntwuerfeVonKunde: Kundenloeschung entfernt alle off-Entwuerfe vollstaendig, Fehler werden nur protokolliert. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
428 lines
21 KiB
JavaScript
428 lines
21 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 { TERMINE_DATETIMES } from '../server/services/termine/schema.js'
|
||
|
||
// Entwuerfe des Terminplaners (server/services/termine/entwuerfe.js, Terminplaner TP2 C6): ein Funktionsdokument im Zustand `off`
|
||
// samt Einstellung (termineConfig) ist ein Entwurf des Fragebogens. Er wird 90 Tage nach der letzten Aenderung geloescht, bei der
|
||
// Kundenloeschung sofort. Aufbau wie termineJobs.test.js: Nachbau von Appwrite mit Schema und Standard-Limit 25, feste Zeit JETZT.
|
||
const aw = await startAppwriteMock({
|
||
schema: {
|
||
portalFeatures: schemaKeys('portalFeatures'),
|
||
termineConfig: schemaKeys('termineConfig'),
|
||
termineBookings: schemaKeys('termineBookings'),
|
||
},
|
||
datetimes: TERMINE_DATETIMES,
|
||
defaultLimit: 25,
|
||
})
|
||
after(() => aw.close())
|
||
useTestEnv({ appwriteUrl: aw.url, stripeUrl: 'http://127.0.0.1:1' })
|
||
|
||
const { deleteDocument } = await import('../server/services/appwriteAdmin.js')
|
||
const { withLocks } = await import('../server/services/keyedLock.js')
|
||
const { speichereFragebogen, resetKontextForTests } = await import('../server/services/termine/kontext.js')
|
||
const { ENTWURF_TAGE, MAX_GEPRUEFT, MAX_GELOESCHT, loescheEntwuerfe, loescheEntwuerfeVonKunde } = await import('../server/services/termine/entwuerfe.js')
|
||
|
||
const TAG = 24 * 60 * 60 * 1000
|
||
const JETZT = new Date('2026-10-05T08:00:00.000Z') // Montag, 10:00 Berlin
|
||
const KUNDE = 'c_kunde01'
|
||
const ANDERER = 'c_kunde02'
|
||
const SITE_KEY = 'abcdefghijklmnop'
|
||
|
||
const vor = (tage) => new Date(JETZT.getTime() - tage * TAG).toISOString()
|
||
const fid = (nr) => `f_p_e${nr}_termine`
|
||
const pid = (nr) => `p_e${nr}`
|
||
const dok = (nr) => aw.docs('portalFeatures').get(fid(nr))
|
||
const einstellung = (nr) => aw.docs('termineConfig').get(fid(nr))
|
||
|
||
/** Entwurf: Funktionsdokument im Zustand off (`tage` seit der letzten Aenderung) und, wenn config, die Einstellung (`cfgTage`). */
|
||
function legeEntwurf(nr, { tage = 91, cfgTage = tage, config = true, kunde = KUNDE, key = 'termine', doc = {} } = {}) {
|
||
aw.seed('portalFeatures', {
|
||
$id: key === 'termine' ? fid(nr) : `f_p_e${nr}_${key}`,
|
||
customerId: kunde,
|
||
projectId: pid(nr),
|
||
featureKey: key,
|
||
state: 'off',
|
||
enabled: false,
|
||
siteKey: SITE_KEY,
|
||
$updatedAt: vor(tage),
|
||
...doc,
|
||
})
|
||
if (config) {
|
||
aw.seed('termineConfig', {
|
||
$id: key === 'termine' ? fid(nr) : `f_p_e${nr}_${key}`,
|
||
customerId: kunde,
|
||
projectId: pid(nr),
|
||
einstellung: '{}',
|
||
version: 1,
|
||
updatedAt: vor(cfgTage),
|
||
updatedBy: 'kunde',
|
||
$updatedAt: vor(cfgTage),
|
||
})
|
||
}
|
||
}
|
||
|
||
/** n Entwuerfe mit den Nummern von..bis (nach $id sortiert und damit in dieser Reihenfolge gelesen: Nummern mit gleicher Stellenzahl). */
|
||
function legeViele(von, bis, optionen) {
|
||
for (let nr = von; nr <= bis; nr += 1) legeEntwurf(nr, optionen)
|
||
}
|
||
|
||
const warteBis = async (bedingung) => {
|
||
for (let i = 0; i < 400 && !bedingung(); i += 1) await new Promise((fertig) => setTimeout(fertig, 5))
|
||
assert.ok(bedingung(), 'Bedingung wurde nicht erfuellt')
|
||
}
|
||
|
||
let log
|
||
const logText = () => [...log.error.mock.calls, ...log.warn.mock.calls].map((aufruf) => aufruf.arguments.map(String).join(' ')).join('\n')
|
||
|
||
beforeEach(async () => {
|
||
for (const collection of ['portalFeatures', 'termineConfig', 'termineBookings']) {
|
||
const ids = [...aw.docs(collection).keys()]
|
||
for (let i = 0; i < ids.length; i += 50) await Promise.all(ids.slice(i, i + 50).map((id) => deleteDocument(collection, id)))
|
||
}
|
||
resetKontextForTests()
|
||
aw.resetListCalls()
|
||
log = { error: mock.method(console, 'error', () => {}), warn: mock.method(console, 'warn', () => {}) }
|
||
})
|
||
afterEach(() => {
|
||
// das Protokoll nennt nie Kunden-, Projekt- oder Funktions-IDs und keinen siteKey
|
||
const text = logText()
|
||
mock.restoreAll()
|
||
assert.doesNotMatch(text, /f_p_e|p_e\d|c_kunde|abcdefghijklmnop/, 'keine Kennungen im Log')
|
||
})
|
||
|
||
// ---------------------------------------------------------------- Konstanten
|
||
|
||
test('Konstanten: 90 Tage, höchstens 2000 geprüft, höchstens 100 gelöscht', () => {
|
||
assert.deepEqual([ENTWURF_TAGE, MAX_GEPRUEFT, MAX_GELOESCHT], [90, 2000, 100])
|
||
})
|
||
|
||
// ---------------------------------------------------------------- Frist
|
||
|
||
test('Frist: 89 und 90 Tage alt bleiben, 91 Tage: Einstellung und Funktionsdokument sind weg', async () => {
|
||
legeEntwurf(1, { tage: 89 })
|
||
legeEntwurf(2, { tage: 90 })
|
||
legeEntwurf(3, { tage: 91 })
|
||
// dieselbe Grenze für die Einstellung allein (das Funktionsdokument ist längst älter)
|
||
legeEntwurf(4, { tage: 300, cfgTage: 89 })
|
||
legeEntwurf(5, { tage: 300, cfgTage: 90 })
|
||
legeEntwurf(6, { tage: 300, cfgTage: 91 })
|
||
const erg = await loescheEntwuerfe(JETZT)
|
||
assert.deepEqual(erg, { geprueft: 6, geloescht: 2, uebrig: 0, fehler: 0 })
|
||
for (const nr of [1, 2, 4, 5]) {
|
||
assert.ok(dok(nr), `Funktionsdokument ${nr} bleibt`)
|
||
assert.ok(einstellung(nr), `Einstellung ${nr} bleibt`)
|
||
}
|
||
for (const nr of [3, 6]) {
|
||
assert.equal(dok(nr), undefined, `Funktionsdokument ${nr}`)
|
||
assert.equal(einstellung(nr), undefined, `Einstellung ${nr}`)
|
||
}
|
||
})
|
||
|
||
test('Frist: der jüngere Zeitpunkt zählt – ein Entwurf, den der Kunde frisch bearbeitet hat (alte $updatedAt, neues termineConfig.updatedAt), bleibt', async () => {
|
||
legeEntwurf(1, { tage: 200, cfgTage: 200 })
|
||
// der Schreibweg von PUT /api/termine/fragebogen: aendert nur das termineConfig-Dokument, nie das Funktionsdokument
|
||
await speichereFragebogen(dok(1), (bisher) => bisher, { now: new Date(JETZT.getTime() - 10 * TAG), by: 'kunde' })
|
||
assert.equal(Date.parse(dok(1).$updatedAt), Date.parse(vor(200)), 'das Funktionsdokument blieb unberührt')
|
||
assert.equal(Date.parse(einstellung(1).updatedAt), Date.parse(vor(10)))
|
||
|
||
const erg = await loescheEntwuerfe(JETZT)
|
||
assert.deepEqual(erg, { geprueft: 1, geloescht: 0, uebrig: 0, fehler: 0 })
|
||
assert.ok(dok(1))
|
||
assert.ok(einstellung(1))
|
||
// 91 Tage nach der Bearbeitung ist auch dieser Entwurf fällig
|
||
const spaeter = await loescheEntwuerfe(new Date(JETZT.getTime() + 81 * TAG + 1000))
|
||
assert.deepEqual(spaeter, { geprueft: 1, geloescht: 1, uebrig: 0, fehler: 0 })
|
||
assert.equal(dok(1), undefined)
|
||
})
|
||
|
||
test('Frist: ein frisch geändertes Funktionsdokument (z. B. nach abgebrochener Zahlung wieder auf off) bleibt, auch wenn die Einstellung alt ist', async () => {
|
||
legeEntwurf(1, { tage: 3, cfgTage: 200, doc: { purchaseStatus: 'failed' } })
|
||
const erg = await loescheEntwuerfe(JETZT)
|
||
assert.deepEqual(erg, { geprueft: 1, geloescht: 0, uebrig: 0, fehler: 0 })
|
||
assert.ok(dok(1))
|
||
assert.ok(einstellung(1))
|
||
})
|
||
|
||
test('Funktionsdokument ohne Einstellung und älter als 90 Tage: das Dokument geht, ohne Einstellung wird nichts gelesen oder gelöscht', async () => {
|
||
legeEntwurf(1, { tage: 100, config: false })
|
||
legeEntwurf(2, { tage: 20, config: false })
|
||
const erg = await loescheEntwuerfe(JETZT)
|
||
assert.deepEqual(erg, { geprueft: 2, geloescht: 1, uebrig: 0, fehler: 0 })
|
||
assert.equal(dok(1), undefined)
|
||
assert.ok(dok(2))
|
||
})
|
||
|
||
test('Nur Termine im Zustand off: ordered, setup, active und disabled bleiben unabhängig vom Alter, ebenso andere Funktionen', async () => {
|
||
for (const [nr, state] of [[1, 'ordered'], [2, 'setup'], [3, 'active'], [4, 'disabled']]) {
|
||
legeEntwurf(nr, { tage: 400, doc: { state, enabled: state === 'active', ...(state === 'disabled' ? { disabledAt: vor(400), disabledReason: 'team' } : {}) } })
|
||
}
|
||
legeEntwurf(5, { tage: 400, key: 'news' })
|
||
legeEntwurf(6, { tage: 400, key: 'analyse', config: false })
|
||
const vorher = [...aw.docs('portalFeatures').values()].map((d) => ({ ...d }))
|
||
const erg = await loescheEntwuerfe(JETZT)
|
||
assert.deepEqual(erg, { geprueft: 0, geloescht: 0, uebrig: 0, fehler: 0 })
|
||
assert.deepEqual([...aw.docs('portalFeatures').values()], vorher)
|
||
assert.equal(aw.docs('termineConfig').size, 5, 'keine Einstellung wurde gelöscht')
|
||
// die Abfrage nennt Funktion und Zustand
|
||
const abfrage = aw.listCalls('portalFeatures')[0].queries
|
||
assert.deepEqual(abfrage.filter((q) => q.method === 'equal').map((q) => [q.attribute, q.values]), [['featureKey', ['termine']], ['state', ['off']]])
|
||
})
|
||
|
||
// ---------------------------------------------------------------- Spuren von Betrieb und Zahlung
|
||
|
||
const SPUREN = [
|
||
['activatedAt', { activatedAt: vor(200) }],
|
||
['ticketId', { ticketId: 'wo_12345' }],
|
||
['disabledAt', { disabledAt: vor(150) }],
|
||
['unlockedByPurchase', { unlockedByPurchase: true }],
|
||
['purchaseStatus paid', { purchaseStatus: 'paid' }],
|
||
]
|
||
|
||
test('Ein Funktionsdokument, das in Betrieb oder bezahlt war, verliert nur die Einstellung (der siteKey bleibt gültig)', async () => {
|
||
SPUREN.forEach(([, felder], i) => legeEntwurf(i + 1, { doc: felder }))
|
||
const vorher = SPUREN.map((_, i) => ({ ...dok(i + 1) }))
|
||
const erg = await loescheEntwuerfe(JETZT)
|
||
assert.deepEqual(erg, { geprueft: 5, geloescht: 5, uebrig: 0, fehler: 0 }, 'die Einstellung ist gelöscht, das zählt als Löschung')
|
||
SPUREN.forEach(([name], i) => {
|
||
assert.deepEqual(dok(i + 1), vorher[i], `${name}: Funktionsdokument unverändert`)
|
||
assert.equal(dok(i + 1).siteKey, SITE_KEY)
|
||
assert.equal(einstellung(i + 1), undefined, `${name}: Einstellung gelöscht`)
|
||
})
|
||
// am nächsten Tag ist nichts mehr zu tun
|
||
assert.deepEqual(await loescheEntwuerfe(new Date(JETZT.getTime() + TAG)), { geprueft: 5, geloescht: 0, uebrig: 0, fehler: 0 })
|
||
})
|
||
|
||
test('Von E8 zurückgesetztes Dokument (off, purchaseStatus zurueckgesetzt): nach 91 Tagen bleiben Dokument und siteKey unverändert, eine vorhandene Einstellung wird gelöscht', async () => {
|
||
const zurueckgesetzt = { purchaseStatus: 'zurueckgesetzt', unlockedByPurchase: false, ticketId: '', activatedAt: null, disabledAt: null, disabledReason: null, zahlungMeldung: '', adminNote: 'Daten gelöscht (Löschfrist abgelaufen)' }
|
||
legeEntwurf(1, { tage: 91, doc: zurueckgesetzt }) // mit neuem Entwurf
|
||
legeEntwurf(2, { tage: 400, config: false, doc: zurueckgesetzt }) // ohne Einstellung
|
||
const vorher = [{ ...dok(1) }, { ...dok(2) }]
|
||
const erg = await loescheEntwuerfe(JETZT)
|
||
assert.deepEqual(erg, { geprueft: 2, geloescht: 1, uebrig: 0, fehler: 0 })
|
||
assert.deepEqual(dok(1), vorher[0])
|
||
assert.deepEqual(dok(2), vorher[1])
|
||
assert.equal(dok(1).siteKey, SITE_KEY)
|
||
assert.equal(einstellung(1), undefined)
|
||
})
|
||
|
||
test('Nach fehlgeschlagener Zahlung (purchaseStatus failed) ist der Entwurf nach 90 Tagen weg, davor nicht', async () => {
|
||
legeEntwurf(1, { tage: 89, doc: { purchaseStatus: 'failed', stripeSessionId: 'cs_test_1', orderedAt: vor(89) } })
|
||
legeEntwurf(2, { tage: 91, doc: { purchaseStatus: 'failed', stripeSessionId: 'cs_test_2', orderedAt: vor(91) } })
|
||
const erg = await loescheEntwuerfe(JETZT)
|
||
assert.deepEqual(erg, { geprueft: 2, geloescht: 1, uebrig: 0, fehler: 0 })
|
||
assert.ok(dok(1))
|
||
assert.equal(dok(2), undefined)
|
||
assert.equal(einstellung(2), undefined)
|
||
})
|
||
|
||
// ---------------------------------------------------------------- frisch lesen unter den Sperren
|
||
|
||
test('Rennen: wird der Entwurf bestellt, während der Lauf auf die Sperre feature-order wartet, liest der Lauf frisch und löscht nichts', async () => {
|
||
legeEntwurf(1)
|
||
let freigeben
|
||
const gehalten = new Promise((fertig) => (freigeben = fertig))
|
||
const sperre = withLocks([`feature-order:${KUNDE}:${pid(1)}`], () => gehalten)
|
||
const lauf = loescheEntwuerfe(JETZT)
|
||
await warteBis(() => aw.listCalls('portalFeatures').length > 0) // der Kandidat steht in der Liste, jetzt wartet der Lauf
|
||
// gleich alt wie vorher (seed behält $updatedAt): nur das frische Lesen des Zustands kann das Löschen noch verhindern
|
||
aw.seed('portalFeatures', { ...dok(1), state: 'ordered', stripeSessionId: 'cs_test_9' })
|
||
freigeben()
|
||
await sperre
|
||
const erg = await lauf
|
||
assert.deepEqual(erg, { geprueft: 1, geloescht: 0, uebrig: 0, fehler: 0 })
|
||
assert.equal(dok(1).state, 'ordered')
|
||
assert.ok(einstellung(1), 'die Einstellung der Bestellung bleibt')
|
||
})
|
||
|
||
test('Rennen: dieselbe Lage unter feature-admin und termine-cfg (Team schaltet ein, Kunde speichert)', async () => {
|
||
legeEntwurf(1)
|
||
legeEntwurf(2)
|
||
let freigeben
|
||
const gehalten = new Promise((fertig) => (freigeben = fertig))
|
||
const sperre = withLocks([`feature-admin:${pid(1)}`, `termine-cfg:${fid(2)}`], () => gehalten)
|
||
const lauf = loescheEntwuerfe(JETZT)
|
||
await warteBis(() => aw.listCalls('portalFeatures').length > 0)
|
||
aw.seed('portalFeatures', { ...dok(1), state: 'active', enabled: true })
|
||
aw.seed('termineConfig', { ...einstellung(2), updatedAt: vor(1) }) // der Kunde hat den Entwurf gerade bearbeitet
|
||
freigeben()
|
||
await sperre
|
||
const erg = await lauf
|
||
assert.deepEqual(erg, { geprueft: 2, geloescht: 0, uebrig: 0, fehler: 0 })
|
||
assert.equal(dok(1).state, 'active')
|
||
assert.ok(einstellung(1))
|
||
assert.ok(dok(2))
|
||
assert.ok(einstellung(2))
|
||
})
|
||
|
||
test('Buchungen zur Funktion: nichts wird gelöscht, weder Einstellung noch Dokument', async () => {
|
||
legeEntwurf(1)
|
||
legeEntwurf(2)
|
||
aw.seed('termineBookings', { $id: 'b_aaaaaaaa1', featureId: fid(1), status: 'bestaetigt' })
|
||
const erg = await loescheEntwuerfe(JETZT)
|
||
assert.deepEqual(erg, { geprueft: 2, geloescht: 1, uebrig: 0, fehler: 0 })
|
||
assert.ok(dok(1))
|
||
assert.ok(einstellung(1))
|
||
assert.equal(dok(2), undefined)
|
||
})
|
||
|
||
// ---------------------------------------------------------------- Deckel
|
||
|
||
test('130 Kandidaten: der erste Lauf löscht 100 und lässt 30 übrig, der zweite löscht die 30; die Liste wird seitenweise gelesen', async () => {
|
||
legeViele(100, 229)
|
||
let erg = await loescheEntwuerfe(JETZT)
|
||
assert.deepEqual(erg, { geprueft: 130, geloescht: 100, uebrig: 30, fehler: 0 })
|
||
assert.equal(aw.docs('portalFeatures').size, 30)
|
||
assert.equal(aw.docs('termineConfig').size, 30)
|
||
const abfragen = aw.listCalls('portalFeatures')
|
||
assert.equal(abfragen.length, 2, 'zwei Seiten')
|
||
for (const abfrage of abfragen) {
|
||
assert.ok(abfrage.queries.some((q) => q.method === 'limit' && q.values[0] === 100), 'Seite zu 100')
|
||
assert.ok(abfrage.queries.some((q) => q.method === 'orderAsc' && q.attribute === '$id'))
|
||
}
|
||
assert.equal(abfragen[0].queries.some((q) => q.method === 'cursorAfter'), false)
|
||
assert.equal(abfragen[1].queries.find((q) => q.method === 'cursorAfter').values[0], fid(199), 'die zweite Seite beginnt nach dem 100. Dokument')
|
||
assert.match(logText(), /Rest folgt/, 'der Deckel steht im Log')
|
||
|
||
erg = await loescheEntwuerfe(new Date(JETZT.getTime() + TAG))
|
||
assert.deepEqual(erg, { geprueft: 30, geloescht: 30, uebrig: 0, fehler: 0 })
|
||
assert.equal(aw.docs('portalFeatures').size, 0)
|
||
assert.equal(aw.docs('termineConfig').size, 0)
|
||
})
|
||
|
||
test('Übrig zählt nur Kandidaten, die der Deckel aufhält: junge Entwürfe in der Liste sind nie „offen“', async () => {
|
||
legeViele(100, 219) // 120 fällige
|
||
legeViele(220, 239, { tage: 10 }) // 20 junge
|
||
const erg = await loescheEntwuerfe(JETZT)
|
||
assert.deepEqual(erg, { geprueft: 140, geloescht: 100, uebrig: 20, fehler: 0 })
|
||
})
|
||
|
||
test('Nach 2000 geprüften Dokumenten endet der Lauf mit einer Logzeile ohne Kennungen; genau 2000 enden ohne', async () => {
|
||
legeViele(1000, 2999, { tage: 10 }) // 2000 junge: nichts zu tun
|
||
let erg = await loescheEntwuerfe(JETZT)
|
||
assert.deepEqual(erg, { geprueft: 2000, geloescht: 0, uebrig: 0, fehler: 0 })
|
||
assert.equal(log.warn.mock.calls.length, 0, 'genau 2000: der Lauf ist vollständig')
|
||
|
||
legeEntwurf(3000, { tage: 10 })
|
||
aw.resetListCalls()
|
||
erg = await loescheEntwuerfe(JETZT)
|
||
assert.deepEqual(erg, { geprueft: 2000, geloescht: 0, uebrig: 0, fehler: 0 })
|
||
assert.equal(log.warn.mock.calls.length, 1)
|
||
assert.match(String(log.warn.mock.calls[0].arguments[0]), /^\[termine-entwuerfe\] .*2000.*nächsten Tagen/)
|
||
assert.equal(aw.listCalls('portalFeatures').length, 21, '21 Seiten: die 21. zeigt, dass es weitergeht')
|
||
})
|
||
|
||
// ---------------------------------------------------------------- Fehler
|
||
|
||
test('Einzelfehler beim Löschen: fehler 1, die übrigen werden gelöscht, der Rest folgt am nächsten Tag', async () => {
|
||
legeViele(1, 3)
|
||
aw.failNext({ collection: 'termineConfig', method: 'DELETE', status: 500, message: 'Nachbau kaputt' }) // trifft den ersten Kandidaten
|
||
const erg = await loescheEntwuerfe(JETZT)
|
||
assert.deepEqual(erg, { geprueft: 3, geloescht: 2, uebrig: 0, fehler: 1 })
|
||
assert.ok(dok(1), 'bei einem Fehler an der Einstellung bleibt auch das Dokument')
|
||
assert.ok(einstellung(1))
|
||
assert.equal(dok(2), undefined)
|
||
assert.equal(dok(3), undefined)
|
||
assert.equal(log.error.mock.calls.length, 1)
|
||
assert.match(String(log.error.mock.calls[0].arguments[0]), /^\[termine-entwuerfe\] .*Nachbau kaputt/)
|
||
|
||
const naechster = await loescheEntwuerfe(new Date(JETZT.getTime() + TAG))
|
||
assert.deepEqual(naechster, { geprueft: 1, geloescht: 1, uebrig: 0, fehler: 0 })
|
||
assert.equal(dok(1), undefined)
|
||
})
|
||
|
||
test('Scheitert nur das Löschen des Funktionsdokuments, ist die Einstellung weg und der nächste Lauf löscht das Dokument', async () => {
|
||
legeEntwurf(1)
|
||
aw.failNext({ collection: 'portalFeatures', method: 'DELETE', status: 500, message: 'Nachbau kaputt' })
|
||
const erg = await loescheEntwuerfe(JETZT)
|
||
assert.deepEqual(erg, { geprueft: 1, geloescht: 0, uebrig: 0, fehler: 1 })
|
||
assert.ok(dok(1))
|
||
assert.equal(einstellung(1), undefined)
|
||
|
||
assert.deepEqual(await loescheEntwuerfe(new Date(JETZT.getTime() + TAG)), { geprueft: 1, geloescht: 1, uebrig: 0, fehler: 0 })
|
||
assert.equal(dok(1), undefined)
|
||
})
|
||
|
||
test('Ein Lesefehler beim Prüfen eines Kandidaten zählt als Fehler und löscht nichts; die übrigen Kandidaten laufen weiter', async () => {
|
||
legeViele(1, 2)
|
||
// 1. GET auf portalFeatures = die Liste, 2. = frisches Lesen des ersten Kandidaten
|
||
aw.failNext({ collection: 'portalFeatures', method: 'GET', status: 500, message: 'Nachbau kaputt', after: 1 })
|
||
const erg = await loescheEntwuerfe(JETZT)
|
||
assert.deepEqual(erg, { geprueft: 2, geloescht: 1, uebrig: 0, fehler: 1 })
|
||
assert.ok(dok(1))
|
||
assert.ok(einstellung(1))
|
||
assert.equal(dok(2), undefined)
|
||
})
|
||
|
||
test('Ist die Liste nicht lesbar, gibt es keinen Wurf: fehler 1, nichts geprüft', async () => {
|
||
legeEntwurf(1)
|
||
aw.failNext({ collection: 'portalFeatures', method: 'GET', status: 500, message: 'Nachbau kaputt' })
|
||
assert.deepEqual(await loescheEntwuerfe(JETZT), { geprueft: 0, geloescht: 0, uebrig: 0, fehler: 1 })
|
||
assert.ok(dok(1))
|
||
})
|
||
|
||
test('Die Zeit muss ein gültiges Date sein', async () => {
|
||
await assert.rejects(() => loescheEntwuerfe(), TypeError)
|
||
await assert.rejects(() => loescheEntwuerfe('heute'), TypeError)
|
||
})
|
||
|
||
// ---------------------------------------------------------------- Kundenloeschung
|
||
|
||
test('Kunde löschen: loescheEntwuerfeVonKunde entfernt alle off-Entwürfe des Kunden vollständig, unabhängig vom Alter und von Spuren', async () => {
|
||
legeEntwurf(1, { tage: 1 }) // frisch
|
||
legeEntwurf(2, { tage: 400, doc: { purchaseStatus: 'zurueckgesetzt' } }) // zurückgesetzt, der siteKey fällt mit dem Kunden
|
||
legeEntwurf(3, { tage: 5, doc: { activatedAt: vor(300), unlockedByPurchase: true } })
|
||
legeEntwurf(4, { tage: 5, config: false })
|
||
// nicht betroffen: andere Zustände, News, anderer Kunde
|
||
legeEntwurf(5, { tage: 400, doc: { state: 'active', enabled: true } })
|
||
legeEntwurf(6, { tage: 400, doc: { state: 'ordered' } })
|
||
legeEntwurf(7, { tage: 400, doc: { state: 'disabled', disabledAt: vor(5), disabledReason: 'kunde_geloescht' } })
|
||
legeEntwurf(8, { tage: 400, key: 'news' })
|
||
legeEntwurf(9, { tage: 400, kunde: ANDERER })
|
||
|
||
assert.deepEqual(await loescheEntwuerfeVonKunde(KUNDE), { geloescht: 4 })
|
||
for (const nr of [1, 2, 3, 4]) {
|
||
assert.equal(dok(nr), undefined, `Funktionsdokument ${nr}`)
|
||
assert.equal(einstellung(nr), undefined, `Einstellung ${nr}`)
|
||
}
|
||
for (const nr of [5, 6, 7, 9]) {
|
||
assert.ok(dok(nr), `Funktionsdokument ${nr} bleibt`)
|
||
assert.ok(einstellung(nr), `Einstellung ${nr} bleibt`)
|
||
}
|
||
assert.ok(aw.docs('portalFeatures').get('f_p_e8_news'))
|
||
assert.deepEqual(await loescheEntwuerfeVonKunde(KUNDE), { geloescht: 0 }, 'ein zweiter Aufruf findet nichts mehr')
|
||
assert.deepEqual(await loescheEntwuerfeVonKunde('c_unbekannt'), { geloescht: 0 })
|
||
})
|
||
|
||
test('Kunde löschen: Buchungen zur Funktion halten den Entwurf zurück, der Zustand wird unter den Sperren frisch gelesen', async () => {
|
||
legeEntwurf(1, { tage: 1 })
|
||
legeEntwurf(2, { tage: 1 })
|
||
aw.seed('termineBookings', { $id: 'b_aaaaaaaa1', featureId: fid(1), status: 'bestaetigt' })
|
||
let freigeben
|
||
const gehalten = new Promise((fertig) => (freigeben = fertig))
|
||
const sperre = withLocks([`feature-order:${KUNDE}:${pid(2)}`], () => gehalten)
|
||
aw.resetListCalls()
|
||
const lauf = loescheEntwuerfeVonKunde(KUNDE)
|
||
await warteBis(() => aw.listCalls('portalFeatures').length > 0)
|
||
aw.seed('portalFeatures', { ...dok(2), state: 'ordered', stripeSessionId: 'cs_test_9' })
|
||
freigeben()
|
||
await sperre
|
||
assert.deepEqual(await lauf, { geloescht: 0 })
|
||
assert.ok(dok(1))
|
||
assert.ok(einstellung(1))
|
||
assert.equal(dok(2).state, 'ordered')
|
||
assert.ok(einstellung(2))
|
||
})
|
||
|
||
test('Kunde löschen: ein Fehler bei einem Entwurf hält die übrigen nicht auf und wird danach weitergereicht', async () => {
|
||
legeViele(1, 3, { tage: 1 })
|
||
aw.failNext({ collection: 'termineConfig', method: 'DELETE', status: 500, message: 'Nachbau kaputt' })
|
||
await assert.rejects(() => loescheEntwuerfeVonKunde(KUNDE), /Nachbau kaputt/)
|
||
assert.ok(dok(1), 'der Entwurf mit Fehler bleibt')
|
||
assert.equal(dok(2), undefined)
|
||
assert.equal(dok(3), undefined)
|
||
assert.deepEqual(await loescheEntwuerfeVonKunde(KUNDE), { geloescht: 1 }, 'ein zweiter Versuch holt ihn nach')
|
||
})
|