151 lines
8.4 KiB
JavaScript
151 lines
8.4 KiB
JavaScript
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.')
|
||
})
|
||
|
||
// Abschlussreview: zwei Mitarbeiter entscheiden im selben Moment -> genau eine Entscheidung, eine Kunden-Mail
|
||
test('gleichzeitige Entscheidungen: eine gewinnt, die andere bekommt 409', async () => {
|
||
credit('tc7', 'c2', 'offen')
|
||
const sink = mailSink()
|
||
setMailTransportForTests(sink)
|
||
const checks = { recipient: true, service: true, period: true, replaced: true }
|
||
const figures = { invoiceAmountCents: 12000, billingMonths: 12, paidUntil: '2026-12-31', takeoverFrom: '2026-10-01' }
|
||
const [a, b] = await Promise.all([
|
||
post('/api/team/takeover-credits/tc7/decision', { action: 'approve', amountCents: 5000, checks, note: '', ...figures }, staff),
|
||
post('/api/team/takeover-credits/tc7/decision', { action: 'reject', note: 'Leider nicht moeglich' }, staff),
|
||
])
|
||
assert.deepEqual([a.status, b.status].sort(), [200, 409], JSON.stringify([a.body, b.body]))
|
||
const loser = a.status === 409 ? a : b
|
||
assert.equal(loser.body.error, 'Über diesen Antrag wurde bereits entschieden.')
|
||
assert.equal(sink.sent.length, 1, 'genau eine Kunden-Mail')
|
||
})
|