250 lines
8.0 KiB
TypeScript
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");
|
|
});
|
|
});
|