173 lines
8.8 KiB
JavaScript
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: {} })
|
|
})
|