import { test, after } from 'node:test' import assert from 'node:assert/strict' import os from 'node:os' import path from 'node:path' import { mkdtempSync } from 'node:fs' import { startAppwriteMock } from './support/appwriteMock.js' import { startFakeStripe } from './support/fakeStripe.js' import { useTestEnv } from './support/testEnv.js' import { mailSink } from './support/mailSink.js' // Chat ueber den Team-Eingang (/api/team, JWT) mit signierten Anhang-Links; Admin-Panel (/api/portal-admin) unveraendert process.env.CHAT_UPLOAD_DIR = mkdtempSync(path.join(os.tmpdir(), 'team-chat-')) const aw = await startAppwriteMock() const st = await startFakeStripe() useTestEnv({ appwriteUrl: aw.url, stripeUrl: st.url }) const { startTestApp, rawGet, post } = await import('./support/testApp.js') const { seedFixtures, adminCookie } = await import('./support/fixtures.js') const { setMailTransportForTests } = await import('../server/services/mailer.js') const { default: teamRoutes } = await import('../server/routes/team/index.js') const { default: portalAdminRoutes } = await import('../server/routes/portalAdmin.js') const app = await startTestApp({ '/api/team': teamRoutes, '/api/portal-admin': portalAdminRoutes }) const sink = mailSink() setMailTransportForTests(sink) after(() => 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' }) aw.seed('portalMessages', { $id: 'm1', customerId: 'c1', sender: 'customer', senderName: 'Kunde Eins', text: 'Frage 1', readByAdmin: false, readByCustomer: true, createdAt: '2026-10-01T08:00:00.000Z' }) aw.seed('portalMessages', { $id: 'm2', customerId: 'c1', sender: 'customer', senderName: 'Kunde Eins', text: 'Frage 2', readByAdmin: false, readByCustomer: true, createdAt: '2026-10-01T08:01:00.000Z' }) aw.seed('portalMessages', { $id: 'm3', customerId: 'c2', sender: 'admin', senderName: 'Kenso', text: 'Hallo c2', readByAdmin: true, readByCustomer: false, createdAt: '2026-10-01T09:00:00.000Z' }) const staff = { headers: { authorization: 'Bearer jwt-staff' } } const upload = (customerId, body, headers = {}) => fetch(`${app.url}/api/team/chats/${customerId}/attachments`, { method: 'POST', headers: { authorization: 'Bearer jwt-staff', 'content-type': 'application/octet-stream', 'x-file-name': 'notiz.txt', ...headers }, body, }).then(async (res) => ({ status: res.status, body: await res.json() })) const URL_RE = /^https:\/\/project\.webklar\.com\/api\/team\/chats\/c1\/attachments\/([^?]+)\?exp=(\d+)&sig=([0-9a-f]{64})$/ test('Liste und ungelesene Zahl ueber den Team-Eingang', async () => { const list = await rawGet('/api/team/chats', staff) assert.equal(list.status, 200) const c1 = list.body.chats.find((c) => c.customerId === 'c1') assert.equal(c1.unreadFromCustomer, 2) assert.equal(c1.customerName, 'Kunde Eins') assert.equal((await rawGet('/api/team/chats/unread-count', staff)).body.unread, 2) }) test('Senden als Mitarbeiter: Absendername aus dem JWT, K17-Mail an den Kunden', async () => { const res = await post('/api/team/chats/c1/messages', { text: 'Hallo' }, staff) assert.equal(res.status, 201) assert.equal(res.body.message.senderName, 'Andrej') assert.equal(res.body.message.sender, 'admin') assert.equal(res.body.notice.sent, true) assert.equal(sink.sent.at(-1).to[0].address, 'k@example.invalid') }) let fileUrl = null test('Anhang hochladen, mit Nachricht senden -> signierte url', async () => { const up = await upload('c1', Buffer.from('hallo welt')) assert.equal(up.status, 201) const sent = await post('/api/team/chats/c1/messages', { text: '', attachmentIds: [up.body.attachment.id] }, staff) assert.equal(sent.status, 201) fileUrl = sent.body.message.attachments[0].url assert.match(fileUrl, URL_RE) assert.equal(URL_RE.exec(fileUrl)[1], up.body.attachment.id) const listed = await rawGet('/api/team/chats/c1/messages', staff) assert.match(listed.body.messages.at(-1).attachments[0].url, URL_RE) }) test('signierter Link: ohne JWT 200, manipuliert/fremder Kunde/abgelaufen 403, ohne sig 401, JWT ohne sig 200', async (t) => { const u = new URL(fileUrl) const rel = u.pathname + u.search const ok = await fetch(app.url + rel) assert.equal(ok.status, 200) assert.equal(await ok.text(), 'hallo welt') const tampered = rel.replace(/sig=([0-9a-f])/, (m, c) => `sig=${c === 'a' ? 'b' : 'a'}`) assert.equal((await rawGet(tampered)).status, 403) assert.equal((await rawGet(rel.replace('/chats/c1/', '/chats/c2/'))).status, 403) assert.equal((await rawGet(u.pathname)).status, 401) assert.equal((await rawGet(u.pathname, staff)).status, 200) 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 die Unterhaltung neu laden.') }) test('Lesen markiert Kundennachrichten', async () => { const res = await post('/api/team/chats/c1/read', {}, staff) assert.equal(res.body.updated, 2) assert.equal((await rawGet('/api/team/chats/unread-count', staff)).body.unread, 0) }) test('Admin-Panel unveraendert: /api/portal-admin/chats mit Admin-Cookie 200, ohne 401', async () => { assert.equal((await rawGet('/api/portal-admin/chats', { cookie: adminCookie() })).status, 200) assert.equal((await rawGet('/api/portal-admin/chats')).status, 401) }) test('Team-Chat ohne JWT -> 401', async () => { assert.equal((await rawGet('/api/team/chats')).status, 401) assert.equal((await post('/api/team/chats/c1/messages', { text: 'x' })).status, 401) })