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

250 lines
8.0 KiB
TypeScript

import { beforeEach, describe, expect, it, vi } from "vitest";
import { CHAT_PERMISSIONS, type ChatMemberChannelDetail, type ChatMemberChannelRecord, type ListMemberChannelsResult } from "@hivegrid/chat";
const mocks = vi.hoisted(() => ({
listMemberChannels: vi.fn(),
getMemberChannelDetail: vi.fn(),
listDiscoverablePublicGroupChats: vi.fn(),
joinPublicGroupChat: vi.fn(),
setChatMemberPreferences: vi.fn(),
tenantHasModule: vi.fn(),
}));
vi.mock("@hivegrid/chat/repository.server", () => ({
listMemberChannels: mocks.listMemberChannels,
getMemberChannelDetail: mocks.getMemberChannelDetail,
}));
vi.mock("@hivegrid/chat/discovery.server", () => ({
listDiscoverablePublicGroupChats: mocks.listDiscoverablePublicGroupChats,
}));
vi.mock("@hivegrid/chat/lifecycle.server", () => ({
joinPublicGroupChat: mocks.joinPublicGroupChat,
}));
vi.mock("@hivegrid/chat/member-preferences.server", () => ({
setChatMemberPreferences: mocks.setChatMemberPreferences,
}));
vi.mock("~/utils/connector/tenantConnector.server", () => ({
tenantHasModule: mocks.tenantHasModule,
}));
vi.mock("../routes/chatAccess.server", () => ({
requireChatPermission: (context: unknown) => context,
}));
import { joinPublicChatChannel, loadMemberChatChannel, loadMemberChatChannels, updateOwnChatChannelPreferences } from "./ChatService.server";
import type { ChatRouteContext } from "../routes/chatAccess.server";
const context: ChatRouteContext = {
tenantId: "tenant_1",
userId: "user_1",
tenantUserId: "tenant_user_1",
permissions: [CHAT_PERMISSIONS.readOwnChannels, CHAT_PERMISSIONS.write],
hasModule: true,
};
const labels = {
directMessage: "Direktnachricht",
unnamedGroup: "Unbenannte Gruppe",
unnamedProject: "Projektchat",
unknownUser: "Unbekannte Person",
unknownMention: "Unbekannte Erwähnung",
};
const emptyChannelResult: ListMemberChannelsResult = { channels: [], nextCursor: null };
const listedChannel: ChatMemberChannelRecord = {
id: "chat_1",
kind: "group",
name: "Montage",
description: null,
projektId: null,
isPrivate: false,
archivedAtIso: null,
lastMessageAtIso: null,
eventStreamId: "stream_1",
eventCursor: "stream_1:12",
member: {
roleInChat: "member",
notificationPreference: "all_messages",
lastReadMessageId: null,
lastReadAtIso: null,
isPinned: false,
canWrite: true,
},
memberCount: 1,
participants: [{ userId: "user_1", displayName: "Raffael Sheikh", avatarUrl: null }],
unreadCount: 0,
hasUnreadMention: false,
lastMessage: null,
};
describe("ChatService", () => {
beforeEach(() => {
vi.clearAllMocks();
mocks.listMemberChannels.mockResolvedValue(emptyChannelResult);
mocks.listDiscoverablePublicGroupChats.mockResolvedValue({ channels: [] });
mocks.joinPublicGroupChat.mockResolvedValue({ chatId: "chat_public", memberUserId: "user_1", roleInChat: "member", changed: true });
mocks.setChatMemberPreferences.mockResolvedValue({ chatId: "chat_1", isPinned: true, notificationPreference: "muted", changed: true });
mocks.tenantHasModule.mockResolvedValue(true);
});
it("passes the authenticated tenant/user context into the RLS repository list", async () => {
mocks.listMemberChannels.mockResolvedValue({ channels: [listedChannel], nextCursor: "next_cursor" });
const vm = await loadMemberChatChannels({
context,
tenantSlug: "demo",
filter: { query: "Montage", view: "groups", showArchived: true },
afterCursor: "current_cursor",
limit: 25,
labels,
});
expect(mocks.listMemberChannels).toHaveBeenCalledWith({
tenantId: "tenant_1",
userId: "user_1",
permissions: context.permissions,
query: "Montage",
view: "groups",
includeArchived: true,
afterCursor: "current_cursor",
limit: 25,
});
expect(vm.status).toBe("ready");
expect(vm.data?.rows.map((row) => row.id)).toEqual(["chat_1"]);
expect(vm.data?.nextCursor).toBe("next_cursor");
expect(mocks.listDiscoverablePublicGroupChats).not.toHaveBeenCalled();
});
it("loads discoverable public groups only on the first page and maps no pre-join detail link", async () => {
mocks.listDiscoverablePublicGroupChats.mockResolvedValue({
channels: [{ id: "chat_public", name: "Montage Nord", description: "Austausch", memberCount: 4 }],
});
const vm = await loadMemberChatChannels({
context,
tenantSlug: "demo",
filter: { query: "Montage", view: "all", showArchived: false },
labels,
});
expect(mocks.listDiscoverablePublicGroupChats).toHaveBeenCalledWith({
tenantId: "tenant_1",
userId: "user_1",
permissions: context.permissions,
query: "Montage",
});
expect(vm.status).toBe("ready");
expect(vm.data?.permissions.canDiscover).toBe(true);
expect(vm.data?.discoverRows).toEqual([
expect.objectContaining({
id: "chat_public",
title: "Montage Nord",
memberCount: 4,
canJoin: true,
joinHref: "/demo/chat",
detailHref: null,
}),
]);
});
it("joins through the lifecycle boundary with the authenticated tenant context", async () => {
await joinPublicChatChannel({ context, chatId: "chat_public", requestId: "request_join_1" });
expect(mocks.joinPublicGroupChat).toHaveBeenCalledWith({
tenantId: "tenant_1",
userId: "user_1",
permissions: context.permissions,
chatId: "chat_public",
requestId: "request_join_1",
});
});
it("updates only the authenticated member's channel preferences through the RLS boundary", async () => {
await updateOwnChatChannelPreferences({
context,
chatId: "chat_1",
requestId: "request_preferences_1",
isPinned: true,
notificationPreference: "muted",
});
expect(mocks.setChatMemberPreferences).toHaveBeenCalledWith({
tenantId: "tenant_1",
userId: "user_1",
permissions: context.permissions,
chatId: "chat_1",
requestId: "request_preferences_1",
isPinned: true,
notificationPreference: "muted",
});
});
it("passes the opaque history cursor and returns the repository event cursor", async () => {
const detail: ChatMemberChannelDetail = {
channel: {
id: "chat_1",
kind: "group",
name: "Montage",
description: null,
projektId: null,
isPrivate: false,
archivedAtIso: null,
lastMessageAtIso: null,
eventStreamId: "stream_1",
eventCursor: "stream_1:12",
member: {
roleInChat: "member",
notificationPreference: "all_messages",
lastReadMessageId: null,
lastReadAtIso: null,
isPinned: false,
},
memberCount: 1,
participants: [{ userId: "user_1", displayName: "Raffael Sheikh", avatarUrl: null }],
unreadCount: 0,
hasUnreadMention: false,
lastMessage: null,
},
messages: [],
readReceipts: [],
attachments: [],
hasOlderMessages: false,
olderMessageCursor: null,
eventCursor: "stream_1:12",
};
mocks.getMemberChannelDetail.mockResolvedValue(detail);
const result = await loadMemberChatChannel({
context,
tenantSlug: "demo",
chatId: "chat_1",
beforeMessageCursor: "opaque_cursor",
labels,
});
expect(mocks.getMemberChannelDetail).toHaveBeenCalledWith({
tenantId: "tenant_1",
userId: "user_1",
permissions: context.permissions,
chatId: "chat_1",
beforeMessageCursor: "opaque_cursor",
});
expect(mocks.tenantHasModule).toHaveBeenCalledWith({ tenantId: "tenant_1", userId: "user_1", moduleKey: "files" });
expect(mocks.tenantHasModule).toHaveBeenCalledWith({ tenantId: "tenant_1", userId: "user_1", moduleKey: "tasks" });
expect(result.eventCursor).toBe("stream_1:12");
expect(result.authors).toEqual([
{
userId: "user_1",
displayName: "Raffael Sheikh",
avatar: { imageUrl: null, fallbackLabel: "RS" },
isCurrentUser: true,
},
]);
expect(result.vm.status).toBe("ready");
});
});