diff --git a/test/auftraegeKundenansicht.test.js b/test/auftraegeKundenansicht.test.js new file mode 100644 index 0000000..befcf90 --- /dev/null +++ b/test/auftraegeKundenansicht.test.js @@ -0,0 +1,89 @@ +import { test, after, beforeEach } from 'node:test' +import assert from 'node:assert/strict' +import os from 'node:os' +import path from 'node:path' +import { mkdtempSync } from 'node:fs' +import { startAppwriteMock } from './support/appwriteMock.js' +import { useTestEnv } from './support/testEnv.js' +import { mailSink } from './support/mailSink.js' +import { AUFTRAEGE_DATETIMES, AUFTRAEGE_SCHEMA, TOKEN, abDokument } from './auftraegeBestaetigungHilfe.js' + +// Kundenansicht (Mitarbeiter im Portal eines Kunden): Bestätigen ist eine Erklärung im Namen des Kunden und bleibt gesperrt +process.env.AUFTRAEGE_UPLOAD_DIR = mkdtempSync(path.join(os.tmpdir(), 'auftraege-kundenansicht-')) +process.env.AUFTRAEGE_ENABLED = 'on' +const aw = await startAppwriteMock({ schema: AUFTRAEGE_SCHEMA, datetimes: AUFTRAEGE_DATETIMES, defaultLimit: 25 }) +useTestEnv({ appwriteUrl: aw.url, stripeUrl: 'http://127.0.0.1:1' }) +const { startTestApp, send } = await import('./support/testApp.js') +const { seedFixtures, customerCookie, kundenansichtCookie } = await import('./support/fixtures.js') +const { auftragToDoc, getAuftrag } = await import('../server/services/auftraege/auftragStore.js') +const { speicherePdf } = await import('../server/services/auftraege/ablage.js') +const { kundenansichtGuard, KUNDENANSICHT_ALLOWED } = await import('../server/middleware/kundenansichtGuard.js') +const { setMailTransportForTests } = await import('../server/services/mailer.js') +const { default: kundeRoutes } = await import('../server/routes/auftraegeKunde.js') +const { default: tokenRoutes } = await import('../server/routes/auftrag.js') +// Mount-Pfade und Reihenfolge wie in server/index.js: der Wächter steht vor den Routen +const app = await startTestApp({ '/': kundenansichtGuard, '/api/auftraege': kundeRoutes, '/api/auftrag': tokenRoutes }) +after(() => { + setMailTransportForTests(null) + return Promise.all([app.close(), aw.close()]) +}) +seedFixtures(aw) +await speicherePdf('c1', 'ab1', Buffer.from('%PDF-1.4 Test')) + +const VIEW = 'a'.repeat(32) +const kv = () => ({ cookie: kundenansichtCookie({ customerId: 'c1', viewId: VIEW }), headers: { 'x-kundenansicht': VIEW } }) +let sink +beforeEach(() => { + sink = mailSink() + setMailTransportForTests(sink) + aw.seed('auftraege', { $id: 'ab1', ...auftragToDoc(abDokument()) }) +}) +const unveraendert = async () => { + const doc = await getAuftrag('ab1') + assert.deepEqual([doc.status, doc.bestaetigtAm, sink.sent.length], ['versendet', null, 0]) +} + +test('kein Eintrag der Erlaubnisliste gilt für Aufträge', () => { + for (const pfad of ['/api/auftraege', '/api/auftraege/ab1/bestaetigen', '/api/auftraege/ab1/pdf', `/api/auftrag/${TOKEN}`, `/api/auftrag/${TOKEN}/bestaetigen`]) { + assert.ok(!KUNDENANSICHT_ALLOWED.some(([, muster]) => muster.test(pfad)), pfad) + } +}) + +test('POST /api/auftraege/:id/bestaetigen in der Kundenansicht: 403 view_blocked, die AB bleibt unverändert', async () => { + const res = await send('POST', '/api/auftraege/ab1/bestaetigen', { ...kv(), body: { bestaetigt: true, fruehbeginn: true } }) + assert.equal(res.status, 403) + assert.equal(res.body.code, 'view_blocked') + await unveraendert() +}) + +test('alle Schreib-Routen des Kunden-Routers sind in der Kundenansicht gesperrt (nur Bestätigen ist eine)', async () => { + const schreibRouten = kundeRoutes.stack + .filter((schicht) => schicht.route) + .flatMap((schicht) => Object.keys(schicht.route.methods).filter((m) => !['get', 'head', 'options', '_all'].includes(m)).map((m) => [m.toUpperCase(), `/api/auftraege${String(schicht.route.path).replace(/:[A-Za-z]+/g, 'ab1')}`])) + assert.deepEqual(schreibRouten, [['POST', '/api/auftraege/ab1/bestaetigen']]) + for (const [methode, pfad] of schreibRouten) { + const res = await send(methode, pfad, { ...kv(), body: { bestaetigt: true } }) + assert.equal(res.status, 403, `${methode} ${pfad}`) + } + await unveraendert() +}) + +test('auch der öffentliche Link bestätigt nichts, wenn der Browser eine Kundenansicht-Sitzung trägt', async () => { + const mitKennung = await send('POST', `/api/auftrag/${TOKEN}/bestaetigen`, { ...kv(), body: { bestaetigt: true } }) + assert.deepEqual([mitKennung.status, mitKennung.body.code], [403, 'view_blocked']) + const ohneKennung = await send('POST', `/api/auftrag/${TOKEN}/bestaetigen`, { cookie: kv().cookie, body: { bestaetigt: true } }) + assert.deepEqual([ohneKennung.status, ohneKennung.body.code], [409, 'view_replaced']) + await unveraendert() +}) + +test('Lesen bleibt erlaubt, kannBestaetigen ist in der Kundenansicht false; der Kunde selbst darf bestätigen', async () => { + const liste = await send('GET', '/api/auftraege', kv()) + assert.equal(liste.status, 200) + assert.deepEqual(liste.body.auftraege.map((a) => [a.id, a.kannBestaetigen]), [['ab1', false]]) + assert.equal((await send('GET', '/api/auftraege/ab1/pdf', kv())).status, 200) + const selbst = await send('GET', '/api/auftraege', { cookie: customerCookie('c1') }) + assert.deepEqual(selbst.body.auftraege.map((a) => [a.id, a.kannBestaetigen]), [['ab1', true]]) + const res = await send('POST', '/api/auftraege/ab1/bestaetigen', { cookie: customerCookie('c1'), body: { bestaetigt: true } }) + assert.equal(res.status, 200) + assert.equal((await getAuftrag('ab1')).status, 'bestaetigt') +})