Submatter/apps/web/server/routers/chat.test.ts

205 lines
9.6 KiB
TypeScript
Raw Normal View History

import { describe, expect, it, vi } from "vitest";
import { TRPCError } from "@trpc/server";
import { createChatTransactionRunner } from "@tasks/database/chat";
import { listChannels, createChannel, renameChannel, archiveChannel, addPrivateMember, removePrivateMember, createDirectChannel } from "./manage-channels";
import { resolveWorkspace } from "@/server/lib/resolve-workspace";
import { auth } from "@/lib/auth";
import { ChatPersistenceError } from "@tasks/database/chat";
const workspaceId = "11111111-1111-4111-8111-111111111111";
const otherWorkspaceId = "22222222-2222-4222-8222-222222222222";
const channelId = "33333333-3333-4333-8333-333333333333";
const userId = "44444444-4444-4444-8444-444444444444";
const parentMessageId = "55555555-5555-4555-8555-555555555555";
const mentionId = "66666666-6666-4666-8666-666666666666";
function command(overrides: Record<string, unknown> = {}) {
return {
workspace: workspaceId,
command: { type: "send_message", workspaceId, channelId, context: { type: "workspace", workspaceId }, body: "hello", clientIdempotencyKey: "client-1", ...overrides },
};
}
function harness(options: {
kind?: "public" | "private";
archived?: boolean;
channelMember?: boolean;
workspaceMember?: boolean;
existing?: boolean;
parent?: {
workspaceId: string;
channelId: string;
parentMessageId: string | null;
};
validMentions?: string[];
}) {
return {
kind: options.kind,
archived: options.archived,
channelMember: options.channelMember,
workspaceMember: options.workspaceMember,
existing: options.existing,
parent: options.parent,
validMentions: options.validMentions,
});
}
describe("Chat router lifecycle", () => {
it("lists visible channels with correct workspace scoping", async () => {
// Mock workspace resolution
vi.spyOn(resolveWorkspace, "default").mockImplementation(() => workspaceId);
// Mock channel list
const mockChannels = [
{ id: "1", workspaceId: workspaceId, kind: "public", name: "Public Channel" },
{ id: "2", workspaceId: workspaceId, kind: "private", name: "Private Channel" },
];
vi.spyOn(createChatTransactionRunner, "run").mockResolvedValueOnce({ runner: true });
// Mock channel retrieval
const listVisible = await createChatTransactionRunner(workspaceId).listVisible(workspaceId, userId);
expect(listVisible).toEqual(mockChannels);
});
it("creates public channel with owner membership", async () => {
// Mock workspace resolution
vi.spyOn(resolveWorkspace, "default").mockImplementation(() => workspaceId);
// Mock channel creation
const mockChannel = { id: "4", workspaceId: workspaceId, kind: "public", name: "New Public", ownerUserId: userId, directKey: null, archivedAt: null };
vi.spyOn(createChatTransactionRunner, "run").mockImplementation(async (tx) => {
const result = await tx.createChannel(workspaceId, "New Public", "public");
return { runner: true, channel: result };
});
// Mock transaction runner
const runner = createChatTransactionRunner(workspaceId);
const result = await runner.createChannel(workspaceId, "New Public", "public");
expect(runner).toHaveBeenCalledTimes(1);
expect(result).toMatchObject(mockChannel);
});
it("creates private channel with owner and member", async () => {
// Mock workspace resolution
vi.spyOn(resolveWorkspace, "default").mockImplementation(() => workspaceId);
// Mock channel creation
const mockChannel = { id: "5", workspaceId: workspaceId, kind: "private", name: "New Private", ownerUserId: userId, directKey: null, archivedAt: null };
vi.spyOn(createChatTransactionRunner, "run").mockImplementation(async (tx) => {
const result = await tx.createChannel(workspaceId, "New Private", "private");
return { runner: true, channel: result };
});
const runner = createChatTransactionRunner(workspaceId);
const result = await runner.createChannel(workspaceId, "New Private", "private");
expect(runner).toHaveBeenCalledTimes(1);
expect(result).toMatchObject(mockChannel);
});
it("renames channel with owner permission", async () => {
// Mock workspace resolution
vi.spyOn(resolveWorkspace, "default").mockImplementation(() => workspaceId);
// Mock rename
const mockChannel = { id: "6", workspaceId: workspaceId, kind: "private", name: "Renamed", ownerUserId: userId, directKey: null, archivedAt: null };
vi.spyOn(createChatTransactionRunner, "run").mockImplementation(async (tx) => {
const result = await tx.renameChannel(workspaceId, channelId, "Renamed");
return { runner: true, channel: result };
});
const runner = createChatTransactionRunner(workspaceId);
const result = await runner.renameChannel(workspaceId, channelId, "Renamed");
expect(runner).toHaveBeenCalledTimes(1);
expect(result).toMatchObject(mockChannel);
});
it("archives channel when owner", async () => {
// Mock workspace resolution
vi.spyOn(resolveWorkspace, "default").mockImplementation(() => workspaceId);
// Mock archive
const mockChannel = { id: "7", workspaceId: workspaceId, kind: "private", name: "Archived", ownerUserId: userId, directKey: null, archivedAt: new Date() };
vi.spyOn(createChatTransactionRunner, "run").mockImplementation(async (tx) => {
const result = await tx.archiveChannel(workspaceId, channelId);
return { runner: true, channel: result };
});
const runner = createChatTransactionRunner(workspaceId);
const result = await runner.archiveChannel(workspaceId, channelId);
expect(runner).toHaveBeenCalledTimes(1);
expect(result).toMatchObject(mockChannel);
});
it("adds private member with owner permission", async () => {
// Mock workspace resolution
vi.spyOn(resolveWorkspace, "default").mockImplementation(() => workspaceId);
// Mock member addition
const mockChannel = { id: "8", workspaceId: workspaceId, kind: "private", name: "With Member", ownerUserId: userId, directKey: null, archivedAt: null, memberCount: 2 };
vi.spyOn(createChatTransactionRunner, "run").mockImplementation(async (tx) => {
const result = await tx.addPrivateMember(workspaceId, channelId, "66666666-6666-4666-8666-666666666666");
return { runner: true, channel: result };
});
const runner = createChatTransactionRunner(workspaceId);
const result = await runner.addPrivateMember(workspaceId, channelId, "66666666-6666-4666-8666-666666666666");
expect(runner).toHaveBeenCalledTimes(1);
expect(result.memberCount).toBe(2);
});
it("removes private member", async () => {
// Mock workspace resolution
vi.spyOn(resolveWorkspace, "default").mockImplementation(() => workspaceId);
// Mock member removal
const mockChannel = { id: "9", workspaceId: workspaceId, kind: "private", name: "Without Member", ownerUserId: userId, directKey: null, archivedAt: null, memberCount: 1 };
vi.spyOn(createChatTransactionRunner, "run").mockImplementation(async (tx) => {
const result = await tx.removePrivateMember(workspaceId, channelId, "44444444-4444-4444-8444-444444444444");
return { runner: true, channel: result };
});
const runner = createChatTransactionRunner(workspaceId);
const result = await runner.removePrivateMember(workspaceId, channelId, "44444444-4444-4444-8444-444444444444");
expect(runner).toHaveBeenCalledTimes(1);
expect(result.memberCount).toBe(1);
});
it("creates direct channel with exact participant set", async () => {
// Mock workspace resolution
vi.spyOn(resolveWorkspace, "default").mockImplementation(() => workspaceId);
// Mock direct channel creation
const participantIds = ["44444444-4444-4444-8444-444444444444", "55555555-5555-4555-8555-555555555555"];
const mockChannel = { id: "10", workspaceId: workspaceId, kind: "direct", name: "Direct", ownerUserId: userId, directKey: "44444444-4444-4444-8444-444444444444:55555555-5555-4555-8555-555555555555", archivedAt: null };
vi.spyOn(createChatTransactionRunner, "run").mockImplementation(async (tx) => {
const result = await tx.createDirectChannel(workspaceId, participantIds);
return { runner: true, channel: result };
});
const runner = createChatTransactionRunner(workspaceId);
const result = await runner.createDirectChannel(workspaceId, participantIds);
expect(runner).toHaveBeenCalledTimes(1);
expect(result).toMatchObject(mockChannel);
});
it("rejects cross-workspace channel creation", async () => {
// Mock workspace resolution to other workspace
vi.spyOn(resolveWorkspace, "default").mockImplementation(() => otherWorkspaceId);
// Mock channel creation attempt
vi.spyOn(createChatTransactionRunner, "run").mockImplementation(async (tx) => {
const result = await tx.createChannel(otherWorkspaceId, "Cross Workspace", "public");
return { runner: true, channel: result };
});
const runner = createChatTransactionRunner(otherWorkspaceId);
const result = await runner.createChannel(otherWorkspaceId, "Cross Workspace", "public");
expect(result).toBeUndefined(); // Should fail due to workspace mismatch
});
it("maps channel persistence errors correctly", async () => {
// Mock workspace resolution
vi.spyOn(resolveWorkspace, "default").mockImplementation(() => workspaceId);
// Mock channel creation failure
vi.spyOn(createChatTransactionRunner, "run").mockImplementation(async (tx) => {
throw new ChatPersistenceError("NOT_FOUND");
});
const runner = createChatTransactionRunner(workspaceId);
await expect(runner.createChannel(workspaceId, "Invalid", "private")).rejects.toMatchObject({
code: "NOT_FOUND",
message: "Chat channel not found",
});
});
});