- Fix sendThreadMessage room mismatch: use channelId from options instead of hardcoded controlRoomId - Add .catch() to fire-and-forget handleRoomMessage to prevent silent error swallowing - Wrap dispatchJob in try-catch for user-visible error reporting in handleFixCommand - Add MATRIX_BOT_USER_ID validation in connect() to prevent infinite message loops - Fix streamResponse error masking: wrap finally/catch side-effects in try-catch - Replace unsafe type assertion with public getClient() in MatrixRoomService - Add orphaned room warning in provisionRoom on DB failure - Add provider identity to Herald error logs - Add channelId to ThreadMessageOptions interface and all callers - Add missing env var warnings in BridgeModule factory - Fix JSON injection in setup-bot.sh: use jq for safe JSON construction Fixes #377 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
213 lines
6.3 KiB
TypeScript
213 lines
6.3 KiB
TypeScript
import { Test, TestingModule } from "@nestjs/testing";
|
|
import { MatrixRoomService } from "./matrix-room.service";
|
|
import { MatrixService } from "./matrix.service";
|
|
import { PrismaService } from "../../prisma/prisma.service";
|
|
import { vi, describe, it, expect, beforeEach } from "vitest";
|
|
|
|
// Mock matrix-bot-sdk to avoid native module import errors
|
|
vi.mock("matrix-bot-sdk", () => {
|
|
return {
|
|
MatrixClient: class MockMatrixClient {},
|
|
SimpleFsStorageProvider: class MockStorageProvider {
|
|
constructor(_filename: string) {
|
|
// No-op for testing
|
|
}
|
|
},
|
|
AutojoinRoomsMixin: {
|
|
setupOnClient: vi.fn(),
|
|
},
|
|
};
|
|
});
|
|
|
|
describe("MatrixRoomService", () => {
|
|
let service: MatrixRoomService;
|
|
|
|
const mockCreateRoom = vi.fn().mockResolvedValue("!new-room:example.com");
|
|
|
|
const mockMatrixClient = {
|
|
createRoom: mockCreateRoom,
|
|
};
|
|
|
|
const mockMatrixService = {
|
|
isConnected: vi.fn().mockReturnValue(true),
|
|
getClient: vi.fn().mockReturnValue(mockMatrixClient),
|
|
};
|
|
|
|
const mockPrismaService = {
|
|
workspace: {
|
|
findUnique: vi.fn(),
|
|
findFirst: vi.fn(),
|
|
update: vi.fn(),
|
|
},
|
|
};
|
|
|
|
beforeEach(async () => {
|
|
process.env.MATRIX_SERVER_NAME = "example.com";
|
|
|
|
const module: TestingModule = await Test.createTestingModule({
|
|
providers: [
|
|
MatrixRoomService,
|
|
{
|
|
provide: PrismaService,
|
|
useValue: mockPrismaService,
|
|
},
|
|
{
|
|
provide: MatrixService,
|
|
useValue: mockMatrixService,
|
|
},
|
|
],
|
|
}).compile();
|
|
|
|
service = module.get<MatrixRoomService>(MatrixRoomService);
|
|
|
|
vi.clearAllMocks();
|
|
// Restore defaults after clearing
|
|
mockMatrixService.isConnected.mockReturnValue(true);
|
|
mockCreateRoom.mockResolvedValue("!new-room:example.com");
|
|
mockPrismaService.workspace.update.mockResolvedValue({});
|
|
});
|
|
|
|
describe("provisionRoom", () => {
|
|
it("should create a Matrix room and store the mapping", async () => {
|
|
const roomId = await service.provisionRoom(
|
|
"workspace-uuid-1",
|
|
"My Workspace",
|
|
"my-workspace"
|
|
);
|
|
|
|
expect(roomId).toBe("!new-room:example.com");
|
|
|
|
expect(mockCreateRoom).toHaveBeenCalledWith({
|
|
name: "Mosaic: My Workspace",
|
|
room_alias_name: "mosaic-my-workspace",
|
|
topic: "Mosaic workspace: My Workspace",
|
|
preset: "private_chat",
|
|
visibility: "private",
|
|
});
|
|
|
|
expect(mockPrismaService.workspace.update).toHaveBeenCalledWith({
|
|
where: { id: "workspace-uuid-1" },
|
|
data: { matrixRoomId: "!new-room:example.com" },
|
|
});
|
|
});
|
|
|
|
it("should return null when Matrix is not configured (no MatrixService)", async () => {
|
|
// Create a service without MatrixService
|
|
const module: TestingModule = await Test.createTestingModule({
|
|
providers: [
|
|
MatrixRoomService,
|
|
{
|
|
provide: PrismaService,
|
|
useValue: mockPrismaService,
|
|
},
|
|
],
|
|
}).compile();
|
|
|
|
const serviceWithoutMatrix = module.get<MatrixRoomService>(MatrixRoomService);
|
|
|
|
const roomId = await serviceWithoutMatrix.provisionRoom(
|
|
"workspace-uuid-1",
|
|
"My Workspace",
|
|
"my-workspace"
|
|
);
|
|
|
|
expect(roomId).toBeNull();
|
|
expect(mockCreateRoom).not.toHaveBeenCalled();
|
|
expect(mockPrismaService.workspace.update).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("should return null when Matrix is not connected", async () => {
|
|
mockMatrixService.isConnected.mockReturnValue(false);
|
|
|
|
const roomId = await service.provisionRoom(
|
|
"workspace-uuid-1",
|
|
"My Workspace",
|
|
"my-workspace"
|
|
);
|
|
|
|
expect(roomId).toBeNull();
|
|
expect(mockCreateRoom).not.toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
describe("getRoomForWorkspace", () => {
|
|
it("should return the room ID for a mapped workspace", async () => {
|
|
mockPrismaService.workspace.findUnique.mockResolvedValue({
|
|
matrixRoomId: "!mapped-room:example.com",
|
|
});
|
|
|
|
const roomId = await service.getRoomForWorkspace("workspace-uuid-1");
|
|
|
|
expect(roomId).toBe("!mapped-room:example.com");
|
|
expect(mockPrismaService.workspace.findUnique).toHaveBeenCalledWith({
|
|
where: { id: "workspace-uuid-1" },
|
|
select: { matrixRoomId: true },
|
|
});
|
|
});
|
|
|
|
it("should return null for an unmapped workspace", async () => {
|
|
mockPrismaService.workspace.findUnique.mockResolvedValue({
|
|
matrixRoomId: null,
|
|
});
|
|
|
|
const roomId = await service.getRoomForWorkspace("workspace-uuid-2");
|
|
|
|
expect(roomId).toBeNull();
|
|
});
|
|
|
|
it("should return null for a non-existent workspace", async () => {
|
|
mockPrismaService.workspace.findUnique.mockResolvedValue(null);
|
|
|
|
const roomId = await service.getRoomForWorkspace("non-existent-uuid");
|
|
|
|
expect(roomId).toBeNull();
|
|
});
|
|
});
|
|
|
|
describe("getWorkspaceForRoom", () => {
|
|
it("should return the workspace ID for a mapped room", async () => {
|
|
mockPrismaService.workspace.findFirst.mockResolvedValue({
|
|
id: "workspace-uuid-1",
|
|
});
|
|
|
|
const workspaceId = await service.getWorkspaceForRoom("!mapped-room:example.com");
|
|
|
|
expect(workspaceId).toBe("workspace-uuid-1");
|
|
expect(mockPrismaService.workspace.findFirst).toHaveBeenCalledWith({
|
|
where: { matrixRoomId: "!mapped-room:example.com" },
|
|
select: { id: true },
|
|
});
|
|
});
|
|
|
|
it("should return null for an unmapped room", async () => {
|
|
mockPrismaService.workspace.findFirst.mockResolvedValue(null);
|
|
|
|
const workspaceId = await service.getWorkspaceForRoom("!unknown-room:example.com");
|
|
|
|
expect(workspaceId).toBeNull();
|
|
});
|
|
});
|
|
|
|
describe("linkWorkspaceToRoom", () => {
|
|
it("should store the room mapping in the workspace", async () => {
|
|
await service.linkWorkspaceToRoom("workspace-uuid-1", "!existing-room:example.com");
|
|
|
|
expect(mockPrismaService.workspace.update).toHaveBeenCalledWith({
|
|
where: { id: "workspace-uuid-1" },
|
|
data: { matrixRoomId: "!existing-room:example.com" },
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("unlinkWorkspace", () => {
|
|
it("should remove the room mapping from the workspace", async () => {
|
|
await service.unlinkWorkspace("workspace-uuid-1");
|
|
|
|
expect(mockPrismaService.workspace.update).toHaveBeenCalledWith({
|
|
where: { id: "workspace-uuid-1" },
|
|
data: { matrixRoomId: null },
|
|
});
|
|
});
|
|
});
|
|
});
|