Files
Termix/plugins/session-sharing/tests/backend/rooms.test.ts
T

325 lines
9.8 KiB
TypeScript

import { afterEach, describe, expect, it } from "vitest";
import { startServer, type TestServer } from "./helpers";
let server: TestServer | null = null;
afterEach(async () => {
await server?.close();
server = null;
});
async function roomWithBob(s: TestServer, persistent = false) {
const created = await s.request("POST", "/rooms", {
body: { name: "Standup", persistent },
});
const roomId = created.body.room.id as string;
await s.request("POST", `/rooms/${roomId}/members`, {
body: { userIds: ["bob"] },
});
return roomId;
}
async function present(s: TestServer, roomId: string, user = "alice") {
s.live.addSession(`sess-${user}`, user);
return s.request("POST", `/rooms/${roomId}/present`, {
user,
body: { protocol: "ssh", sessionId: `sess-${user}`, hostId: 1 },
});
}
describe("collab rooms", () => {
it("resolves a synced host to the server ID only with connect access", async () => {
server = await startServer({ hostIds: [41, 42], accessibleHostIds: [42] });
server.db.sqlite.exec("UPDATE ssh_data SET sync_id = 'shared-host'");
expect(await server.request("GET", "/meeting-host/shared-host")).toEqual({
status: 200,
body: { id: 42 },
});
server.db.sqlite.exec(
"UPDATE ssh_data SET sync_id = 'private-host' WHERE id = 41",
);
expect(
(await server.request("GET", "/meeting-host/private-host")).status,
).toBe(404);
expect(
(await server.request("GET", "/meeting-host/missing-host")).status,
).toBe(404);
expect(
(await server.request("GET", "/meeting-host/shared-host", { user: null }))
.status,
).toBe(401);
});
it("creates a room with the creator as host and hides the guest token", async () => {
server = await startServer();
const res = await server.request("POST", "/rooms", {
body: { name: " Standup " },
});
expect(res.status).toBe(200);
expect(res.body.room).toMatchObject({
name: "Standup",
ownerUserId: "alice",
guestLinkEnabled: false,
});
expect(res.body.room).not.toHaveProperty("guestLinkToken");
const detail = await server.request("GET", `/rooms/${res.body.room.id}`);
expect(detail.body.isHost).toBe(true);
expect(detail.body.members).toEqual([
expect.objectContaining({ userId: "alice", roomRole: "host" }),
]);
expect(detail.body.eventsWsPath).toBe("/plugin-ws/ssh-terminal/terminal");
expect(server.mock.audits.map((entry) => entry.action)).toContain(
"collab_room_create",
);
});
it("keeps rooms private to members and invites to the host", async () => {
server = await startServer();
const roomId = await roomWithBob(server);
expect(
(await server.request("GET", `/rooms/${roomId}`, { user: "carol" }))
.status,
).toBe(404);
expect(
(
await server.request("POST", `/rooms/${roomId}/members`, {
user: "bob",
body: { userIds: ["carol"] },
})
).status,
).toBe(403);
expect(
(await server.request("GET", "/rooms", { user: "bob" })).body.rooms,
).toHaveLength(1);
});
it("expands a role invite to its current members", async () => {
server = await startServer();
server.db.sqlite.exec(
"INSERT INTO roles (id, name) VALUES (5, 'ops'); INSERT INTO user_roles (user_id, role_id) VALUES ('carol', 5)",
);
const created = await server.request("POST", "/rooms", {
body: { name: "Ops" },
});
const roomId = created.body.room.id;
await server.request("POST", `/rooms/${roomId}/members`, {
body: { roleIds: [5] },
});
expect(
(await server.request("GET", `/rooms/${roomId}`, { user: "carol" }))
.status,
).toBe(200);
expect(
(
await server.request("POST", `/rooms/${roomId}/members`, {
body: { roleIds: [99] },
})
).status,
).toBe(404);
});
it("puts a member's live session on stage as a read-only room share", async () => {
server = await startServer();
const roomId = await roomWithBob(server);
const res = await present(server, roomId, "bob");
expect(res.status).toBe(200);
const stage = await server.request("GET", `/rooms/${roomId}/stage`);
expect(stage.body.stage).toMatchObject({
presenterUserId: "bob",
protocol: "ssh",
sessionId: "sess-bob",
});
const row = server.db.sqlite
.prepare(
"SELECT share_type, permission_level FROM p_session_sharing_shares",
)
.get();
expect(row).toEqual({ share_type: "room", permission_level: "read-only" });
});
it("refuses to present someone else's session", async () => {
server = await startServer();
const roomId = await roomWithBob(server);
server.live.addSession("sess-alice", "alice");
const res = await server.request("POST", `/rooms/${roomId}/present`, {
user: "bob",
body: { protocol: "ssh", sessionId: "sess-alice", hostId: 1 },
});
expect(res.status).toBe(403);
});
it("clears a stage whose session died", async () => {
server = await startServer();
const roomId = await roomWithBob(server);
await present(server, roomId);
server.live.sessions.delete("sess-alice");
const stage = await server.request("GET", `/rooms/${roomId}/stage`, {
user: "bob",
});
expect(stage.body.stage).toBeNull();
const detail = await server.request("GET", `/rooms/${roomId}`);
expect(detail.body.stage.shareId).toBeNull();
});
it("hands control only from the presenter or host, and only to members", async () => {
server = await startServer();
const roomId = await roomWithBob(server);
await present(server, roomId);
expect(
(
await server.request("POST", `/rooms/${roomId}/control`, {
user: "bob",
body: { userId: "bob" },
})
).status,
).toBe(403);
expect(
(
await server.request("POST", `/rooms/${roomId}/control`, {
body: { userId: "carol" },
})
).status,
).toBe(404);
const granted = await server.request("POST", `/rooms/${roomId}/control`, {
body: { userId: "bob" },
});
expect(granted.body).toEqual({ controllerUserId: "bob" });
expect(server.live.calls).toContainEqual([
"setRoomShareControl",
"sess-alice",
expect.any(String),
"bob",
]);
// The controller may give it back.
expect(
(
await server.request("POST", `/rooms/${roomId}/control`, {
user: "bob",
body: { userId: null },
})
).status,
).toBe(200);
});
it("rate limits hand raises", async () => {
server = await startServer();
const roomId = await roomWithBob(server);
await present(server, roomId);
const first = await server.request(
"POST",
`/rooms/${roomId}/control/request`,
{ user: "bob" },
);
expect(first.body.request).toMatchObject({
userId: "bob",
username: "bob",
});
expect(
(
await server.request("POST", `/rooms/${roomId}/control/request`, {
user: "bob",
})
).status,
).toBe(429);
expect(
(
await server.request("GET", `/rooms/${roomId}/control/requests`, {
user: "bob",
})
).status,
).toBe(403);
expect(
(await server.request("GET", `/rooms/${roomId}/control/requests`)).body
.requests,
).toHaveLength(1);
});
it("removes a member and drops them from the stage", async () => {
server = await startServer();
const roomId = await roomWithBob(server);
await present(server, roomId);
expect(
(await server.request("DELETE", `/rooms/${roomId}/members/alice`)).status,
).toBe(400);
expect(
(await server.request("DELETE", `/rooms/${roomId}/members/bob`)).status,
).toBe(200);
expect(server.live.calls).toContainEqual([
"disconnectParticipants",
"sess-alice",
expect.any(String),
{
userId: "bob",
reason: "You were removed from the collaboration room",
},
]);
});
it("lists stage guests for members and lets only the host remove them", async () => {
server = await startServer();
const roomId = await roomWithBob(server);
await present(server, roomId);
const detail = await server.request("GET", `/rooms/${roomId}`, {
user: "bob",
});
expect(detail.body.guests).toEqual([{ label: "Guest 1" }]);
expect(
(
await server.request("POST", `/rooms/${roomId}/guests/remove`, {
user: "bob",
})
).status,
).toBe(403);
expect(
(await server.request("POST", `/rooms/${roomId}/guests/remove`)).status,
).toBe(200);
expect(server.live.calls).toContainEqual([
"disconnectParticipants",
"sess-alice",
expect.any(String),
{ userId: null, reason: "The host removed the guests" },
]);
});
it("ends a one-off room for good and keeps a persistent one", async () => {
server = await startServer();
const oneOff = await roomWithBob(server);
await present(server, oneOff);
expect(
(await server.request("POST", `/rooms/${oneOff}/end`, { user: "bob" }))
.status,
).toBe(403);
await server.request("POST", `/rooms/${oneOff}/end`);
expect((await server.request("GET", `/rooms/${oneOff}`)).status).toBe(404);
const persistent = await roomWithBob(server, true);
await server.request("POST", `/rooms/${persistent}/end`);
expect((await server.request("GET", `/rooms/${persistent}`)).status).toBe(
200,
);
});
it("deletes a room and its members", async () => {
server = await startServer();
const roomId = await roomWithBob(server);
expect((await server.request("DELETE", `/rooms/${roomId}`)).status).toBe(
200,
);
expect(
server.db.sqlite
.prepare("SELECT COUNT(*) AS n FROM p_session_sharing_room_members")
.get(),
).toEqual({ n: 0 });
});
});