import { test, after, beforeEach, mock } from 'node:test' import assert from 'node:assert/strict' import { starteUmgebung, KUNDE_ID, PROJEKT_ID, TICKET_ID, FREMD, VON, MELDUNG, festpreisEingabe, aufwandEingabe, kundeDoc, baueDeps } from './helpers/auftraegeAb.js' const umg = await starteUmgebung() const { aw, config } = umg const { startTestApp, send } = await import('./support/testApp.js') const { default: router } = await import('../server/routes/admin/auftraege.js') const { getAuftrag } = await import('../server/services/auftraege/auftragStore.js') const { SCHALTER_AUS_MELDUNG } = await import('../server/services/auftraege/konstanten.js') const { legeEntwurfAn, sendeAuftrag, setzeDepsFuerTests } = await import('../server/services/auftraege/auftraege.js') const app = await startTestApp({ '/api/admin/finanzen/auftraege': router }) after(async () => { await app.close() await umg.schliesse() }) let kontext // Attrappen für PDF, Token und Mail des aktuellen Tests beforeEach(async () => { await umg.zuruecksetzen() kontext = baueDeps() setzeDepsFuerTests(kontext.deps) }) const BASIS = '/api/admin/finanzen/auftraege' const api = (methode, pfad, body, jwt = 'jwt-admin') => send(methode, BASIS + pfad, { headers: { Authorization: `Bearer ${jwt}` }, body }) const entwurfAnlegen = (patch) => legeEntwurfAn(festpreisEingabe(patch), { von: VON }) const ZUGRIFF = [ ['GET', `/?kunde=${KUNDE_ID}`], ['POST', '/'], ['GET', '/a_1'], ['PATCH', '/a_1'], ['DELETE', '/a_1'], ['GET', '/a_1/pdf'], ['POST', '/a_1/senden'], ['POST', '/a_1/erinnern'], ['POST', '/a_1/neue-fassung'], ['POST', '/a_1/stornieren'], ['POST', '/a_1/projekt'], ] test('Zugriff: ohne JWT 401, ohne Admin-Label 403 – bei jeder Route', async () => { for (const [methode, pfad] of ZUGRIFF) { const body = ['GET', 'DELETE'].includes(methode) ? undefined : {} assert.equal((await send(methode, BASIS + pfad, { body })).status, 401, `${methode} ${pfad} ohne JWT`) for (const jwt of ['jwt-staff', 'jwt-kunde']) assert.equal((await api(methode, pfad, body, jwt)).status, 403, `${methode} ${pfad} mit ${jwt}`) } }) test('Schalter: ist die Funktion für den Kunden aus, antworten Liste, Anlegen, Lesen, Ändern, Löschen und PDF mit 404 und der festen Meldung', async () => { const a = await entwurfAnlegen() // angelegt, solange der Schalter an ist config.auftraege.mode = 'off' const routen = [['GET', `/?kunde=${KUNDE_ID}`], ['POST', '/', festpreisEingabe()], ['GET', `/${a.id}`], ['PATCH', `/${a.id}`, { titel: 'x' }], ['DELETE', `/${a.id}`], ['GET', `/${a.id}/pdf`]] for (const [methode, pfad, body] of routen) { const r = await api(methode, pfad, body) assert.deepEqual([r.status, r.body], [404, { error: SCHALTER_AUS_MELDUNG }], `${methode} ${pfad}`) } assert.equal((await getAuftrag(a.id)).titel, 'Website Beispiel') // nichts wurde geändert oder gelöscht config.auftraege.mode = 'pilot' config.auftraege.pilotEmails = ['anna@example.invalid'] // E-Mail des Kunden assert.equal((await api('GET', `/${a.id}`)).status, 200) config.auftraege.pilotEmails = ['jemand@example.invalid'] assert.equal((await api('GET', `/${a.id}`)).status, 404) }) test('GET Liste: Aufträge des Kunden, neueste zuerst, ohne interne Felder; 400 ohne Kunde, 404 bei unbekanntem Kunden', async () => { const a = await entwurfAnlegen() const b = await legeEntwurfAn(aufwandEingabe(), { von: VON }) const r = await api('GET', `/?kunde=${KUNDE_ID}`) assert.equal(r.status, 200) assert.deepEqual(r.body.auftraege.map((x) => x.id), [b.id, a.id]) for (const x of r.body.auftraege) { for (const feld of ['tokenHash', 'pdfDatei', 'bestaetigtPdfSha256']) assert.equal(feld in x, false, feld) assert.equal(typeof x.pdfVorhanden, 'boolean') } const ohneKunde = await api('GET', '/') assert.deepEqual([ohneKunde.status, ohneKunde.body], [400, { error: 'Kunde fehlt.' }]) assert.equal((await api('GET', '/?kunde=c_gibtsnicht')).status, 404) assert.equal((await api('GET', '/?kunde=../x')).status, 404) }) test('POST: legt einen Entwurf an (200, AuftragTeam mit Texten); Fehler als { error } mit festen Meldungen', async () => { const r = await api('POST', '/', festpreisEingabe()) assert.equal(r.status, 200) const a = r.body.auftrag assert.deepEqual([a.status, a.titel, a.summeCents, a.erstelltVon], ['entwurf', 'Website Beispiel', 80000, 'kenso@webklar.com']) assert.deepEqual([typeof a.zahlungsplanText, a.pdfVorhanden, 'tokenHash' in a], ['string', false, false]) const ohnePositionen = await api('POST', '/', festpreisEingabe({ positionen: [] })) assert.deepEqual([ohnePositionen.status, ohnePositionen.body], [400, { error: MELDUNG.positionen }]) const ohneKunde = await api('POST', '/', festpreisEingabe({ kundenId: undefined })) assert.deepEqual([ohneKunde.status, ohneKunde.body], [400, { error: 'Kunde: Bitte einen Kunden angeben.' }]) assert.equal((await api('POST', '/', festpreisEingabe({ kundenId: 'c_gibtsnicht' }))).status, 404) }) test('GET, PATCH, DELETE: Entwurf lesen, ändern, löschen; gesendete AB ist unveränderlich (409); ungültige Kennungen 404', async () => { const a = await entwurfAnlegen() const gelesen = await api('GET', `/${a.id}`) assert.deepEqual([gelesen.status, gelesen.body.auftrag.id], [200, a.id]) const geaendert = await api('PATCH', `/${a.id}`, { titel: 'Neu' }) assert.deepEqual([geaendert.status, geaendert.body.auftrag.titel], [200, 'Neu']) const falsch = await api('PATCH', `/${a.id}`, { zahlungszielTage: 99 }) assert.deepEqual([falsch.status, falsch.body], [400, { error: 'Zahlungsziel: Das Zahlungsziel muss eine ganze Zahl von 0 bis 60 Tagen sein.' }]) const geloescht = await api('DELETE', `/${a.id}`) assert.deepEqual([geloescht.status, geloescht.body], [200, { ok: true }]) assert.deepEqual((await api('GET', `/${a.id}`)).body, { error: MELDUNG.auftragFehlt }) for (const kennung of ['a%20b', 'x'.repeat(40), '..%2F..']) assert.equal((await api('GET', `/${kennung}`)).status, 404, kennung) const b = await entwurfAnlegen() await sendeAuftrag(b.id, { von: VON }) const patch = await api('PATCH', `/${b.id}`, { titel: 'Nachträglich' }) assert.deepEqual([patch.status, patch.body], [409, { error: MELDUNG.nurEntwurfAendern }]) const loeschen = await api('DELETE', `/${b.id}`) assert.deepEqual([loeschen.status, loeschen.body], [409, { error: MELDUNG.nurEntwurfLoeschen }]) }) test('GET pdf: Entwurf als Vorschau, gesendete AB aus der gespeicherten Datei, mit Kopfzeilen', async () => { const entwurf = await entwurfAnlegen() const vorschau = await api('GET', `/${entwurf.id}/pdf`) assert.equal(vorschau.status, 200) assert.equal(vorschau.body, '%PDF-1.4 Entwurf') assert.match(vorschau.headers.get('content-type'), /^application\/pdf/) assert.equal(vorschau.headers.get('content-disposition'), 'inline; filename="Auftragsbestaetigung-Entwurf.pdf"') assert.equal(vorschau.headers.get('cache-control'), 'no-store') const { auftrag } = await sendeAuftrag(entwurf.id, { von: VON }) // Attrappen aus setzeDepsFuerTests const gespeichert = await api('GET', `/${entwurf.id}/pdf`) assert.equal(gespeichert.body, `%PDF-1.4 ${auftrag.nummer}`) assert.equal(gespeichert.headers.get('content-disposition'), `inline; filename="${auftrag.nummer}.pdf"`) assert.equal((await api('GET', '/a_gibtsnicht/pdf')).status, 404) }) test('Unerwartete Fehler: 500 mit fester Meldung, nichts von Appwrite gelangt an die Seite', async () => { mock.method(console, 'error', () => {}) const a = await entwurfAnlegen() aw.failNext({ collection: config.collections.auftraege, method: 'GET', status: 500, message: 'GEHEIM-Appwrite-Detail' }) const r = await api('GET', `/${a.id}`) assert.deepEqual([r.status, r.body], [500, { error: 'Interner Fehler.' }]) }) test('POST senden: 200 ohne Token und Dateipfad; Kundendaten-, PDF- und Statusfehler mit festen Meldungen; Mail-Fehler als mailFehler: true', async () => { mock.method(console, 'error', () => {}) const a = await entwurfAnlegen() aw.seed(config.collections.customers, kundeDoc({ email: '' })) let r = await api('POST', `/${a.id}/senden`) assert.deepEqual([r.status, r.body], [400, { error: MELDUNG.keineMail }]) aw.seed(config.collections.customers, kundeDoc({ street: '' })) r = await api('POST', `/${a.id}/senden`) assert.deepEqual([r.status, r.body], [409, { error: MELDUNG.adresse }]) aw.seed(config.collections.customers, kundeDoc()) kontext = baueDeps({ pdfFehler: new Error('pdfkit') }) setzeDepsFuerTests(kontext.deps) r = await api('POST', `/${a.id}/senden`) assert.deepEqual([r.status, r.body], [500, { error: MELDUNG.pdf }]) kontext = baueDeps({ mailFehler: new Error('smtp') }) setzeDepsFuerTests(kontext.deps) r = await api('POST', `/${a.id}/senden`) assert.equal(r.status, 200) assert.equal(r.body.mailFehler, true) assert.deepEqual([r.body.auftrag.status, r.body.auftrag.mailStatus, r.body.auftrag.pdfVorhanden], ['versendet', 'fehler', true]) assert.match(r.body.auftrag.nummer, /^AB-\d{4}-0001$/) for (const feld of ['tokenHash', 'pdfDatei', 'bestaetigtPdfSha256']) assert.equal(feld in r.body.auftrag, false, feld) r = await api('POST', `/${a.id}/senden`) assert.deepEqual([r.status, r.body], [409, { error: MELDUNG.nurEntwurfSenden }]) }) test('POST erinnern, neue-fassung, stornieren: Antworten, Folgezustände und 409 bei falschem Status', async () => { const a = await entwurfAnlegen() let r = await api('POST', `/${a.id}/erinnern`) assert.deepEqual([r.status, r.body], [409, { error: MELDUNG.keineErinnerung }]) r = await api('POST', `/${a.id}/senden`) assert.deepEqual([r.status, r.body.mailFehler, r.body.auftrag.status], [200, false, 'versendet']) r = await api('POST', `/${a.id}/erinnern`) assert.deepEqual([r.status, r.body.mailFehler, r.body.auftrag.status], [200, false, 'versendet']) assert.equal(kontext.calls.mail.at(-1).erinnerung, true) r = await api('POST', `/${a.id}/neue-fassung`) assert.deepEqual([r.status, r.body.auftrag.status, r.body.auftrag.ersetztId], [200, 'entwurf', a.id]) r = await api('POST', `/${a.id}/neue-fassung`) assert.deepEqual([r.status, r.body], [409, { error: MELDUNG.schonEntwurf }]) r = await api('POST', `/${a.id}/stornieren`) assert.deepEqual([r.status, r.body.auftrag.status], [200, 'storniert']) r = await api('POST', `/${a.id}/stornieren`) assert.deepEqual([r.status, r.body], [409, { error: MELDUNG.keinStorno }]) }) test('POST projekt: ordnet einer AB ohne Projekt das Projekt zu (200, AuftragTeam); 400 ohne oder mit fremdem Projekt, 404 bei unbekannter AB, 409 bei vorhandenem Projekt oder falschem Status', async () => { const a = await entwurfAnlegen({ projektId: '', titel: 'Beratung ohne Projekt' }) await api('POST', `/${a.id}/senden`) let r = await api('POST', `/${a.id}/projekt`, {}) assert.deepEqual([r.status, r.body], [400, { error: MELDUNG.projektFehlt }]) r = await api('POST', `/${a.id}/projekt`) assert.deepEqual([r.status, r.body], [400, { error: MELDUNG.projektFehlt }]) r = await api('POST', `/${a.id}/projekt`, { projektId: FREMD.projekt }) assert.deepEqual([r.status, r.body], [400, { error: MELDUNG.projektFremd }]) r = await api('POST', '/a_gibtsnicht/projekt', { projektId: PROJEKT_ID }) assert.deepEqual([r.status, r.body], [404, { error: MELDUNG.auftragFehlt }]) assert.equal((await getAuftrag(a.id)).projektId, '') // die abgelehnten Aufrufe haben nichts geändert r = await api('POST', `/${a.id}/projekt`, { projektId: PROJEKT_ID }) assert.equal(r.status, 200) assert.deepEqual([r.body.auftrag.projektId, r.body.auftrag.zeitquelle, r.body.auftrag.status], [PROJEKT_ID, [TICKET_ID], 'versendet']) for (const feld of ['tokenHash', 'pdfDatei', 'bestaetigtPdfSha256']) assert.equal(feld in r.body.auftrag, false, feld) r = await api('POST', `/${a.id}/projekt`, { projektId: PROJEKT_ID }) assert.deepEqual([r.status, r.body], [409, { error: MELDUNG.schonProjekt }]) const b = await entwurfAnlegen({ projektId: '', titel: 'Zweite Beratung ohne Projekt' }) await api('POST', `/${b.id}/senden`) await api('POST', `/${b.id}/stornieren`) r = await api('POST', `/${b.id}/projekt`, { projektId: PROJEKT_ID }) assert.deepEqual([r.status, r.body], [409, { error: MELDUNG.keinProjekt }]) }) test('Schalter aus: auch senden, erinnern, neue-fassung, stornieren und projekt antworten 404 mit der festen Meldung', async () => { const a = await entwurfAnlegen({ projektId: '', titel: 'Beratung ohne Projekt' }) config.auftraege.mode = 'off' for (const aktion of ['senden', 'erinnern', 'neue-fassung', 'stornieren', 'projekt']) { const r = await api('POST', `/${a.id}/${aktion}`, aktion === 'projekt' ? { projektId: PROJEKT_ID } : undefined) assert.deepEqual([r.status, r.body], [404, { error: SCHALTER_AUS_MELDUNG }], aktion) } const danach = await getAuftrag(a.id) assert.deepEqual([danach.status, danach.projektId], ['entwurf', '']) assert.equal(kontext.calls.pdf.length + kontext.calls.mail.length, 0) })