Files
Webklar-Kundenbereich/test/teamAdminRoutes.test.js
T

134 lines
7.3 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { test, after } from 'node:test'
import assert from 'node:assert/strict'
import os from 'node:os'
import path from 'node:path'
import { mkdtempSync, mkdirSync, writeFileSync } from 'node:fs'
import { startAppwriteMock } from './support/appwriteMock.js'
import { startFakeStripe } from './support/fakeStripe.js'
import { useTestEnv } from './support/testEnv.js'
import { schemaKeys } from './support/schema.js'
import { mailSink } from './support/mailSink.js'
// Etappe 3: Gutschriften, Abo-Angebote, E-Mail-Speicher und Funktionen ueber den Team-Eingang (/api/team, JWT)
process.env.TAKEOVER_UPLOAD_DIR = mkdtempSync(path.join(os.tmpdir(), 'team-takeover-'))
const aw = await startAppwriteMock({
schema: {
subscriptions: schemaKeys('subscriptions'),
contractEvents: schemaKeys('contractEvents'),
takeoverCredits: schemaKeys('takeoverCredits'),
aboOffers: schemaKeys('aboOffers'),
portalFeatures: schemaKeys('portalFeatures'),
},
})
const st = await startFakeStripe()
useTestEnv({ appwriteUrl: aw.url, stripeUrl: st.url })
const { startTestApp, rawGet, post } = await import('./support/testApp.js')
const { seedFixtures, consent } = await import('./support/fixtures.js')
const { setMailTransportForTests } = await import('../server/services/mailer.js')
const { recordContractEvent } = await import('../server/services/contractEvents.js')
const { default: teamRoutes } = await import('../server/routes/team/index.js')
const app = await startTestApp({ '/api/team': teamRoutes })
setMailTransportForTests(mailSink())
after(() => {
setMailTransportForTests(null)
return Promise.all([app.close(), aw.close(), st.close()])
})
seedFixtures(aw)
aw.seed('employees', { $id: 'e_staff2', userId: 'u_staff2', displayName: 'Andrej', email: 'andrej@webklar.com' })
const credit = (id, customerId, status) =>
aw.seed('takeoverCredits', {
$id: id, customerId, email: 'k@example.invalid', services: JSON.stringify(['hosting']), provider: 'IONOS', domain: 'kunde.de',
invoiceAmountCents: 12000, billingMonths: 12, paidUntil: '2026-12-31T00:00:00.000Z', message: 'Bitte verrechnen',
files: '[]', status, history: '[]', takeoverFrom: '2026-10-01T00:00:00.000Z',
})
credit('tc1', 'c1', 'offen')
credit('tc2', 'c1', 'abgelehnt')
credit('tc3', 'c2', 'offen')
await recordContractEvent({
id: 'speicher-c2', customerId: 'c2', subscriptionId: 'sub_h2', type: 'speicher', productKey: 'hosting',
details: { kind: 'einrichten', fromUnits: 0, toUnits: 2, effectiveAt: new Date().toISOString(), totalGb: 15, monthlyCents: 200 },
consent, status: 'offen', dueAt: new Date().toISOString(),
})
aw.seed('websiteProjects', { $id: 'pa1', customerId: 'c1', projectName: 'Team-Test' })
const staff = { headers: { authorization: 'Bearer jwt-staff' } }
test('alle vier Bereiche mit jwt-staff erreichbar (kein Admin-Label noetig)', async () => {
for (const p of ['/api/team/takeover-credits', '/api/team/offers', '/api/team/storage-tasks', '/api/team/funktionen/c1']) {
assert.equal((await rawGet(p, staff)).status, 200, p)
}
})
test('Kundenfilter: Gutschriften nur des Kunden, openCount weiter ueber alle', async () => {
const all = (await rawGet('/api/team/takeover-credits?status=all&customerId=c1', staff)).body
assert.deepEqual(all.requests.map((r) => r.id).sort(), ['tc1', 'tc2'])
assert.equal(all.openCount, 2)
const open = (await rawGet('/api/team/takeover-credits?customerId=c2', staff)).body
assert.deepEqual(open.requests.map((r) => r.id), ['tc3'])
})
test('Angebot anlegen als Mitarbeiter, Kundenfilter der Angebotsliste', async () => {
const res = await post('/api/team/offers', { customerId: 'c2', productKey: 'wartung', monthlyPriceCents: 2500, months: 2, notifyCustomer: false }, staff)
assert.equal(res.status, 201, JSON.stringify(res.body))
assert.equal(res.body.offer.createdBy, 'Andrej <andrej@webklar.com>')
assert.deepEqual((await rawGet('/api/team/offers?customerId=c2', staff)).body.offers.map((o) => o.customerId), ['c2'])
assert.deepEqual((await rawGet('/api/team/offers?customerId=c1', staff)).body.offers, [])
})
test('E-Mail-Speicher open-count ohne Stripe-Abgleich', async () => {
const before = st.calls.length
assert.deepEqual((await rawGet('/api/team/storage-tasks/open-count', staff)).body, { open: 1 })
assert.equal(st.calls.length, before)
})
test('Mitarbeiter aus dem JWT: Rueckfrage, Erledigt und Funktionen-Notiz tragen Andrej', async () => {
const q = await post('/api/team/takeover-credits/tc3/decision', { action: 'question', note: 'Bitte Rechnung schicken' }, staff)
assert.equal(q.status, 200, JSON.stringify(q.body))
const detail = (await rawGet('/api/team/takeover-credits/tc3', staff)).body
assert.equal(detail.request.history.at(-1).by, 'Andrej <andrej@webklar.com>')
const done = await post('/api/team/storage-tasks/speicher-c2/done', {}, staff)
assert.equal(done.status, 200, JSON.stringify(done.body))
assert.equal(done.body.task.doneBy, 'andrej@webklar.com')
const news = await post('/api/team/funktionen/c1/news', { projectId: 'pa1', action: 'activate', note: 'Pilot' }, staff)
assert.equal(news.status, 200)
assert.match(news.body.projects[0].features.news.adminNote, /Andrej: activate – Pilot$/)
})
test('ohne JWT 401 fuer alle neuen Pfade', async () => {
for (const p of ['/api/team/takeover-credits', '/api/team/takeover-credits/open-count', '/api/team/offers', '/api/team/storage-tasks', '/api/team/storage-tasks/open-count', '/api/team/funktionen/c1']) {
assert.equal((await rawGet(p)).status, 401, p)
}
})
// Task 2: Nachweise per signiertem Link (15 min) – Links im Ticketsystem tragen keinen Authorization-Header
const FILE_ID = '0b9c2f0e-6d6e-4c1a-9a51-6a3a9d1b2c3d'
aw.seed('takeoverCredits', {
// eigener Kunde und entschieden, damit Filter und openCount der anderen Tests gleich bleiben
$id: 'tc9', customerId: 'c4', email: 'k@example.invalid', services: JSON.stringify(['hosting']), provider: 'IONOS', domain: 'kunde.de',
invoiceAmountCents: 12000, billingMonths: 12, paidUntil: '2026-12-31T00:00:00.000Z', message: 'x', status: 'abgelehnt', history: '[]',
takeoverFrom: '2026-10-01T00:00:00.000Z',
files: JSON.stringify([{ id: FILE_ID, name: 'rechnung.pdf', type: 'application/pdf', ext: 'pdf', size: 9, uploadedAt: '2026-10-01T08:00:00.000Z' }]),
})
mkdirSync(path.join(process.env.TAKEOVER_UPLOAD_DIR, 'tc9'), { recursive: true })
writeFileSync(path.join(process.env.TAKEOVER_UPLOAD_DIR, 'tc9', `${FILE_ID}.pdf`), '%PDF-1.4\n')
test('Nachweis: signierter Link ohne JWT 200 mit Inhalt, fremder Antrag/abgelaufen 403, ohne sig 401', async (t) => {
const detail = (await rawGet('/api/team/takeover-credits/tc9', staff)).body
const url = detail.request.files[0].url
assert.match(url, new RegExp(`^https://project\\.webklar\\.com/api/team/takeover-credits/tc9/files/${FILE_ID}\\?exp=\\d+&sig=[0-9a-f]{64}$`))
const u = new URL(url)
const rel = u.pathname + u.search
const ok = await fetch(app.url + rel)
assert.equal(ok.status, 200)
assert.equal(ok.headers.get('content-type'), 'application/pdf')
assert.equal(await ok.text(), '%PDF-1.4\n')
assert.equal((await rawGet(rel.replace('/tc9/', '/tc1/'))).status, 403)
assert.equal((await rawGet(u.pathname)).status, 401)
const now = Date.now()
t.mock.method(Date, 'now', () => now + 16 * 60 * 1000)
const expired = await rawGet(rel)
assert.equal(expired.status, 403)
assert.equal(expired.body.error, 'Link abgelaufen. Bitte den Antrag neu öffnen.')
})