Files
HiveGrid-Wireframes/03 Quellen und Referenzen/hivegrid-cloud/apps/web/app/modules/chat/services/ChatChannelSettingsService.server.test.ts
T

174 lines
6.8 KiB
TypeScript

import { beforeEach, describe, expect, it, vi } from "vitest";
const mocks = vi.hoisted(() => ({
loadSettings: vi.fn(),
setMember: vi.fn(),
removeMember: vi.fn(),
setPrivacy: vi.fn(),
leave: vi.fn(),
archive: vi.fn(),
projectFindFirst: vi.fn(),
}));
vi.mock("@hivegrid/chat/channel-settings.server", () => ({ loadChatChannelSettings: mocks.loadSettings }));
vi.mock("@hivegrid/chat/lifecycle.server", () => ({
setChatChannelMember: mocks.setMember,
removeChatChannelMember: mocks.removeMember,
setGroupChatPrivacy: mocks.setPrivacy,
leaveGroupChat: mocks.leave,
archiveChatChannel: mocks.archive,
}));
vi.mock("~/utils/db/.server/tenantRls.server", () => ({
withTenantRLS: vi.fn((_tenantId: string, operation: (tx: unknown) => Promise<unknown>) =>
operation({ projekt: { findFirst: mocks.projectFindFirst } })
),
}));
import {
addChatChannelMember,
archiveChatChannel,
changeChatChannelMemberRole,
changeChatChannelPrivacy,
leaveChatChannel,
loadChatChannelSettingsPage,
removeChatChannelMember,
} from "./ChatChannelSettingsService.server";
const context = {
tenantId: "tenant_1",
userId: "user_current",
tenantUserId: "tenant_user_current",
permissions: ["chat.read_own_channels"],
hasModule: true as const,
};
const labels = {
directMessage: "Direktnachricht",
unnamedGroup: "Gruppe",
unnamedProject: "Projekt",
unknownProjectStage: "Unbekannte Phase",
};
function profile(userId: string, displayName: string) {
return { userId, displayName, email: `${userId}@example.test`, avatarUrl: null };
}
function settingsRecord(kind: "group" | "dm" | "project" = "group") {
return {
channel: {
id: "chat_1",
kind,
name: kind === "group" ? "Montage" : null,
description: null,
privacy: kind === "group" ? ("private" as const) : null,
archivedAtIso: null,
projectId: kind === "project" ? "project_1" : null,
},
members: [
{
...profile("user_current", "Raffael Sheikh"),
roleInChat: "admin" as const,
joinedAtIso: "2026-07-15T10:00:00.000Z",
joinedViaUserId: null,
writeRevokedAtIso: null,
provenance: {
isManualMember: kind !== "dm",
manualRoleInChat: kind !== "dm" ? ("admin" as const) : null,
isProjectManaged: kind === "project",
projectRoleInChat: kind === "project" ? ("admin" as const) : null,
isGroupManaged: false,
},
},
{
...profile("user_peer", "Mara Klein"),
roleInChat: "member" as const,
joinedAtIso: "2026-07-15T10:00:00.000Z",
joinedViaUserId: null,
writeRevokedAtIso: null,
provenance: {
isManualMember: kind === "group",
manualRoleInChat: kind === "group" ? ("member" as const) : null,
isProjectManaged: kind === "project",
projectRoleInChat: kind === "project" ? ("member" as const) : null,
isGroupManaged: false,
},
},
],
candidateUsers: kind === "group" ? [profile("user_candidate", "Ada Lovelace")] : [],
capabilities: {
canManageMembers: kind === "group",
canManageAdmins: kind === "group",
canChangePrivacy: kind === "group",
canManageManualGuests: kind === "project",
canArchive: kind !== "dm",
canLeave: false,
isReadOnly: kind !== "group",
},
};
}
describe("AK-P15b chat channel settings service", () => {
beforeEach(() => {
vi.clearAllMocks();
mocks.loadSettings.mockResolvedValue(settingsRecord());
});
it("maps the RLS-scoped group record to the pure settings VM", async () => {
const vm = await loadChatChannelSettingsPage({ context, tenantSlug: "demo", chatId: "chat_1", labels });
expect(mocks.loadSettings).toHaveBeenCalledWith({
tenantId: "tenant_1",
userId: "user_current",
permissions: context.permissions,
chatId: "chat_1",
});
expect(vm.status).toBe("ready");
if (vm.status !== "ready") throw new Error("fixture must be ready");
expect(vm.data.channel).toMatchObject({ title: "Montage", privacy: "private", isArchived: false });
expect(vm.data.members.map(({ displayName }) => displayName)).toEqual(["Raffael Sheikh", "Mara Klein"]);
expect(vm.data.candidateUsers.map(({ displayName }) => displayName)).toEqual(["Ada Lovelace"]);
});
it("resolves project metadata inside tenant RLS and keeps project membership read-only", async () => {
mocks.loadSettings.mockResolvedValue(settingsRecord("project"));
mocks.projectFindFirst.mockResolvedValue({
id: "project_1",
projektNummer: "P-42",
projektname: "Wärmepumpe Klein",
stage: { name: "Montage" },
});
const vm = await loadChatChannelSettingsPage({ context, tenantSlug: "demo", chatId: "chat_1", labels });
expect(mocks.projectFindFirst).toHaveBeenCalledWith(expect.objectContaining({ where: { tenantId: "tenant_1", id: "project_1" } }));
expect(vm.status).toBe("ready");
if (vm.status !== "ready") throw new Error("fixture must be ready");
expect(vm.data.channel.project).toEqual({
id: "project_1",
number: "P-42",
label: "Wärmepumpe Klein",
stageLabel: "Montage",
href: "/demo/projects/project_1",
});
expect(vm.data.infoKey).toBe("chat.channelSettings.project.managedExternally");
expect(vm.data.permissions).toMatchObject({ canManageManualGuests: true, canArchive: true });
});
it("delegates group settings mutations with tenant context and stable request ids", async () => {
const base = { context, chatId: "chat_1", requestId: "request_1" };
await addChatChannelMember({ ...base, memberUserId: "user_new" });
await changeChatChannelMemberRole({ ...base, memberUserId: "user_new", roleInChat: "admin" });
await removeChatChannelMember({ ...base, memberUserId: "user_new" });
await changeChatChannelPrivacy({ ...base, privacy: "public" });
await leaveChatChannel(base);
await archiveChatChannel(base);
const repositoryContext = { tenantId: "tenant_1", userId: "user_current", permissions: context.permissions };
expect(mocks.setMember).toHaveBeenNthCalledWith(1, { ...repositoryContext, chatId: "chat_1", requestId: "request_1", memberUserId: "user_new", roleInChat: "member" });
expect(mocks.setMember).toHaveBeenNthCalledWith(2, { ...repositoryContext, chatId: "chat_1", requestId: "request_1", memberUserId: "user_new", roleInChat: "admin" });
expect(mocks.removeMember).toHaveBeenCalledWith({ ...repositoryContext, chatId: "chat_1", requestId: "request_1", memberUserId: "user_new" });
expect(mocks.setPrivacy).toHaveBeenCalledWith({ ...repositoryContext, chatId: "chat_1", requestId: "request_1", isPrivate: false });
expect(mocks.leave).toHaveBeenCalledWith({ ...repositoryContext, chatId: "chat_1", requestId: "request_1" });
expect(mocks.archive).toHaveBeenCalledWith({ ...repositoryContext, chatId: "chat_1", requestId: "request_1" });
});
});