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

294 lines
9.0 KiB
TypeScript

import { beforeEach, describe, expect, it, vi } from "vitest";
const mocks = vi.hoisted(() => ({
listChannels: vi.fn(),
getDetail: vi.fn(),
getAdministration: vi.fn(),
recordSession: vi.fn(),
archiveChannel: vi.fn(),
setMemberRole: vi.fn(),
removeMember: vi.fn(),
getAttachment: vi.fn(),
}));
vi.mock("@hivegrid/chat/audit.server", () => ({
listChatAuditChannels: mocks.listChannels,
getChatAuditChannelDetail: mocks.getDetail,
getChatAuditAdministration: mocks.getAdministration,
recordChatAuditSessionStarted: mocks.recordSession,
forceArchiveChatAuditChannel: mocks.archiveChannel,
forceSetChatAuditMemberRole: mocks.setMemberRole,
forceRemoveChatAuditMember: mocks.removeMember,
getChatAuditAttachmentDownload: mocks.getAttachment,
}));
import {
archiveChatAuditChannel,
loadChatAuditChannel,
loadChatAuditAttachment,
loadChatAuditOverview,
removeChatAuditMember,
setChatAuditMemberRole,
startChatAuditSession,
} from "./ChatAuditService.server";
const context = {
tenantId: "tenant-1",
userId: "admin-1",
tenantUserId: "tenant-user-1",
permissions: ["chat.read_all_channels"],
hasModule: true as const,
};
const session = {
id: "session-1",
tenantId: "tenant-1",
userId: "admin-1",
reason: "Kundenreklamation prüfen",
issuedAt: Date.parse("2026-07-16T10:00:00.000Z"),
expiresAt: Date.parse("2026-07-16T10:15:00.000Z"),
};
const labels = {
directMessage: "Direktnachricht",
unnamedGroup: "Gruppe",
unnamedProject: "Projekt",
unknownUser: "Unbekannt",
unknownMention: "Unbekannte Erwähnung",
};
describe("AK-P15b chat audit service", () => {
beforeEach(() => {
vi.clearAllMocks();
mocks.listChannels.mockResolvedValue([]);
mocks.getAdministration.mockResolvedValue({
channel: { id: "dm-1", kind: "dm", archivedAtIso: null },
canAdminister: true,
members: [
{ userId: "user-a", displayName: "Anna Adler", avatarUrl: null, roleInChat: "admin" },
{ userId: "user-b", displayName: "Ben Bauer", avatarUrl: null, roleInChat: "member" },
],
});
});
it("does not load tenant channels before a justified session exists", async () => {
const vm = await loadChatAuditOverview({ context, tenantSlug: "demo", session: null, labels });
expect(mocks.listChannels).not.toHaveBeenCalled();
if (vm.status !== "ready") throw new Error("expected ready");
expect(vm.data.requiresReason).toBe(true);
});
it("audits the overview read with the active reason session", async () => {
const vm = await loadChatAuditOverview({
context,
tenantSlug: "demo",
session,
requestId: "overview-read-1",
labels,
});
expect(mocks.listChannels).toHaveBeenCalledWith(
expect.objectContaining({
requestId: "overview-read-1",
auditSession: expect.objectContaining({
sessionId: "session-1",
reason: "Kundenreklamation prüfen",
}),
})
);
expect(vm.status).toBe("ready");
});
it("records the session reason with the canonical read-all permission boundary", async () => {
await startChatAuditSession({ context, session, requestId: "request-1" });
expect(mocks.recordSession).toHaveBeenCalledWith({
tenantId: "tenant-1",
userId: "admin-1",
permissions: ["chat.read_all_channels"],
requestId: "request-1",
auditSession: {
sessionId: "session-1",
reason: "Kundenreklamation prüfen",
expiresAtIso: "2026-07-16T10:15:00.000Z",
},
});
});
it("renders direct messages with the mandatory audit banner and no write affordance", async () => {
mocks.getDetail.mockResolvedValue(detailFixture());
const result = await loadChatAuditChannel({
context,
tenantSlug: "demo",
chatId: "dm-1",
session,
requestId: "open-1",
labels,
});
expect(mocks.getDetail).toHaveBeenCalledWith(expect.objectContaining({ tenantId: "tenant-1", chatId: "dm-1" }));
if (result.vm.status !== "ready") throw new Error("expected ready");
expect(result.vm.data.header).toMatchObject({
isAuditMode: true,
dmAuditBannerKey: "chat.audit.dmBanner",
title: "Anna Adler · Ben Bauer",
canManage: false,
settingsHref: null,
});
expect(result.vm.data.composer).toMatchObject({ enabled: false, canSend: false, disabledReasonKey: "chat.composer.auditReadOnly" });
expect(result.vm.data.messages[0]?.actions).toEqual({
canReact: false,
canReply: false,
canEdit: false,
canDelete: false,
canPin: false,
canMarkAsTask: false,
});
expect(result.vm.data.messages[0]?.attachments[0]).toMatchObject({
downloadHref: "/demo/settings/chat/audit/dm-1/attachments/file-1/original",
previewHref: "/demo/settings/chat/audit/dm-1/attachments/file-1/preview",
});
if (result.administrationVm.status !== "ready") throw new Error("expected ready");
expect(result.administrationVm.data).toMatchObject({ canAdminister: true, canArchive: true });
});
it("keeps attachment delivery inside the active audit session", async () => {
mocks.getAttachment.mockResolvedValue({ fileId: "file-1", storageKey: "tenant/file-1" });
await loadChatAuditAttachment({
context,
chatId: "dm-1",
fileId: "file-1",
variant: "original",
session,
requestId: "download-1",
});
expect(mocks.getAttachment).toHaveBeenCalledWith(
expect.objectContaining({
tenantId: "tenant-1",
userId: "admin-1",
chatId: "dm-1",
fileId: "file-1",
variant: "original",
requestId: "download-1",
auditSession: expect.objectContaining({ sessionId: "session-1", reason: session.reason }),
})
);
});
it("routes cleanup and force-member mutations through both admin permissions and the active reason", async () => {
const adminContext = { ...context, permissions: ["chat.read_own_channels", "chat.read_all_channels", "chat.admin"] };
await archiveChatAuditChannel({ context: adminContext, chatId: "dm-1", session, requestId: "archive-1" });
await setChatAuditMemberRole({
context: adminContext,
chatId: "dm-1",
memberUserId: "user-b",
roleInChat: "admin",
session,
requestId: "role-1",
});
await removeChatAuditMember({
context: adminContext,
chatId: "dm-1",
memberUserId: "user-b",
session,
requestId: "remove-1",
});
const reasonContext = {
tenantId: "tenant-1",
permissions: adminContext.permissions,
};
expect(mocks.archiveChannel).toHaveBeenCalledWith(
expect.objectContaining({
...reasonContext,
requestId: "archive-1",
auditSession: expect.objectContaining({ sessionId: "session-1", reason: "Kundenreklamation prüfen" }),
})
);
expect(mocks.setMemberRole).toHaveBeenCalledWith(
expect.objectContaining({
...reasonContext,
memberUserId: "user-b",
roleInChat: "admin",
requestId: "role-1",
auditSession: expect.objectContaining({ sessionId: "session-1", reason: "Kundenreklamation prüfen" }),
})
);
expect(mocks.removeMember).toHaveBeenCalledWith(
expect.objectContaining({
...reasonContext,
memberUserId: "user-b",
requestId: "remove-1",
auditSession: expect.objectContaining({ sessionId: "session-1", reason: "Kundenreklamation prüfen" }),
})
);
});
});
function detailFixture() {
return {
channel: {
id: "dm-1",
kind: "dm",
name: null,
description: null,
projektId: null,
isPrivate: true,
archivedAtIso: null,
lastMessageAtIso: "2026-07-16T09:00:00.000Z",
eventStreamId: "stream-1",
eventCursor: "stream-1:1",
member: {
roleInChat: "member",
notificationPreference: "muted",
lastReadMessageId: null,
lastReadAtIso: null,
isPinned: false,
canWrite: false,
},
memberCount: 2,
participants: [
{ userId: "user-a", displayName: "Anna Adler", avatarUrl: null },
{ userId: "user-b", displayName: "Ben Bauer", avatarUrl: null },
],
unreadCount: 0,
hasUnreadMention: false,
lastMessage: null,
},
messages: [
{
id: "message-1",
clientMessageId: "client-1",
authorUserId: "user-a",
body: "Vertrauliche Nachricht",
bodyFormat: "markdown",
parentMessageId: null,
mentions: [],
attachmentIds: ["file-1"],
linkPreviews: [],
createdAt: "2026-07-16T09:00:00.000Z",
updatedAt: "2026-07-16T09:00:00.000Z",
editedAt: null,
deletedAt: null,
},
],
readReceipts: [],
messageFeatures: [],
attachments: [
{
messageId: "message-1",
fileId: "file-1",
displayName: "foto.png",
mimeType: "image/png",
sizeBytes: 42,
linkRole: "image",
previewStatus: "ready",
scanStatus: "clean",
},
],
hasOlderMessages: false,
olderMessageCursor: null,
eventCursor: "stream-1:1",
};
}