Submatter/apps/web/server/routers/chat.test.ts
varutasu ff01a5f097
Some checks are pending
CI / Lint, type-check, test (push) Waiting to run
Deploy homelab (CT 116) / Deploy to CT 116 (push) Waiting to run
Make workspace Chat durable and ready for source-built deployment
2026-10-29 19:30:20 +00:00

116 lines
No EOL
4.1 KiB
TypeScript

import { beforeEach, describe, expect, it, vi } from "vitest";
import { db } from "@tasks/database";
import type { Session } from "next-auth";
import {
ChatMutationError,
mutateChatMessage,
createMessageMutationRunner,
chatMutationCatchUp,
} from "@tasks/database/chat";
import { resolveWorkspace } from "@/server/lib/resolve-workspace";
import { createCallerFactory } from "@/server/trpc";
import { chatRouter } from "./chat";
vi.mock("@tasks/database/chat", async (importOriginal) => ({
...(await importOriginal<typeof import("@tasks/database/chat")>()),
mutateChatMessage: vi.fn(),
chatMutationCatchUp: vi.fn(),
createMessageMutationRunner: vi.fn(() => ({ transaction: vi.fn() })),
}));
vi.mock("@/server/lib/resolve-workspace", () => ({
resolveWorkspace: vi.fn(),
}));
vi.mock("@/lib/auth", () => ({ auth: vi.fn() }));
const workspaceId = "11111111-1111-4111-8111-111111111111";
const channelId = "33333333-3333-4333-8333-333333333333";
const userId = "44444444-4444-4444-8444-444444444444";
const messageId = "77777777-7777-4777-8777-777777777777";
const clientMutationId = "66666666-6666-4666-8666-666666666666";
const caller = () =>
createCallerFactory(chatRouter)({
db,
session: { user: { id: userId } } as Session,
});
const command = {
type: "edit_message" as const,
workspaceId,
channelId,
context: { type: "workspace" as const, workspaceId },
messageId,
body: "corrected",
expectedRevision: 2,
clientMutationId,
};
beforeEach(() => {
vi.clearAllMocks();
vi.mocked(resolveWorkspace).mockResolvedValue({
id: workspaceId,
slug: "acme",
name: "Acme",
role: "member",
ownerUserId: userId,
});
});
describe("chat message mutation procedures", () => {
it("delegates validated edit and delete with actor and exact command", async () => {
const result = {
changed: true,
result: { messageId, revision: 3, status: "edited" as const },
};
vi.mocked(mutateChatMessage).mockResolvedValue(result);
await expect(caller().editMessage({ workspace: workspaceId, command })).resolves.toEqual(result);
expect(mutateChatMessage).toHaveBeenCalledWith(expect.anything(), {
actorUserId: userId,
command,
});
});
});
describe("mutation recovery procedure", () => {
const input = { workspace: "acme", channelId, after: "0", limit: 2 };
const result = {
mutations: [{ messageId, mutationSequence: "1", revision: 3, kind: "edited" as const }],
next: "1",
through: "3",
hasMore: true,
};
it("passes the resolved workspace and session actor, and preserves the fixed high water", async () => {
vi.mocked(chatMutationCatchUp).mockResolvedValue(result as Awaited<ReturnType<typeof chatMutationCatchUp>>);
await expect(caller().mutationCatchUp(input)).resolves.toEqual(result);
expect(chatMutationCatchUp).toHaveBeenCalledWith(db, {
workspaceId,
actorUserId: userId,
channelId,
after: "0",
through: undefined,
limit: 2,
});
await caller().mutationCatchUp({ ...input, after: "1", through: "3" });
expect(chatMutationCatchUp).toHaveBeenLastCalledWith(db, {
workspaceId,
actorUserId: userId,
channelId,
after: "1",
through: "3",
limit: 2,
});
});
it("rejects malformed cursors and maps read denial without exposing a channel", async () => {
await expect(caller().mutationCatchUp({ ...input, after: "01" })).rejects.toMatchObject({ code: "BAD_REQUEST" });
await expect(caller().mutationCatchUp({ ...input, through: "0", after: "1" })).rejects.toMatchObject({ code: "BAD_REQUEST" });
expect(chatMutationCatchUp).not.toHaveBeenCalled();
vi.mocked(chatMutationCatchUp).mockRejectedValueOnce(new (await import("@tasks/database/chat")).ChatReadError());
await expect(caller().mutationCatchUp(input)).rejects.toMatchObject({ code: "NOT_FOUND" });
});
it("requires an authenticated workspace member", async () => {
vi.mocked(resolveWorkspace).mockRejectedValueOnce(new Error("not a member"));
await expect(caller().mutationCatchUp(input)).rejects.toThrow();
expect(chatMutationCatchUp).not.toHaveBeenCalled();
});
});