174 lines
6.8 KiB
TypeScript
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" });
|
|
});
|
|
});
|