AB-Kern: Erinnerung mit neuem Token und geprüfter PDF-Datei

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
ClaudeandClaude Sonnet 5.5 committed 2026-10-07 14:14:46 +00:00
1 parent c74f7eeea7
commit 0da5b30119
2 files changed
+158 -1

No files matched your search

+121
View File
@@ -0,0 +1,121 @@
import { test, after, beforeEach, mock } from 'node:test'
import assert from 'node:assert/strict'
import fs from 'node:fs'
import path from 'node:path'
import {
starteUmgebung, KUNDE_ID, PROJEKT_ID, TICKET_ID, ZWEITES_TICKET, FREMD, VON, MELDUNG, festpreisEingabe, kundeDoc, projektDoc,
baueDeps, sha256, abgelehnt, dateienIn, ergebnis, tor, warte, wartebis,
} from './helpers/auftraegeAb.js'
const umg = await starteUmgebung()
const { aw, config, uploadDir } = umg
after(() => umg.schliesse())
beforeEach(umg.zuruecksetzen)
const { getAuftrag, updateAuftrag, findAuftragByTokenHash } = await import('../server/services/auftraege/auftragStore.js')
const { createProjektRechnung, getProjektRechnung } = await import('../server/services/auftraege/projektRechnungenStore.js')
const { lesePdf } = await import('../server/services/auftraege/ablage.js')
const { sperreAuftrag } = await import('../server/services/auftraege/konstanten.js')
const { withLocks } = await import('../server/services/keyedLock.js')
const { legeEntwurfAn, aendereEntwurf, loescheEntwurf, sendeAuftrag, erinnereAuftrag, neueFassung, storniereAuftrag, ordneProjektZu } = await import('../server/services/auftraege/auftraege.js')
const JETZT = new Date('2026-10-06T10:00:00Z')
/** Eine versendete AB (Entwurf anlegen und senden); liefert den Auftrag. */
async function gesendet(deps, patch) {
const entwurf = await legeEntwurfAn(festpreisEingabe(patch), { von: VON })
return (await sendeAuftrag(entwurf.id, { von: VON, deps, jetzt: JETZT })).auftrag
}
/**
* Hält die Sperre der AB wie L5 beim Bestätigen (sperreAuftrag(id), V2), startet die Aktion, wartet, bis sie an der Sperre
* steht, führt waehrend() aus (dort schreibt „L5“) und liefert das Ergebnis der Aktion ({ wert } oder { fehler }) nach dem Freigeben.
*/
async function unterFremderSperre(id, { aktion, bereit = () => true, waehrend }) {
const sperre = tor()
const haelt = withLocks(sperreAuftrag(id), () => sperre.promise)
const lauf = ergebnis(aktion())
await wartebis(bereit)
await warte(30) // die Aktion steht jetzt an der Sperre und darf noch nichts geschrieben haben
await waehrend()
sperre.freigeben()
await haelt
return lauf
}
test('erinnereAuftrag: neuer Token (der alte gilt nicht mehr), gleiche PDF-Datei, Mail mit erinnerung: true, Versanddatum bleibt', async () => {
const { deps, calls } = baueDeps()
const erste = await gesendet(deps)
const spaeter = new Date('2026-10-20T08:00:00Z')
const { auftrag, mailFehler } = await erinnereAuftrag(erste.id, { deps, jetzt: spaeter })
assert.equal(mailFehler, false)
assert.equal(calls.token.length, 2)
assert.notEqual(auftrag.tokenHash, erste.tokenHash)
assert.equal(auftrag.tokenHash, sha256(calls.token[1]))
assert.equal(new Date(auftrag.tokenGueltigBis).getTime(), spaeter.getTime() + 60 * 86_400_000)
assert.equal(await findAuftragByTokenHash(erste.tokenHash), null)
assert.equal((await findAuftragByTokenHash(auftrag.tokenHash)).id, erste.id)
assert.deepEqual(
[auftrag.status, auftrag.nummer, auftrag.gesendetAm, auftrag.pdfDatei, auftrag.pdfSha256, auftrag.mailStatus],
['versendet', erste.nummer, erste.gesendetAm, erste.pdfDatei, erste.pdfSha256, 'gesendet'],
)
assert.equal(calls.pdf.length, 1) // kein neues PDF
assert.equal(calls.mail.length, 2)
assert.deepEqual(calls.mail[1].pdf, await lesePdf(erste.pdfDatei))
assert.deepEqual([calls.mail[1].token, calls.mail[1].erinnerung], [calls.token[1], true])
assert.deepEqual(await getAuftrag(erste.id), auftrag)
})
test('erinnereAuftrag nach einem Mail-Fehler: gewöhnliche Mail ohne Erinnerungs-Hinweis an die aktuelle Adresse des Kunden', async () => {
mock.method(console, 'error', () => {})
const erste = await gesendet(baueDeps({ mailFehler: new Error('Verbindung abgebrochen') }).deps)
assert.deepEqual([erste.mailStatus, erste.gesendetAn], ['fehler', 'anna@example.invalid'])
aw.seed(config.collections.customers, kundeDoc({ email: 'neu@example.invalid' })) // der Admin hat die Adresse korrigiert
const { deps, calls } = baueDeps()
const { auftrag, mailFehler } = await erinnereAuftrag(erste.id, { deps })
assert.equal(mailFehler, false)
assert.deepEqual([auftrag.mailStatus, auftrag.mailFehler, auftrag.gesendetAn], ['gesendet', '', 'neu@example.invalid'])
assert.deepEqual([calls.mail[0].erinnerung, calls.mail[0].auftrag.gesendetAn], [false, 'neu@example.invalid'])
})
test('erinnereAuftrag: scheitert die Mail, gilt trotzdem der neue Token; das Ergebnis meldet mailFehler', async () => {
mock.method(console, 'error', () => {})
const erste = await gesendet(baueDeps().deps)
const kaputt = baueDeps({ mailFehler: Object.assign(new Error('x'), { code: 'ECONNREFUSED' }) })
const { auftrag, mailFehler } = await erinnereAuftrag(erste.id, { deps: kaputt.deps })
assert.equal(mailFehler, true)
assert.deepEqual([auftrag.status, auftrag.mailStatus, auftrag.mailFehler], ['versendet', 'fehler', 'E-Mail konnte nicht gesendet werden (ECONNREFUSED).'])
assert.equal(auftrag.tokenHash, sha256(kaputt.calls.token[0]))
assert.equal(await findAuftragByTokenHash(erste.tokenHash), null)
})
test('erinnereAuftrag: nur versendete (409), unbekannt 404, ohne E-Mail 400, beschädigte PDF-Datei 500 – jeweils ohne neuen Token', async () => {
mock.method(console, 'error', () => {})
const { deps } = baueDeps()
const a = await gesendet(deps)
for (const status of ['entwurf', 'bestaetigt', 'abgerechnet', 'storniert', 'ersetzt', 'widerrufen']) {
await updateAuftrag(a.id, { status })
await abgelehnt(erinnereAuftrag(a.id, { deps }), 409, MELDUNG.keineErinnerung, status)
}
await abgelehnt(erinnereAuftrag('a_gibtsnicht', { deps }), 404, MELDUNG.auftragFehlt)
await updateAuftrag(a.id, { status: 'versendet' })
aw.seed(config.collections.customers, kundeDoc({ email: ' ' }))
await abgelehnt(erinnereAuftrag(a.id, { deps }), 400, MELDUNG.keineMail)
aw.seed(config.collections.customers, kundeDoc())
fs.writeFileSync(path.join(uploadDir, a.pdfDatei), 'manipuliert')
await abgelehnt(erinnereAuftrag(a.id, { deps }), 500, MELDUNG.pruefsumme)
assert.equal((await getAuftrag(a.id)).tokenHash, a.tokenHash)
})
test('erinnereAuftrag wartet auf die Sperre der AB und prüft den Stand danach (L5 bestätigt gerade: keine Erinnerung mehr)', async () => {
const { deps, calls } = baueDeps()
const a = await gesendet(deps)
const lauf = await unterFremderSperre(a.id, {
aktion: () => erinnereAuftrag(a.id, { deps }),
waehrend: async () => {
assert.equal((await getAuftrag(a.id)).tokenHash, a.tokenHash) // noch kein neuer Token
await updateAuftrag(a.id, { status: 'bestaetigt', bestaetigtAm: new Date().toISOString(), bestaetigtUeber: 'link' })
},
})
assert.deepEqual([lauf.fehler?.status, lauf.fehler?.message], [409, MELDUNG.keineErinnerung])
assert.equal(calls.mail.length, 1) // nur die Mail des ersten Sendens
assert.equal((await getAuftrag(a.id)).tokenHash, a.tokenHash)
})