Files
FEDEO/backend/tests/communicationAccess.test.ts

117 lines
8.6 KiB
TypeScript

import assert from "node:assert/strict"
import test from "node:test"
import { randomUUID } from "node:crypto"
import Fastify from "fastify"
import communicationRoutes from "../src/routes/communication"
import { assertCustomRoomKey } from "../src/modules/communication-access"
import { collaborationDatabase, testDatabaseUrl } from "./helpers/collaboration-db"
test("custom room keys cannot impersonate system rooms", () => {
for (const key of ["allgemein", "direct_abc", "project_7"]) assert.throws(() => assertCustomRoomKey(key))
assert.doesNotThrow(() => assertCustomRoomKey("baustelle"))
})
test("general chat accepts legacy room metadata without exposing protected rooms", { skip: !testDatabaseUrl }, async (t) => {
const fixture = await collaborationDatabase(), app = Fastify()
const user = randomUUID(), other = randomUUID(), legacyRoom = randomUUID(), otherTenantRoom = randomUUID()
try {
await fixture.pool.query("INSERT INTO tenants VALUES (1,'Legacy tenant',null),(2,'Other tenant',null)")
await fixture.pool.query("INSERT INTO auth_users(id,email) VALUES ($1,'user@example.test'),($2,'other@example.test')", [user, other])
await fixture.pool.query("INSERT INTO auth_tenant_users(tenant_id,user_id) VALUES (1,$1),(1,$2),(2,$2)", [user, other])
// Matrix's general-room endpoints supplied key/name but omitted type, defaulting to 'room'.
await fixture.pool.query(`INSERT INTO communication_rooms(id,tenant_id,key,name,topic,type,archived)
VALUES ($1,1,'allgemein','Allgemeiner Chat','Existing topic','room',false),
($2,2,'allgemein','Allgemeiner Chat',null,'room',false)`, [legacyRoom, otherTenantRoom])
await fixture.pool.query("INSERT INTO communication_room_members(room_id,user_id) VALUES ($1,$2)", [legacyRoom, other])
await fixture.pool.query("INSERT INTO communication_messages(tenant_id,room_id,author_user_id,body) VALUES (1,$1,$2,'Existing history')", [legacyRoom, other])
app.decorate("db", fixture.db)
app.addHook("preHandler", async (req: any) => { req.user = { tenant_id: 1, user_id: user }; req.hasPermission = () => false })
await app.register(communicationRoutes)
await t.test("concurrent legacy loads preserve the room, history and tenant boundary", async () => {
const responses = await Promise.all([app.inject("/communication/chat/rooms"), app.inject("/communication/chat/rooms")])
for (const response of responses) {
assert.equal(response.statusCode, 200, response.body)
assert.equal(response.json().rooms.length, 1)
assert.equal(response.json().rooms[0].id, legacyRoom)
assert.equal(response.json().rooms[0].type, "general")
assert.equal(response.json().rooms[0].topic, "Existing topic")
}
const history = await app.inject("/communication/chat/rooms/allgemein/messages")
assert.equal(history.statusCode, 200, history.body)
assert.equal(history.json().messages[0].body, "Existing history")
assert.equal((await fixture.pool.query("SELECT count(*) FROM communication_room_members WHERE room_id=$1", [legacyRoom])).rows[0].count, "2")
assert.equal((await fixture.pool.query("SELECT type FROM communication_rooms WHERE id=$1", [otherTenantRoom])).rows[0].type, "room")
})
for (const candidate of [
{ type: "direct" }, { type: "project" }, { type: "room", entityType: "project", entityId: 42 },
{ type: "room", entityId: 42 }, { type: "room", entityUuid: other },
{ type: "general", entityType: "project", entityId: 42 },
{ type: "room", archived: true }, { type: "general", archived: true },
]) await t.test(`protected general-key collision is not converted: ${JSON.stringify(candidate)}`, async () => {
const roomId = randomUUID()
await fixture.pool.query("UPDATE communication_rooms SET key='preserved-legacy' WHERE id=$1", [legacyRoom])
await fixture.pool.query(`INSERT INTO communication_rooms(id,tenant_id,key,name,type,archived,entity_type,entity_id,entity_uuid)
VALUES ($1,1,'allgemein','Protected',$2,$3,$4,$5,$6)`,
[roomId, candidate.type, candidate.archived || false, candidate.entityType || null, candidate.entityId || null, candidate.entityUuid || null])
await fixture.pool.query("INSERT INTO communication_room_members(room_id,user_id) VALUES ($1,$2)", [roomId, other])
try {
const response = await app.inject("/communication/chat/rooms")
assert.equal(response.statusCode, 409, response.body)
const stored = (await fixture.pool.query("SELECT type,archived FROM communication_rooms WHERE id=$1", [roomId])).rows[0]
assert.equal(stored.type, candidate.type)
assert.equal(stored.archived, candidate.archived || false)
assert.deepEqual((await fixture.pool.query("SELECT user_id FROM communication_room_members WHERE room_id=$1", [roomId])).rows.map(m => m.user_id), [other])
} finally {
await fixture.pool.query("DELETE FROM communication_rooms WHERE id=$1", [roomId])
}
})
await t.test("concurrent first use creates one general room", async () => {
const responses = await Promise.all([app.inject("/communication/chat/rooms"), app.inject("/communication/chat/rooms")])
for (const response of responses) assert.equal(response.statusCode, 200, response.body)
assert.equal((await fixture.pool.query("SELECT count(*) FROM communication_rooms WHERE tenant_id=1 AND key='allgemein'")).rows[0].count, "1")
})
} finally { await app.close(); await fixture.close() }
})
test("chat routes protect existing memberships and assigned projects", { skip: !testDatabaseUrl }, async () => {
const fixture = await collaborationDatabase()
const app = Fastify()
const user = "00000000-0000-4000-8000-000000000001"
const other = "00000000-0000-4000-8000-000000000002"
const profile = "00000000-0000-4000-8000-000000000003"
const room = "00000000-0000-4000-8000-000000000004"
try {
await fixture.pool.query(`INSERT INTO tenants VALUES (1, 'A', null), (2, 'B', null);
INSERT INTO auth_users (id,email) VALUES ('${user}', 'a@example.test'), ('${other}', 'b@example.test');
INSERT INTO auth_tenant_users (tenant_id,user_id) VALUES (1,'${user}'), (1,'${other}');
INSERT INTO auth_profiles (id,tenant_id,user_id,first_name,last_name) VALUES ('${profile}',1,'${other}','B','Test');
INSERT INTO projects (id,tenant,name,profiles,archived) VALUES (1,1,'Hidden',ARRAY['${profile}']::uuid[],false);
INSERT INTO communication_rooms (id,tenant_id,key,name,type,archived) VALUES ('${room}',1,'private','Private','direct',false);
INSERT INTO communication_room_members (room_id,user_id) VALUES ('${room}','${other}');`)
app.decorate("db", fixture.db)
app.addHook("preHandler", async (req: any) => {
req.user = { tenant_id: 1, user_id: req.headers["x-test-user"] || user, is_admin: false }
req.hasPermission = () => false
})
await app.register(communicationRoutes)
const duplicate = await app.inject({ method: "POST", url: "/communication/chat/rooms", payload: { key: "private", name: "Open" } })
assert.equal(duplicate.statusCode, 409)
const memberships = await fixture.pool.query("SELECT user_id FROM communication_room_members WHERE room_id=$1", [room])
assert.deepEqual(memberships.rows.map((row) => row.user_id), [other])
const forbidden = await app.inject({ method: "POST", url: "/communication/chat/project-rooms/1/provision" })
assert.equal(forbidden.statusCode, 404)
const list = await app.inject("/communication/chat/project-rooms")
assert.deepEqual(list.json().rooms, [])
const assigned = await app.inject({ method: "POST", url: "/communication/chat/project-rooms/1/provision", headers: { "x-test-user": other } })
assert.equal(assigned.statusCode, 200, assigned.body)
await fixture.pool.query("UPDATE projects SET profiles='{}'::uuid[] WHERE id=1")
const removed = await app.inject({ url: "/communication/chat/rooms/project_1/messages", headers: { "x-test-user": other } })
assert.equal(removed.statusCode, 404)
await fixture.pool.query("DELETE FROM auth_tenant_users WHERE user_id=$1", [user])
assert.equal((await app.inject("/communication/chat/users")).statusCode, 403)
} finally { await app.close(); await fixture.close() }
})