import { test, after, beforeEach, mock } from 'node:test' import assert from 'node:assert/strict' import { startAppwriteMock } from './support/appwriteMock.js' import { startFakeStripe } from './support/fakeStripe.js' import { schemaKeys } from './support/schema.js' import { useTestEnv } from './support/testEnv.js' import { AUFTRAEGE_DATETIMES } from '../server/services/auftraege/schema.js' // Routen des Projektkontos (Strang L1): Admin (requireAppwriteStaff) und Team (requireStaffJwt). Muster: financeRoutesBank.test.js // (JWT-Köpfe des Appwrite-Nachbaus), shopTeam.test.js (Team-Eingang) und den Gerüsttest von L0 (auftraegeGeruest.test.js). const aw = await startAppwriteMock({ schema: { auftraege: schemaKeys('auftraege'), projektRechnungen: schemaKeys('projektRechnungen') }, datetimes: AUFTRAEGE_DATETIMES, defaultLimit: 25, }) const st = await startFakeStripe() useTestEnv({ appwriteUrl: aw.url, stripeUrl: st.url }) const { config } = await import('../server/config.js') const { seedFixtures } = await import('./support/fixtures.js') const { startTestApp, rawGet } = await import('./support/testApp.js') const { default: projektkontenRoutes } = await import('../server/routes/admin/projektkonten.js') const { default: teamRoutes } = await import('../server/routes/team/index.js') const { deleteDocument } = await import('../server/services/appwriteAdmin.js') const { invalidateFinanceCache } = await import('../server/services/finance/stripeFinance.js') const app = await startTestApp({ '/api/admin/finanzen/projektkonten': projektkontenRoutes, '/api/team': teamRoutes }) after(() => Promise.all([app.close(), aw.close(), st.close()])) seedFixtures(aw) const admin = { headers: { Authorization: 'Bearer jwt-admin' } } const staff = { headers: { Authorization: 'Bearer jwt-staff' } } const ohneLabel = { headers: { Authorization: 'Bearer jwt-kunde' } } const BASIS = '/api/admin/finanzen/projektkonten' const COLLECTIONS = ['websiteProjects', 'workorders', 'worksheets', 'auftraege', 'projektRechnungen'] async function reset() { mock.restoreAll() invalidateFinanceCache() st.calls.length = 0 st.invoices.clear() config.auftraege.mode = 'on' config.auftraege.pilotEmails = [] for (const name of COLLECTIONS) { for (const id of aw.docs(config.collections[name]).keys()) await deleteDocument(config.collections[name], id) } } beforeEach(reset) const blatt = (id, minuten) => ({ $id: id, wsid: `10000${id.slice(1)}`, woid: '100001', workorderId: 't1', employeeId: 'e1', employeeName: 'Kenso Grimm', employeeShort: 'KNSO', serviceType: 'Remote', totalTime: minuten, supporters: '', startDate: '06.10.2026', endDate: '06.10.2026', details: 'Startseite', isComment: false, countsToPlan: false, }) function seedBasis() { aw.seed(config.collections.workorders, { $id: 't1', woid: '100001', customerId: 'c1', topic: 'Website', status: 'open' }) aw.seed(config.collections.websiteProjects, { $id: 'p1', customerId: 'c1', projectName: 'Website Müller', ticketId: 't1', kind: 'website', archived: false }) aw.seed(config.collections.worksheets, blatt('w1', 60)) aw.seed(config.collections.worksheets, blatt('w2', 30)) } test('Projektkonto-Routen: ohne JWT 401, ohne Admin-Label 403', async () => { for (const path of [`${BASIS}?kunde=c1`, `${BASIS}/p1`]) { assert.equal((await rawGet(path)).status, 401, `${path} ohne JWT`) assert.equal((await rawGet(path, { headers: { Authorization: 'Bearer jwt-unbekannt' } })).status, 401, `${path} mit falschem JWT`) assert.equal((await rawGet(path, staff)).status, 403, `${path} als Mitarbeiter ohne Admin-Label`) assert.equal((await rawGet(path, ohneLabel)).status, 403, `${path} als Konto ohne Label`) } }) test('GET ?kunde=: ohne, leer, doppelt oder mit unmöglicher Kennung -> 400', async () => { for (const query of ['', '?kunde=', '?kunde=c1&kunde=c2', '?kunde=../x', `?kunde=${'c'.repeat(37)}`, '?kunde[a]=b']) { const res = await rawGet(`${BASIS}${query}`, admin) assert.equal(res.status, 400, query) assert.deepEqual(res.body, { error: 'Kunde fehlt oder ist ungültig.' }, query) } }) test('GET ?kunde= -> 404', async () => { const res = await rawGet(`${BASIS}?kunde=gibtsnicht`, admin) assert.equal(res.status, 404) assert.deepEqual(res.body, { error: 'Kunde nicht gefunden.' }) }) test('GET ?kunde=c1: Projektkonten mit Zeit, Antwort mit genau den vereinbarten Schlüsseln', async () => { seedBasis() const res = await rawGet(`${BASIS}?kunde=c1`, admin) assert.equal(res.status, 200) assert.deepEqual(Object.keys(res.body).sort(), ['auftraegeAktiv', 'konten', 'kunde', 'ohneProjekt', 'stripeOk']) assert.deepEqual(res.body.kunde, { id: 'c1', name: 'Kunde Eins', kundenart: '' }) assert.deepEqual(res.body.ohneProjekt, { berechnetCents: 0, bezahltCents: 0, anzahl: 0 }) assert.equal(res.body.stripeOk, true) assert.equal(res.body.auftraegeAktiv, true) assert.deepEqual(res.body.konten.map((konto) => [konto.projektId, konto.name, konto.projektTyp, konto.zeit.minuten]), [['p1', 'Website Müller', 'Website', 90]]) assert.deepEqual(res.body.konten[0].tickets, [{ id: 't1', woid: '100001' }]) }) test('GET /:projektId: Konto, ABs, Rechnungen und offene Arbeitsblätter; unbekannt und unmöglich -> 404', async () => { seedBasis() const res = await rawGet(`${BASIS}/p1`, admin) assert.equal(res.status, 200) assert.deepEqual(Object.keys(res.body).sort(), ['auftraege', 'konto', 'offeneArbeitsblaetter', 'rechnungen', 'stripeOk']) assert.equal(res.body.konto.projektId, 'p1') assert.deepEqual(res.body.offeneArbeitsblaetter.map((eintrag) => eintrag.id), ['w1', 'w2']) for (const id of ['gibtsnicht', '%20x']) { const fehler = await rawGet(`${BASIS}/${id}`, admin) assert.equal(fehler.status, 404, id) assert.deepEqual(fehler.body, { error: 'Projekt nicht gefunden.' }, id) } }) test('Stripe-Ausfall: beide Routen antworten 200 mit stripeOk false', async () => { mock.method(console, 'warn', () => {}) seedBasis() st.failNext({ method: 'GET', path: '/v1/invoices', status: 400, message: 'kaputt' }) const liste = await rawGet(`${BASIS}?kunde=c1`, admin) assert.equal(liste.status, 200) assert.equal(liste.body.stripeOk, false) assert.equal(liste.body.konten[0].zeit.minuten, 90) st.failNext({ method: 'GET', path: '/v1/invoices', status: 400, message: 'kaputt' }) const detail = await rawGet(`${BASIS}/p1`, admin) assert.equal(detail.status, 200) assert.equal(detail.body.stripeOk, false) }) test('Fehler in Appwrite: 500 mit fester Meldung, ohne Text von Appwrite', async () => { mock.method(console, 'error', () => {}) seedBasis() aw.failNext({ collection: config.collections.websiteProjects, method: 'GET', status: 500, message: 'geheim-appwrite' }) const res = await rawGet(`${BASIS}?kunde=c1`, admin) assert.equal(res.status, 500) assert.deepEqual(res.body, { error: 'Interner Fehler.' }) }) // ---------- Team-Route (Task 8) ---------- const ABGERECHNET = '/api/team/arbeitsblaetter/abgerechnet' const seedPr = (invoiceId, { arbeitsblattIds = [], ...over } = {}) => aw.seed(config.collections.projektRechnungen, { $id: invoiceId, invoiceId, kundenId: 'c1', projektId: 'p1', auftragId: '', art: 'festbetrag', betragCents: 0, arbeitsblattIdsJson: JSON.stringify(arbeitsblattIds), rechnungsnummer: '', aktiv: true, erstelltVon: 'kenso@webklar.com', erstelltAm: '2026-10-01T10:00:00.000Z', ...over, }) test('Abzeichen-Route: ohne JWT 401, Konto ohne Label 403, Mitarbeiter und Admin dürfen', async () => { const path = `${ABGERECHNET}?ticketId=t1` assert.equal((await rawGet(path)).status, 401) assert.equal((await rawGet(path, ohneLabel)).status, 403) assert.equal((await rawGet(path, staff)).status, 200) assert.equal((await rawGet(path, admin)).status, 200) }) test('Abzeichen-Route: ticketId fehlt, ist leer, doppelt oder unmöglich -> 400', async () => { for (const query of ['', '?ticketId=', '?ticketId=t1&ticketId=t2', '?ticketId=../x', `?ticketId=${'t'.repeat(37)}`, '?ticketId[a]=b']) { const res = await rawGet(`${ABGERECHNET}${query}`, staff) assert.equal(res.status, 400, query) assert.deepEqual(res.body, { error: 'ticketId fehlt oder ist ungültig.' }, query) } }) test('Abzeichen-Route: { abgerechnet: { Blatt: Rechnungsnummer | Entwurf } } ohne Beträge, unbekanntes Ticket leer', async () => { seedBasis() seedPr('in_1', { rechnungsnummer: 'WEBKLAR-0007', arbeitsblattIds: ['w1'] }) seedPr('in_2', { rechnungsnummer: '', arbeitsblattIds: ['w2'] }) const res = await rawGet(`${ABGERECHNET}?ticketId=t1`, staff) assert.equal(res.status, 200) assert.deepEqual(res.body, { abgerechnet: { w1: 'WEBKLAR-0007', w2: 'Entwurf' } }) assert.ok(!/cents|betrag|in_1|in_2/i.test(JSON.stringify(res.body)), 'weder Beträge noch Rechnungs-IDs') const unbekannt = await rawGet(`${ABGERECHNET}?ticketId=gibtsnicht`, staff) assert.equal(unbekannt.status, 200) assert.deepEqual(unbekannt.body, { abgerechnet: {} }) })