import { describe, it, expect, vi, beforeEach } from "vitest"; import { TRPCError } from "@trpc/server"; // Mock all imports used by board.ts before importing the router vi.mock("@kan/db/repository/board.repo", () => ({ getBoardForMove: vi.fn(), isBoardSlugAvailable: vi.fn(), moveToWorkspace: vi.fn(), getIdByPublicId: vi.fn(), getByPublicId: vi.fn(), getWithListIdsByPublicId: vi.fn(), getWithLatestListIndexByPublicId: vi.fn(), getWorkspaceAndBoardIdByBoardPublicId: vi.fn(), create: vi.fn(), update: vi.fn(), updatePositions: vi.fn(), archive: vi.fn(), deleteBoard: vi.fn(), getAllByWorkspaceId: vi.fn(), createFavorite: vi.fn(), deleteFavorite: vi.fn(), getFavorite: vi.fn(), })); vi.mock("@kan/db/repository/workspace.repo", () => ({ getByPublicId: vi.fn(), })); vi.mock("@kan/db/repository/card.repo", () => ({ getByPublicId: vi.fn(), create: vi.fn(), update: vi.fn(), })); vi.mock("@kan/db/repository/cardActivity.repo", () => ({ create: vi.fn(), })); vi.mock("@kan/db/repository/label.repo", () => ({ create: vi.fn(), getById: vi.fn(), getByPublicId: vi.fn(), })); vi.mock("@kan/db/repository/list.repo", () => ({ create: vi.fn(), getByPublicId: vi.fn(), })); vi.mock("../utils/permissions", () => ({ assertCanEdit: vi.fn(), assertCanDelete: vi.fn(), assertPermission: vi.fn(), })); vi.mock("@kan/shared/utils", () => ({ generateSlug: vi.fn((name: string) => name.toLowerCase().replace(/\s+/g, "-")), generateUID: vi.fn(() => "abc123"), generateAvatarUrl: vi.fn(), convertDueDateFiltersToRanges: vi.fn(), })); vi.mock("@kan/shared/constants", () => ({ colours: [], })); import * as boardRepo from "@kan/db/repository/board.repo"; import * as workspaceRepo from "@kan/db/repository/workspace.repo"; import { assertCanEdit, assertPermission } from "../utils/permissions"; const mockGetBoardForMove = boardRepo.getBoardForMove as ReturnType; const mockIsBoardSlugAvailable = boardRepo.isBoardSlugAvailable as ReturnType; const mockMoveToWorkspace = boardRepo.moveToWorkspace as ReturnType; const mockWorkspaceGetByPublicId = workspaceRepo.getByPublicId as ReturnType; const mockAssertCanEdit = assertCanEdit as ReturnType; const mockAssertPermission = assertPermission as ReturnType; describe("board.move", () => { const mockDb = {} as never; const mockUser = { id: "user-123", name: "Test User", email: "test@example.com" }; const mockInput = { boardPublicId: "brd-123456789", targetWorkspacePublicId: "ws-target-789", }; const mockBoard = { id: 1, name: "My Board", slug: "my-board", type: "board" as const, isArchived: false, workspaceId: 10, createdBy: "user-123", }; const mockTargetWorkspace = { id: 20, publicId: "ws-target-789" }; beforeEach(() => { vi.clearAllMocks(); mockAssertCanEdit.mockResolvedValue(undefined); mockAssertPermission.mockResolvedValue(undefined); }); it("throws UNAUTHORIZED when user is not authenticated", async () => { const { boardRouter } = await import("./board"); const ctx = { user: null, db: mockDb } as never; await expect( boardRouter.createCaller(ctx).move(mockInput), ).rejects.toThrow(TRPCError); }); it("throws NOT_FOUND when board does not exist", async () => { const { boardRouter } = await import("./board"); mockGetBoardForMove.mockResolvedValueOnce(null); const ctx = { user: mockUser, db: mockDb } as never; await expect( boardRouter.createCaller(ctx).move(mockInput), ).rejects.toThrow(TRPCError); }); it("throws BAD_REQUEST for template boards", async () => { const { boardRouter } = await import("./board"); mockGetBoardForMove.mockResolvedValueOnce({ ...mockBoard, type: "template" }); const ctx = { user: mockUser, db: mockDb } as never; await expect( boardRouter.createCaller(ctx).move(mockInput), ).rejects.toThrow(TRPCError); }); it("throws BAD_REQUEST for archived boards", async () => { const { boardRouter } = await import("./board"); mockGetBoardForMove.mockResolvedValueOnce({ ...mockBoard, isArchived: true }); const ctx = { user: mockUser, db: mockDb } as never; await expect( boardRouter.createCaller(ctx).move(mockInput), ).rejects.toThrow(TRPCError); }); it("checks board:edit permission on source workspace", async () => { const { boardRouter } = await import("./board"); mockGetBoardForMove.mockResolvedValueOnce(mockBoard); mockAssertCanEdit.mockRejectedValueOnce( new TRPCError({ code: "FORBIDDEN", message: "No permission" }), ); const ctx = { user: mockUser, db: mockDb } as never; await expect( boardRouter.createCaller(ctx).move(mockInput), ).rejects.toThrow(TRPCError); expect(mockAssertCanEdit).toHaveBeenCalledWith( mockDb, mockUser.id, mockBoard.workspaceId, "board:edit", mockBoard.createdBy, ); }); it("throws NOT_FOUND when target workspace does not exist", async () => { const { boardRouter } = await import("./board"); mockGetBoardForMove.mockResolvedValueOnce(mockBoard); mockWorkspaceGetByPublicId.mockResolvedValueOnce(null); const ctx = { user: mockUser, db: mockDb } as never; await expect( boardRouter.createCaller(ctx).move(mockInput), ).rejects.toThrow(TRPCError); }); it("throws NOT_FOUND when target workspace is soft-deleted", async () => { const { boardRouter } = await import("./board"); mockGetBoardForMove.mockResolvedValueOnce(mockBoard); mockWorkspaceGetByPublicId.mockResolvedValueOnce({ ...mockTargetWorkspace, deletedAt: new Date(), }); const ctx = { user: mockUser, db: mockDb } as never; await expect( boardRouter.createCaller(ctx).move(mockInput), ).rejects.toThrow(TRPCError); }); it("throws BAD_REQUEST when target is the same workspace", async () => { const { boardRouter } = await import("./board"); mockGetBoardForMove.mockResolvedValueOnce(mockBoard); mockWorkspaceGetByPublicId.mockResolvedValueOnce({ id: mockBoard.workspaceId, publicId: "ws-target-789", }); const ctx = { user: mockUser, db: mockDb } as never; await expect( boardRouter.createCaller(ctx).move(mockInput), ).rejects.toThrow(TRPCError); }); it("checks board:create permission on target workspace", async () => { const { boardRouter } = await import("./board"); mockGetBoardForMove.mockResolvedValueOnce(mockBoard); mockWorkspaceGetByPublicId.mockResolvedValueOnce(mockTargetWorkspace); mockAssertPermission.mockRejectedValueOnce( new TRPCError({ code: "FORBIDDEN", message: "No permission" }), ); const ctx = { user: mockUser, db: mockDb } as never; await expect( boardRouter.createCaller(ctx).move(mockInput), ).rejects.toThrow(TRPCError); expect(mockAssertPermission).toHaveBeenCalledWith( mockDb, mockUser.id, mockTargetWorkspace.id, "board:create", ); }); it("appends UID suffix when slug conflicts in target workspace", async () => { const { boardRouter } = await import("./board"); mockGetBoardForMove.mockResolvedValueOnce(mockBoard); mockWorkspaceGetByPublicId.mockResolvedValueOnce(mockTargetWorkspace); mockIsBoardSlugAvailable.mockResolvedValueOnce(false); mockMoveToWorkspace.mockResolvedValueOnce(undefined); const ctx = { user: mockUser, db: mockDb } as never; await boardRouter.createCaller(ctx).move(mockInput); expect(mockMoveToWorkspace).toHaveBeenCalledWith( mockDb, mockBoard.id, mockTargetWorkspace.id, "my-board-abc123", ); }); it("moves board successfully with available slug", async () => { const { boardRouter } = await import("./board"); mockGetBoardForMove.mockResolvedValueOnce(mockBoard); mockWorkspaceGetByPublicId.mockResolvedValueOnce(mockTargetWorkspace); mockIsBoardSlugAvailable.mockResolvedValueOnce(true); mockMoveToWorkspace.mockResolvedValueOnce(undefined); const ctx = { user: mockUser, db: mockDb } as never; const result = await boardRouter.createCaller(ctx).move(mockInput); expect(result).toEqual({ success: true }); expect(mockMoveToWorkspace).toHaveBeenCalledWith( mockDb, mockBoard.id, mockTargetWorkspace.id, "my-board", ); }); });