118 lines
9.2 KiB
TypeScript
118 lines
9.2 KiB
TypeScript
import assert from "node:assert/strict"
|
|
import test from "node:test"
|
|
import { randomUUID } from "node:crypto"
|
|
import sharp from "sharp"
|
|
import Fastify from "fastify"
|
|
import { chatContent } from "../src/modules/chat-content"
|
|
import { CollaborationService } from "../src/modules/collaboration/service"
|
|
import communicationRoutes from "../src/routes/communication"
|
|
import { collaborationDatabase, testDatabaseUrl } from "./helpers/collaboration-db"
|
|
|
|
const picture = async () => ({ name: "Baustelle.png", dataUrl: `data:image/png;base64,${(await sharp({ create: { width: 80, height: 40, channels: 3, background: '#206050' } }).png().toBuffer()).toString('base64')}` })
|
|
|
|
test("chat content accepts emoji and image-only messages, strips metadata and rejects unsafe input", async () => {
|
|
assert.equal((await chatContent({ text: "Hallo 👋 ❤️" })).body, "Hallo 👋 ❤️")
|
|
const normalized = await chatContent({ image: await picture() })
|
|
assert.equal(normalized.body, "")
|
|
assert.equal(normalized.image?.width, 80)
|
|
const metadata = await sharp(Buffer.from(normalized.image!.dataUrl.split(',')[1], 'base64')).metadata()
|
|
assert.equal(metadata.format, 'webp')
|
|
assert.equal(metadata.exif, undefined)
|
|
for (const input of [{}, { text: ' '.repeat(5) }, { text: 'a'.repeat(10001) },
|
|
{ image: { dataUrl: 'https://example.test/tracker.png' } },
|
|
{ image: { dataUrl: `data:image/png;base64,${Buffer.from('<svg xmlns="http://www.w3.org/2000/svg" width="10" height="10"></svg>').toString('base64')}` } },
|
|
{ image: { dataUrl: 'data:image/png;base64,broken' } },
|
|
{ image: { dataUrl: 'a'.repeat(700001) } }]) await assert.rejects(chatContent(input))
|
|
})
|
|
|
|
test("native chat images and reactions follow tenant, membership, project and message boundaries", { skip: !testDatabaseUrl }, async () => {
|
|
const fixture = await collaborationDatabase(), app = Fastify()
|
|
const owner = randomUUID(), colleague = randomUUID(), outsider = randomUUID(), room = randomUUID(), otherRoom = randomUUID()
|
|
try {
|
|
await fixture.pool.query("INSERT INTO tenants VALUES (1,'A',null),(2,'B',null)")
|
|
for (const [id, tenant] of [[owner, 1], [colleague, 1], [outsider, 2]]) {
|
|
await fixture.pool.query("INSERT INTO auth_users(id,email) VALUES ($1,$2)", [id, `${id}@test.example`])
|
|
await fixture.pool.query("INSERT INTO auth_tenant_users(tenant_id,user_id) VALUES ($1,$2)", [tenant, id])
|
|
}
|
|
await fixture.pool.query("INSERT INTO projects(id,tenant,name,profiles,archived,chat_enabled) VALUES (1,1,'Project','{}',false,true)")
|
|
await fixture.pool.query(`INSERT INTO communication_rooms(id,tenant_id,key,name,type,archived,entity_id)
|
|
VALUES ($1,1,'project_1','Project','project',false,1),($2,1,'other','Other','room',false,null)`, [room, otherRoom])
|
|
await fixture.pool.query("INSERT INTO communication_room_members(room_id,user_id) VALUES ($1,$2),($1,$3),($4,$2)", [room, owner, colleague, otherRoom])
|
|
app.decorate('db', fixture.db)
|
|
app.addHook('preHandler', async (req: any) => {
|
|
req.user = { user_id: req.headers['x-user'] || owner, tenant_id: Number(req.headers['x-tenant'] || 1), is_admin: true }
|
|
req.hasPermission = () => true
|
|
})
|
|
await app.register(communicationRoutes)
|
|
const base = '/communication/chat/rooms/project_1'
|
|
const sent = await app.inject({ method: 'POST', url: `${base}/messages`, payload: { image: await picture() } })
|
|
assert.equal(sent.statusCode, 200, sent.body)
|
|
const message = sent.json()
|
|
assert.equal(message.body, '')
|
|
assert.equal(message.image.width, 80)
|
|
assert.equal(message.image.dataUrl, undefined)
|
|
assert.equal((await app.inject(`${base}/messages`)).json().messages[0].image.dataUrl, undefined)
|
|
const image = await app.inject(`${base}/image?messageId=${message.id}`)
|
|
assert.equal(image.statusCode, 200, image.body)
|
|
assert.equal(image.headers['cache-control'], 'no-store')
|
|
assert.match(image.json().dataUrl, /^data:image\/webp;base64,/)
|
|
const react = (headers = {}, active = true, emoji = '👍') => app.inject({ method: 'PUT', url: `${base}/reactions`, headers, payload: { messageId: message.id, emoji, active } })
|
|
assert.equal((await react()).statusCode, 200)
|
|
assert.equal((await react()).json().reactions[message.id][0].count, 1)
|
|
assert.equal((await react({ 'x-user': colleague })).json().reactions[message.id][0].count, 2)
|
|
const removed = await react({}, false)
|
|
assert.deepEqual(removed.json().reactions[message.id], [{ emoji: '👍', count: 1, own: false }])
|
|
assert.equal((await react({}, true, '<script>')).statusCode, 400)
|
|
assert.equal((await app.inject(`/communication/chat/rooms/other/image?messageId=${message.id}`)).statusCode, 404)
|
|
assert.deepEqual((await app.inject(`/communication/chat/rooms/other/reactions?ids=${message.id}`)).json(), { reactions: {} })
|
|
for (const headers of [{ 'x-user': outsider }, { 'x-user': outsider, 'x-tenant': '2' }]) {
|
|
assert.ok([403, 404].includes((await app.inject({ url: `${base}/image?messageId=${message.id}`, headers })).statusCode))
|
|
assert.ok([403, 404].includes((await react(headers)).statusCode))
|
|
}
|
|
await fixture.pool.query('UPDATE projects SET chat_enabled=false WHERE id=1')
|
|
assert.equal((await app.inject(`${base}/image?messageId=${message.id}`)).statusCode, 404)
|
|
assert.equal((await app.inject(`${base}/reactions?ids=${message.id}`)).statusCode, 404)
|
|
assert.equal((await react()).statusCode, 404)
|
|
} finally { await app.close(); await fixture.close() }
|
|
})
|
|
|
|
test("subroom media respects history, guest read-only rights, revocation and idempotent sends", { skip: !testDatabaseUrl }, async () => {
|
|
const fixture = await collaborationDatabase()
|
|
const service = new CollaborationService(fixture.db, { publicUrl: 'https://example.test', guestPepper: () => 'test', sendMail: async () => ({ success: true }) })
|
|
const ownerId = randomUUID()
|
|
try {
|
|
await fixture.pool.query("INSERT INTO tenants VALUES (1,'A',null)")
|
|
await fixture.pool.query("INSERT INTO auth_users(id,email,is_admin) VALUES ($1,'owner@test.example',true)", [ownerId])
|
|
await fixture.pool.query('INSERT INTO auth_tenant_users(tenant_id,user_id) VALUES (1,$1)', [ownerId])
|
|
const owner = await service.userContext(ownerId, 1)
|
|
await fixture.pool.query("INSERT INTO projects(id,tenant,name,profiles,archived) VALUES (1,1,'Project','{}',false)")
|
|
const room = await service.createRoom(owner, { name: 'Media', projectId: 1 })
|
|
const guest = await service.guest(owner, { name: 'Guest', email: 'guest@test.example' })
|
|
const old = await service.send(owner.principal, room.id, { clientId: randomUUID(), image: await picture() })
|
|
const invitation = await service.invite(owner, room.id, guest, {})
|
|
await service.accept(guest, invitation.id, invitation.token)
|
|
await assert.rejects(service.image(guest, room.id, { messageId: old.id }), /kein Zugriff/)
|
|
await assert.rejects(service.react(guest, room.id, { messageId: old.id, emoji: '👍', active: true }), /kein Zugriff/)
|
|
const command = { clientId: randomUUID(), text: '', image: await picture() }
|
|
const sent = await service.send(guest, room.id, command)
|
|
assert.equal((await service.send(guest, room.id, command)).id, sent.id)
|
|
await assert.rejects(service.send(guest, room.id, { ...command, image: { ...command.image, name: 'different.png' } }), /Auftrags-ID/)
|
|
assert.equal((await service.messages(guest, room.id)).messages[0].image.dataUrl, undefined)
|
|
assert.match((await service.image(guest, room.id, { messageId: sent.id })).dataUrl, /^data:image\/webp/)
|
|
await service.react(guest, room.id, { messageId: sent.id, emoji: '❤️', active: true })
|
|
await service.react(owner.principal, room.id, { messageId: sent.id, emoji: '❤️', active: true })
|
|
assert.deepEqual((await service.reactions(guest, room.id, { ids: `${old.id},${sent.id}` })).reactions[sent.id], [{ emoji: '❤️', count: 2, own: true }])
|
|
const member = (await service.management(owner, room.id)).members.find(m => m.principalId === guest.id)
|
|
await service.updateMember(owner, room.id, member.id, { role: 'reader', overviewAllowed: false })
|
|
await assert.rejects(service.react(guest, room.id, { messageId: sent.id, emoji: '❤️', active: false }), /nur lesbar/)
|
|
await assert.rejects(service.send(guest, room.id, { clientId: randomUUID(), image: await picture() }), /nur lesbar/)
|
|
await service.image(guest, room.id, { messageId: sent.id })
|
|
await service.updateRoom(owner, room.id, { status: 'archived' })
|
|
await assert.rejects(service.react(owner.principal, room.id, { messageId: sent.id, emoji: '👍', active: true }), /archiviert/)
|
|
await service.updateRoom(owner, room.id, { status: 'active' })
|
|
await service.revoke(owner, room.id, member.id)
|
|
await assert.rejects(service.image(guest, room.id, { messageId: sent.id }), /kein Zugriff/)
|
|
await assert.rejects(service.reactions(guest, room.id, { ids: sent.id }), /kein Zugriff/)
|
|
} finally { await fixture.close() }
|
|
})
|