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() } })