Files
FEDEO/backend/tests/chatVisibility.test.ts
flfeders 7e6c2fbac5
All checks were successful
Build and Release FEDEO / verify (push) Successful in 57s
Build and Release FEDEO / build-and-release (push) Successful in 6m27s
feat(communication): add room visibility and project chat controls
2026-10-02 18:27:02 +02:00

111 lines
8.6 KiB
TypeScript

import assert from "node:assert/strict"
import test from "node:test"
import { randomUUID } from "node:crypto"
import { readFile } from "node:fs/promises"
import Fastify from "fastify"
import communicationRoutes from "../src/routes/communication"
import resourceRoutes from "../src/routes/resources/main"
import { projectChatSettingError } from "../src/modules/communication-access"
import { collaborationDatabase, testDatabaseUrl } from "./helpers/collaboration-db"
test("changing the project chat switch requires project management permission and a boolean", () => {
const member = { user: {}, hasPermission: () => false }
const manager = { user: {}, hasPermission: (permission: string) => permission === "organisation.projects.write" }
assert.equal(projectChatSettingError(member, {}), null)
assert.equal(projectChatSettingError(member, { chatEnabled: true }), null)
assert.equal(projectChatSettingError(member, { chatEnabled: false }, false), null)
assert.equal(projectChatSettingError(member, { chatEnabled: false })?.statusCode, 403)
assert.equal(projectChatSettingError(member, { chatEnabled: true }, false)?.statusCode, 403)
assert.equal(projectChatSettingError(manager, { chatEnabled: false }), null)
assert.equal(projectChatSettingError({ user: { is_admin: true } }, { chatEnabled: true }, false), null)
for (const invalid of [null, "false", 0, {}]) assert.equal(projectChatSettingError(manager, { chatEnabled: invalid })?.statusCode, 400)
})
test("migration keeps existing and newly created project chats enabled", { skip: !testDatabaseUrl }, async () => {
const fixture = await collaborationDatabase()
try {
await fixture.pool.query("ALTER TABLE projects DROP COLUMN chat_enabled")
await fixture.pool.query("INSERT INTO projects (id,tenant,name) VALUES (1,1,'Existing')")
await fixture.pool.query(await readFile(new URL("../db/migrations/0078_chat_visibility.sql", import.meta.url), "utf8"))
await fixture.pool.query("INSERT INTO projects (id,tenant,name) VALUES (2,1,'New')")
assert.deepEqual((await fixture.pool.query("SELECT chat_enabled FROM projects ORDER BY id")).rows, [{ chat_enabled: true }, { chat_enabled: true }])
} finally { await fixture.close() }
})
test("hidden rooms persist per user and tenant without changing room memberships", { skip: !testDatabaseUrl }, async () => {
const fixture = await collaborationDatabase(["0078_chat_visibility.sql"]), app = Fastify()
const user = randomUUID(), other = randomUUID(), collaborationKey = `collaboration:${randomUUID()}`
try {
await fixture.pool.query("INSERT INTO tenants(id,name) VALUES (1,'A'),(2,'B')")
await fixture.pool.query("INSERT INTO auth_users(id,email) VALUES ($1,'a@example.test'),($2,'b@example.test')", [user, other])
await fixture.pool.query("INSERT INTO auth_tenant_users(tenant_id,user_id) VALUES (1,$1),(2,$1),(1,$2)", [user, other])
app.decorate("db", fixture.db)
app.addHook("preHandler", async (req: any) => {
req.user = { tenant_id: Number(req.headers["x-tenant"] || 1), user_id: req.headers["x-user"] || user }
req.hasPermission = () => false
})
await app.register(communicationRoutes)
const url = "/communication/chat/room-preferences"
for (const roomKey of ["allgemein", "project_1", collaborationKey, collaborationKey]) {
const result = await app.inject({ method: "PUT", url, payload: { roomKey, hidden: true, userId: other, tenantId: 2 } })
assert.equal(result.statusCode, 200, result.body)
}
assert.deepEqual((await app.inject(url)).json().hiddenRoomKeys.sort(), ["allgemein", "project_1", collaborationKey].sort())
assert.deepEqual((await app.inject({ url, headers: { "x-user": other } })).json().hiddenRoomKeys, [])
assert.deepEqual((await app.inject({ url, headers: { "x-tenant": "2" } })).json().hiddenRoomKeys, [])
assert.equal((await fixture.pool.query("SELECT count(*) FROM communication_room_members")).rows[0].count, "0")
await app.inject({ method: "PUT", url, payload: { roomKey: collaborationKey, hidden: false } })
assert.deepEqual((await app.inject(url)).json().hiddenRoomKeys.sort(), ["allgemein", "project_1"])
for (const payload of [{ roomKey: "allgemein", hidden: "false" }, { roomKey: "../secret", hidden: true }, { roomKey: "", hidden: true }]) {
assert.equal((await app.inject({ method: "PUT", url, payload })).statusCode, 400)
}
await fixture.pool.query("DELETE FROM auth_tenant_users WHERE tenant_id=1 AND user_id=$1", [user])
assert.equal((await app.inject(url)).statusCode, 403)
assert.equal((await app.inject({ method: "PUT", url, payload: { roomKey: "allgemein", hidden: false } })).statusCode, 403)
} finally { await app.close(); await fixture.close() }
})
test("disabling in the project form blocks list, provision, reads and writes until re-enabled", { skip: !testDatabaseUrl }, async () => {
const fixture = await collaborationDatabase(), app = Fastify()
const user = randomUUID(), profile = randomUUID()
try {
await fixture.pool.query("INSERT INTO tenants(id,name) VALUES (1,'A'),(2,'B')")
await fixture.pool.query("INSERT INTO auth_users(id,email) VALUES ($1,'a@example.test')", [user])
await fixture.pool.query("INSERT INTO auth_tenant_users(tenant_id,user_id) VALUES (1,$1)", [user])
await fixture.pool.query("INSERT INTO auth_profiles(id,tenant_id,user_id) VALUES ($1,1,$2)", [profile, user])
await fixture.pool.query("INSERT INTO projects(id,tenant,name,profiles) VALUES (1,1,'Project',ARRAY[$1]::uuid[]),(2,2,'Other tenant','{}')", [profile])
app.decorate("db", fixture.db)
app.addHook("preHandler", async (req: any) => {
req.user = { tenant_id: 1, user_id: user, is_admin: req.headers["x-admin"] === "true" }
req.hasPermission = (permission: string) => permission === "organisation.projects.write" && req.headers["x-manager"] === "true"
})
await app.register(communicationRoutes)
await app.register(resourceRoutes)
const update = (chatEnabled: unknown, headers = { "x-manager": "true" }) => app.inject({ method: "PUT", url: "/resource/projects/1", headers, payload: { chatEnabled } })
const provision = await app.inject({ method: "POST", url: "/communication/chat/project-rooms/1/provision" })
assert.equal(provision.statusCode, 200, provision.body)
const roomId = provision.json().id
await fixture.pool.query("INSERT INTO communication_messages(tenant_id,room_id,author_user_id,body) VALUES (1,$1,$2,'Preserved history')", [roomId, user])
assert.equal((await update(false, { "x-manager": "false" })).statusCode, 403)
assert.equal((await update("false")).statusCode, 400)
assert.equal((await app.inject({ method: "PUT", url: "/resource/projects/2", headers: { "x-manager": "true" }, payload: { chatEnabled: false } })).statusCode, 404)
assert.equal((await update(false)).statusCode, 200)
assert.deepEqual((await app.inject("/communication/chat/project-rooms")).json().rooms, [])
assert.equal((await app.inject({ method: "POST", url: "/communication/chat/project-rooms/1/provision" })).statusCode, 404)
for (const suffix of ["messages", "sync", "members"]) {
assert.equal((await app.inject(`/communication/chat/rooms/project_1/${suffix}`)).statusCode, 404)
assert.equal((await app.inject({ url: `/communication/chat/rooms/project_1/${suffix}`, headers: { "x-admin": "true" } })).statusCode, 404)
}
for (const suffix of ["messages", "read"]) assert.equal((await app.inject({ method: "POST", url: `/communication/chat/rooms/project_1/${suffix}`, payload: { text: "Rejected", messageId: 1 } })).statusCode, 404)
assert.deepEqual((await app.inject("/communication/chat/unread")).json().rooms, {})
assert.equal((await update(true, { "x-manager": "false" })).statusCode, 403)
assert.equal((await update(true)).statusCode, 200)
const history = await app.inject("/communication/chat/rooms/project_1/messages")
assert.equal(history.statusCode, 200, history.body)
assert.deepEqual(history.json().messages.map(message => message.body), ["Preserved history"])
assert.equal((await app.inject("/communication/chat/project-rooms")).json().rooms.length, 1)
const create = await app.inject({ method: "POST", url: "/resource/projects", payload: { id: 3, name: "Disabled", projectNumber: "P-3", chatEnabled: false } })
assert.equal(create.statusCode, 403)
} finally { await app.close(); await fixture.close() }
})