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) => 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" }); }); });