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

105 lines
5.6 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 } 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)
})