Files
Webklar-Kundenbereich/test/auftraegeRoutesProjektkonten.test.js

173 lines
8.8 KiB
JavaScript

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=<unbekannt> -> 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: {} })
})