Files

230 lines
13 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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, VON, MELDUNG, festpreisEingabe, kundeDoc, baueDeps, sha256, abgelehnt,
ergebnis, tor, warte, wartebis, dateienIn,
} from './helpers/auftraegeAb.js'
const umg = await starteUmgebung()
const { aw, config, uploadDir } = umg
after(() => umg.schliesse())
beforeEach(umg.zuruecksetzen)
const { deleteDocument } = await import('../server/services/appwriteAdmin.js')
const { getAuftrag, listAuftraege, updateAuftrag } = await import('../server/services/auftraege/auftragStore.js')
const { lesePdf } = await import('../server/services/auftraege/ablage.js')
const { kundeSnapshot } = await import('../server/services/auftraege/auftragSicht.js')
const { TEXTE_FASSUNG } = await import('../server/services/auftraege/texte.js')
const { AGB_FASSUNG, TOKEN_GUELTIG_TAGE } = await import('../server/services/auftraege/konstanten.js')
const { AuftragError } = await import('../server/services/auftraege/fehler.js')
const { legeEntwurfAn, aendereEntwurf, loescheEntwurf, sendeAuftrag } = await import('../server/services/auftraege/auftraege.js')
const JETZT = new Date('2026-10-06T10:00:00Z') // Berlin: 12:00
const neuerEntwurf = (patch) => legeEntwurfAn(festpreisEingabe(patch), { von: VON })
const sende = (id, deps, extra = {}) => sendeAuftrag(id, { von: VON, deps, jetzt: JETZT, ...extra })
const VOR_DER_NUMMER = [
['Interessent', { customerStatus: 'lead' }, 400, MELDUNG.keinFesterKunde],
['verlorener Kunde (Schreibweise egal)', { customerStatus: ' Lost ' }, 400, MELDUNG.keinFesterKunde],
['Kundenart fehlt', { kundenart: '' }, 400, MELDUNG.keineKundenart],
['Kundenart unbekannt', { kundenart: 'firma' }, 400, MELDUNG.keineKundenart],
['E-Mail fehlt', { email: '' }, 400, MELDUNG.keineMail],
['E-Mail nur Leerzeichen', { email: ' ' }, 400, MELDUNG.keineMail],
['Straße fehlt', { street: '' }, 409, MELDUNG.adresse],
['PLZ nur Leerzeichen', { postalCode: ' ' }, 409, MELDUNG.adresse],
['Ort fehlt', { city: '' }, 409, MELDUNG.adresse],
['Interessent hat Vorrang vor allem anderen', { customerStatus: 'lead', kundenart: '', email: '', street: '' }, 400, MELDUNG.keinFesterKunde],
['Kundenart kommt vor E-Mail und Anschrift', { kundenart: '', email: '', street: '' }, 400, MELDUNG.keineKundenart],
['E-Mail kommt vor der Anschrift', { email: '', street: '' }, 400, MELDUNG.keineMail],
]
test('Senden: unvollständige Kundendaten werden vor der Nummer abgelehnt – nichts wird erzeugt, gespeichert oder verbraucht', async () => {
const entwurf = await neuerEntwurf()
const { deps, calls } = baueDeps()
for (const [name, patch, status, meldung] of VOR_DER_NUMMER) {
aw.seed(config.collections.customers, kundeDoc(patch))
await abgelehnt(sende(entwurf.id, deps), status, meldung, name)
}
assert.deepEqual([calls.pdf.length, calls.token.length, calls.mail.length], [0, 0, 0])
assert.deepEqual(dateienIn(uploadDir), [])
const unveraendert = await getAuftrag(entwurf.id)
assert.deepEqual([unveraendert.status, unveraendert.nummer, unveraendert.jahr], ['entwurf', '', null])
aw.seed(config.collections.customers, kundeDoc()) // der Kunde ist wieder vollständig: die erste echte Nummer ist 0001
assert.equal((await sende(entwurf.id, deps)).auftrag.nummer, 'AB-2026-0001')
})
test('Senden: der Entwurf wird vor der Nummer noch einmal geprüft (beschädigte Daten ergeben die Feldmeldung)', async () => {
const entwurf = await neuerEntwurf()
await updateAuftrag(entwurf.id, { positionen: [] })
await abgelehnt(sende(entwurf.id, baueDeps().deps), 400, MELDUNG.positionen)
})
test('Senden: Nummer, eingefrorener Kunde, PDF mit Prüfsumme, Token und Mail', async () => {
aw.seed(config.collections.financeSettings, { $id: 'main', steuernummer: '12/345/67890', bankDetailsText: 'IBAN DE00 0000 0000 0000 0000 00' })
const entwurf = await neuerEntwurf()
const { deps, calls } = baueDeps()
const { auftrag, mailFehler } = await sende(entwurf.id, deps)
assert.equal(mailFehler, false)
assert.deepEqual([auftrag.id, auftrag.status, auftrag.nummer, auftrag.jahr, auftrag.laufnummer], [entwurf.id, 'versendet', 'AB-2026-0001', 2026, 1])
assert.equal(new Date(auftrag.gesendetAm).getTime(), JETZT.getTime())
assert.equal(auftrag.gesendetAn, 'anna@example.invalid')
assert.deepEqual(auftrag.kunde, kundeSnapshot(kundeDoc()))
assert.equal(auftrag.kunde.kundenart, 'unternehmen')
assert.deepEqual([auftrag.texteFassung, auftrag.agbFassung], [TEXTE_FASSUNG, AGB_FASSUNG])
assert.deepEqual([auftrag.mailStatus, auftrag.mailFehler], ['gesendet', ''])
// PDF: einmal mit der Nummer und dem Kunden-Schnappschuss erzeugt, abgelegt, Prüfsumme gespeichert
const pdf = Buffer.from('%PDF-1.4 AB-2026-0001')
assert.equal(calls.pdf.length, 1)
assert.equal(calls.pdf[0].auftrag.nummer, 'AB-2026-0001')
assert.deepEqual(calls.pdf[0].auftrag.kunde, kundeSnapshot(kundeDoc()))
assert.deepEqual(calls.pdf[0].optionen, { settings: { steuernummer: '12/345/67890', bankDetailsText: 'IBAN DE00 0000 0000 0000 0000 00' } })
assert.equal(auftrag.pdfDatei, `${KUNDE_ID}/${entwurf.id}.pdf`)
assert.equal(auftrag.pdfSha256, sha256(pdf))
assert.deepEqual(await lesePdf(auftrag.pdfDatei), pdf)
// Token: im Dokument nur der Hash, 60 Tage gültig ab dem Versand
assert.equal(calls.token.length, 1)
assert.equal(auftrag.tokenHash, sha256(calls.token[0]))
assert.equal(new Date(auftrag.tokenGueltigBis).getTime(), JETZT.getTime() + TOKEN_GUELTIG_TAGE * 86_400_000)
// Mail: einmal, mit dem gespeicherten Dokument, dem PDF und dem Klartext-Token
assert.equal(calls.mail.length, 1)
assert.equal(calls.mail[0].auftrag.nummer, 'AB-2026-0001')
assert.deepEqual(calls.mail[0].pdf, pdf)
assert.deepEqual([calls.mail[0].token, calls.mail[0].erinnerung], [calls.token[0], false])
assert.deepEqual(await getAuftrag(entwurf.id), auftrag)
})
test('Senden: Nummern zählen fortlaufend, im neuen Berliner Jahr beginnt wieder 0001', async () => {
const { deps } = baueDeps()
const nummern = []
for (let i = 0; i < 2; i += 1) nummern.push((await sende((await neuerEntwurf()).id, deps)).auftrag.nummer)
const neujahr = await sende((await neuerEntwurf()).id, deps, { jetzt: new Date('2026-12-31T23:30:00Z') })
assert.deepEqual(nummern, ['AB-2026-0001', 'AB-2026-0002'])
assert.deepEqual([neujahr.auftrag.nummer, neujahr.auftrag.jahr, neujahr.auftrag.laufnummer], ['AB-2027-0001', 2027, 1])
assert.equal(new Date(neujahr.auftrag.gesendetAm).toISOString(), '2026-12-31T23:30:00.000Z')
})
test('Senden: nur Entwürfe (409), unbekannte Aufträge (404) und Kunden (404)', async () => {
const { deps } = baueDeps()
const entwurf = await neuerEntwurf()
await sende(entwurf.id, deps)
await abgelehnt(sende(entwurf.id, deps), 409, MELDUNG.nurEntwurfSenden)
await abgelehnt(sende('a_gibtsnicht', deps), 404, MELDUNG.auftragFehlt)
const zweiter = await neuerEntwurf()
await deleteDocument(config.collections.customers, KUNDE_ID)
await abgelehnt(sende(zweiter.id, deps), 404, MELDUNG.kundeFehlt)
})
test('Senden: scheitert die PDF-Erzeugung (Fehler, leer oder unvollständiger Entwurf), bleibt alles wie vorher und die Nummer ist nicht verbraucht', async () => {
mock.method(console, 'error', () => {})
const entwurf = await neuerEntwurf()
const faelle = [
[baueDeps({ pdfFehler: new Error('pdfkit kaputt') }), 500, MELDUNG.pdf],
[baueDeps({ pdf: Buffer.alloc(0) }), 500, MELDUNG.pdf],
[baueDeps({ pdfFehler: new AuftragError(409, 'Entwurf unvollständig: Anschrift fehlt.') }), 409, 'Entwurf unvollständig: Anschrift fehlt.'],
]
for (const [fehlerhaft, status, meldung] of faelle) {
await abgelehnt(sende(entwurf.id, fehlerhaft.deps), status, meldung)
assert.deepEqual([fehlerhaft.calls.token.length, fehlerhaft.calls.mail.length], [0, 0])
}
assert.deepEqual(dateienIn(uploadDir), [])
const danach = await getAuftrag(entwurf.id)
assert.deepEqual([danach.status, danach.nummer, danach.jahr], ['entwurf', '', null])
assert.ok(!danach.pdfDatei && !danach.tokenHash)
assert.equal((await sende(entwurf.id, baueDeps().deps)).auftrag.nummer, 'AB-2026-0001')
})
test('Senden: eine schon vorhandene Zieldatei wird nie überschrieben – Fehler, Entwurf und Datei bleiben', async () => {
mock.method(console, 'error', () => {})
const entwurf = await neuerEntwurf()
const ziel = path.join(uploadDir, KUNDE_ID, `${entwurf.id}.pdf`)
fs.mkdirSync(path.dirname(ziel), { recursive: true })
fs.writeFileSync(ziel, 'Rest eines Absturzes')
const { deps, calls } = baueDeps()
await abgelehnt(sende(entwurf.id, deps), 500, MELDUNG.pdf)
assert.equal(fs.readFileSync(ziel, 'utf8'), 'Rest eines Absturzes')
assert.equal(calls.mail.length, 0)
assert.equal((await getAuftrag(entwurf.id)).status, 'entwurf')
})
test('Senden: scheitert das Speichern der Auftragsbestätigung, wird die eben abgelegte Datei entfernt und das Senden lässt sich wiederholen', async () => {
mock.method(console, 'error', () => {})
const entwurf = await neuerEntwurf()
const { deps, calls } = baueDeps()
aw.failNext({ collection: config.collections.auftraege, method: 'PATCH', status: 500 })
await assert.rejects(sende(entwurf.id, deps), (err) => err.status === 500)
assert.equal(calls.mail.length, 0)
assert.deepEqual(dateienIn(uploadDir), [])
assert.equal((await getAuftrag(entwurf.id)).status, 'entwurf')
const { auftrag } = await sende(entwurf.id, deps)
assert.equal(auftrag.nummer, 'AB-2026-0001')
assert.deepEqual(dateienIn(uploadDir), [`${KUNDE_ID}/${entwurf.id}.pdf`])
})
test('Senden: scheitert nur die Mail, bleibt die AB versendet (mailStatus fehler) und der Fehlertext verrät nichts Internes', async () => {
mock.method(console, 'error', () => {})
const [e1, e2] = [await neuerEntwurf(), await neuerEntwurf()]
const mitCode = baueDeps({ mailFehler: Object.assign(new Error('535 Login smtp.example.invalid'), { code: 'EAUTH' }) })
const r1 = await sende(e1.id, mitCode.deps)
assert.equal(r1.mailFehler, true)
assert.deepEqual(
[r1.auftrag.status, r1.auftrag.nummer, r1.auftrag.mailStatus, r1.auftrag.mailFehler],
['versendet', 'AB-2026-0001', 'fehler', 'E-Mail konnte nicht gesendet werden (EAUTH).'],
)
assert.ok(r1.auftrag.pdfDatei && r1.auftrag.tokenHash)
assert.deepEqual(await getAuftrag(e1.id), r1.auftrag)
const ohneCode = baueDeps({ mailFehler: new Error('Verbindung zu mail.example.invalid abgebrochen') })
const r2 = await sende(e2.id, ohneCode.deps)
assert.deepEqual([r2.auftrag.nummer, r2.auftrag.mailStatus, r2.auftrag.mailFehler], ['AB-2026-0002', 'fehler', 'E-Mail konnte nicht gesendet werden.'])
})
test('Senden: zwei Entwürfe gleichzeitig bekommen verschiedene Nummern', async () => {
const [a, b] = [await neuerEntwurf(), await neuerEntwurf()]
const { deps } = baueDeps({ pdfWarten: warte(30) })
const ergebnisse = await Promise.all([sende(a.id, deps), sende(b.id, deps)])
assert.deepEqual(ergebnisse.map((e) => e.auftrag.nummer).sort(), ['AB-2026-0001', 'AB-2026-0002'])
assert.equal(new Set(ergebnisse.map((e) => e.auftrag.pdfDatei)).size, 2)
})
test('Senden: dieselbe AB zweimal gleichzeitig (Doppelklick) – einmal gesendet, das zweite Mal 409, genau eine Nummer und eine Mail', async () => {
const entwurf = await neuerEntwurf()
const { deps, calls } = baueDeps({ pdfWarten: warte(30) })
const ergebnisse = await Promise.allSettled([sende(entwurf.id, deps), sende(entwurf.id, deps)])
assert.deepEqual(ergebnisse.map((r) => r.status).sort(), ['fulfilled', 'rejected'])
const abgewiesen = ergebnisse.find((r) => r.status === 'rejected').reason
assert.deepEqual([abgewiesen.status, abgewiesen.message], [409, MELDUNG.nurEntwurfSenden])
assert.deepEqual([calls.pdf.length, calls.mail.length], [1, 1])
assert.deepEqual((await listAuftraege({ jahr: 2026 })).map((a) => a.nummer), ['AB-2026-0001'])
})
test('Senden hält die Sperre: Ändern und Löschen währenddessen warten und scheitern danach mit 409', async () => {
const entwurf = await neuerEntwurf()
const pdfTor = tor()
const { deps, calls } = baueDeps({ pdfWarten: pdfTor.promise })
const senden = ergebnis(sende(entwurf.id, deps))
await wartebis(() => calls.pdf.length === 1) // das Senden hält die Sperre und wartet auf das PDF
const aendern = ergebnis(aendereEntwurf(entwurf.id, { titel: 'Heimlich geändert' }))
const loeschen = ergebnis(loescheEntwurf(entwurf.id))
await warte(30) // beide sind unterwegs, dürfen aber noch nichts geschrieben haben
assert.equal((await getAuftrag(entwurf.id)).titel, 'Website Beispiel')
pdfTor.freigeben()
const [s, a, l] = await Promise.all([senden, aendern, loeschen])
assert.equal(s.wert.auftrag.status, 'versendet')
assert.deepEqual([a.fehler?.status, a.fehler?.message], [409, MELDUNG.nurEntwurfAendern])
assert.deepEqual([l.fehler?.status, l.fehler?.message], [409, MELDUNG.nurEntwurfLoeschen])
const danach = await getAuftrag(entwurf.id)
assert.deepEqual([danach.titel, danach.status], ['Website Beispiel', 'versendet'])
})
test('Senden: während die Mail unterwegs ist, ist die Sperre schon frei (die nächste AB bekommt ihre Nummer)', async () => {
const [a, b] = [await neuerEntwurf(), await neuerEntwurf()]
const mailTor = tor()
const langsam = baueDeps({ mailWarten: mailTor.promise })
const ersteMail = ergebnis(sende(a.id, langsam.deps))
await wartebis(() => langsam.calls.mail.length === 1)
const zweite = await sende(b.id, baueDeps().deps) // läuft durch, obwohl die Mail der ersten noch hängt
assert.equal(zweite.auftrag.nummer, 'AB-2026-0002')
mailTor.freigeben()
assert.equal((await ersteMail).wert.auftrag.nummer, 'AB-2026-0001')
})