294 lines
9.0 KiB
TypeScript
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",
|
|
};
|
|
}
|