205 lines
No EOL
9.6 KiB
TypeScript
205 lines
No EOL
9.6 KiB
TypeScript
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",
|
|
});
|
|
});
|
|
}); |