Skip to main content
Glama

Todoist MCP Server

dry-run-wrapper.test.ts14.2 kB
import { TodoistApi } from "@doist/todoist-api-typescript"; import { DryRunWrapper, createTodoistClient, } from "../utils/dry-run-wrapper.js"; import type { Task, Label, Section, Comment, PersonalProject, } from "@doist/todoist-api-typescript"; // Mock the TodoistApi jest.mock("@doist/todoist-api-typescript"); describe("DryRunWrapper", () => { let mockTodoistApi: jest.Mocked<TodoistApi>; let dryRunWrapper: DryRunWrapper; let originalEnv: string | undefined; const mockTask: Task = { url: "https://todoist.com/showTask?id=123", id: "123", userId: "user123", projectId: "456", sectionId: null, parentId: null, addedByUid: null, assignedByUid: null, responsibleUid: null, labels: [], deadline: null, duration: null, checked: false, isDeleted: false, addedAt: "2023-01-01T00:00:00Z", completedAt: null, updatedAt: null, due: null, priority: 4, childOrder: 1, content: "Test task", description: "Test description", noteCount: 0, dayOrder: 1, isCollapsed: false, }; const mockProject: PersonalProject = { url: "https://todoist.com/app/project/456", id: "456", canAssignTasks: false, childOrder: 1, color: "blue", createdAt: "2023-01-01T00:00:00Z", isArchived: false, isDeleted: false, isFavorite: false, isFrozen: false, name: "Test Project", updatedAt: null, viewStyle: "list", defaultOrder: 1, description: "", isCollapsed: false, isShared: false, parentId: null, inboxProject: false, }; const mockLabel: Label = { id: "789", order: 1, name: "Test Label", color: "red", isFavorite: false, }; const mockSection: Section = { url: "https://todoist.com/app/project/456/section/101", id: "101", userId: "user123", projectId: "456", addedAt: "2023-01-01T00:00:00Z", updatedAt: "2023-01-01T00:00:00Z", archivedAt: null, name: "Test Section", sectionOrder: 1, isArchived: false, isDeleted: false, isCollapsed: false, }; const mockComment: Comment = { taskId: "123", id: "202", content: "Test comment", postedAt: "2023-01-01T00:00:00Z", fileAttachment: null, postedUid: "user123", uidsToNotify: null, reactions: null, isDeleted: false, projectId: undefined, }; beforeEach(() => { originalEnv = process.env.DRYRUN; process.env.DRYRUN = "true"; // Create a fresh mock for each test mockTodoistApi = new TodoistApi("fake-token") as jest.Mocked<TodoistApi>; dryRunWrapper = new DryRunWrapper(mockTodoistApi); // Setup common mocks mockTodoistApi.getTask.mockResolvedValue(mockTask); mockTodoistApi.getProject.mockResolvedValue(mockProject); mockTodoistApi.getLabel.mockResolvedValue(mockLabel); mockTodoistApi.getSection.mockResolvedValue(mockSection); mockTodoistApi.getComment.mockResolvedValue(mockComment); // Mock console.error to capture dry-run messages jest.spyOn(console, "error").mockImplementation(); }); afterEach(() => { process.env.DRYRUN = originalEnv; jest.restoreAllMocks(); }); describe("Task operations", () => { describe("addTask", () => { it("should pass through to real API when dry-run is disabled", async () => { process.env.DRYRUN = "false"; const wrapper = new DryRunWrapper(mockTodoistApi); mockTodoistApi.addTask.mockResolvedValue(mockTask); const result = await wrapper.addTask({ content: "New task" }); expect(mockTodoistApi.addTask).toHaveBeenCalledWith({ content: "New task", }); expect(result).toEqual(mockTask); expect(console.error).not.toHaveBeenCalled(); }); it("should simulate task creation in dry-run mode", async () => { const result = await dryRunWrapper.addTask({ content: "New task", projectId: "456", priority: 2, }); expect(mockTodoistApi.addTask).not.toHaveBeenCalled(); expect(mockTodoistApi.getProject).toHaveBeenCalledWith("456"); expect(console.error).toHaveBeenCalledWith( expect.stringContaining( '[DRY-RUN] Would create task: "New task" in project 456' ) ); expect(result.content).toBe("New task"); expect(result.priority).toBe(2); expect(result.id).toBeDefined(); }); it("should validate project exists in dry-run mode", async () => { mockTodoistApi.getProject.mockRejectedValue( new Error("Project not found") ); await expect( dryRunWrapper.addTask({ content: "New task", projectId: "nonexistent", }) ).rejects.toThrow("Project with ID nonexistent does not exist"); }); it("should validate parent task exists in dry-run mode", async () => { mockTodoistApi.getTask.mockRejectedValue(new Error("Task not found")); await expect( dryRunWrapper.addTask({ content: "New subtask", parentId: "nonexistent", }) ).rejects.toThrow("Task with ID nonexistent does not exist"); }); }); describe("updateTask", () => { it("should pass through to real API when dry-run is disabled", async () => { process.env.DRYRUN = "false"; const wrapper = new DryRunWrapper(mockTodoistApi); const updatedTask = { ...mockTask, content: "Updated task" }; mockTodoistApi.updateTask.mockResolvedValue(updatedTask); const result = await wrapper.updateTask("123", { content: "Updated task", }); expect(mockTodoistApi.updateTask).toHaveBeenCalledWith("123", { content: "Updated task", }); expect(result).toEqual(updatedTask); }); it("should simulate task update in dry-run mode", async () => { const result = await dryRunWrapper.updateTask("123", { content: "Updated task", priority: 1, }); expect(mockTodoistApi.updateTask).not.toHaveBeenCalled(); expect(mockTodoistApi.getTask).toHaveBeenCalledWith("123"); expect(console.error).toHaveBeenCalledWith( expect.stringContaining("[DRY-RUN] Would update task: ID 123") ); expect(result.content).toBe("Updated task"); expect(result.priority).toBe(1); expect(result.id).toBe("123"); }); it("should validate task exists in dry-run mode", async () => { mockTodoistApi.getTask.mockRejectedValue(new Error("Task not found")); await expect( dryRunWrapper.updateTask("nonexistent", { content: "Updated task" }) ).rejects.toThrow("Task with ID nonexistent does not exist"); }); }); describe("deleteTask", () => { it("should pass through to real API when dry-run is disabled", async () => { process.env.DRYRUN = "false"; const wrapper = new DryRunWrapper(mockTodoistApi); mockTodoistApi.deleteTask.mockResolvedValue(true); const result = await wrapper.deleteTask("123"); expect(mockTodoistApi.deleteTask).toHaveBeenCalledWith("123"); expect(result).toBe(true); }); it("should simulate task deletion in dry-run mode", async () => { const result = await dryRunWrapper.deleteTask("123"); expect(mockTodoistApi.deleteTask).not.toHaveBeenCalled(); expect(mockTodoistApi.getTask).toHaveBeenCalledWith("123"); expect(console.error).toHaveBeenCalledWith( expect.stringContaining("[DRY-RUN] Would delete task: ID 123") ); expect(result).toBe(true); }); }); describe("closeTask", () => { it("should simulate task completion in dry-run mode", async () => { const result = await dryRunWrapper.closeTask("123"); expect(mockTodoistApi.closeTask).not.toHaveBeenCalled(); expect(console.error).toHaveBeenCalledWith( expect.stringContaining("[DRY-RUN] Would complete task: ID 123") ); expect(result).toBe(true); }); }); describe("reopenTask", () => { it("should simulate task reopening in dry-run mode", async () => { const result = await dryRunWrapper.reopenTask("123"); expect(mockTodoistApi.reopenTask).not.toHaveBeenCalled(); expect(console.error).toHaveBeenCalledWith( expect.stringContaining("[DRY-RUN] Would reopen task: ID 123") ); expect(result).toBe(true); }); }); }); describe("Project operations", () => { describe("addProject", () => { it("should simulate project creation in dry-run mode", async () => { const result = await dryRunWrapper.addProject({ name: "New Project", color: "green", isFavorite: true, }); expect(mockTodoistApi.addProject).not.toHaveBeenCalled(); expect(console.error).toHaveBeenCalledWith( expect.stringContaining( '[DRY-RUN] Would create project: "New Project" with color green, favorite: true' ) ); expect(result.name).toBe("New Project"); expect(result.color).toBe("green"); expect(result.isFavorite).toBe(true); expect(result.id).toBeDefined(); }); }); describe("updateProject", () => { it("should simulate project update in dry-run mode", async () => { const result = await dryRunWrapper.updateProject("456", { name: "Updated Project", }); expect(mockTodoistApi.updateProject).not.toHaveBeenCalled(); expect(mockTodoistApi.getProject).toHaveBeenCalledWith("456"); expect(console.error).toHaveBeenCalledWith( expect.stringContaining("[DRY-RUN] Would update project: ID 456") ); expect(result.name).toBe("Updated Project"); }); }); describe("deleteProject", () => { it("should simulate project deletion in dry-run mode", async () => { const result = await dryRunWrapper.deleteProject("456"); expect(mockTodoistApi.deleteProject).not.toHaveBeenCalled(); expect(console.error).toHaveBeenCalledWith( expect.stringContaining("[DRY-RUN] Would delete project: ID 456") ); expect(result).toBe(true); }); }); }); describe("Label operations", () => { describe("addLabel", () => { it("should simulate label creation in dry-run mode", async () => { const result = await dryRunWrapper.addLabel({ name: "New Label", color: "purple", isFavorite: true, }); expect(mockTodoistApi.addLabel).not.toHaveBeenCalled(); expect(console.error).toHaveBeenCalledWith( expect.stringContaining( '[DRY-RUN] Would create label: "New Label" with color purple, favorite: true' ) ); expect(result.name).toBe("New Label"); expect(result.color).toBe("purple"); expect(result.isFavorite).toBe(true); }); }); describe("updateLabel", () => { it("should simulate label update in dry-run mode", async () => { const result = await dryRunWrapper.updateLabel("789", { name: "Updated Label", }); expect(mockTodoistApi.updateLabel).not.toHaveBeenCalled(); expect(mockTodoistApi.getLabel).toHaveBeenCalledWith("789"); expect(console.error).toHaveBeenCalledWith( expect.stringContaining("[DRY-RUN] Would update label: ID 789") ); expect(result.name).toBe("Updated Label"); }); }); describe("deleteLabel", () => { it("should simulate label deletion in dry-run mode", async () => { const result = await dryRunWrapper.deleteLabel("789"); expect(mockTodoistApi.deleteLabel).not.toHaveBeenCalled(); expect(console.error).toHaveBeenCalledWith( expect.stringContaining("[DRY-RUN] Would delete label: ID 789") ); expect(result).toBe(true); }); }); }); describe("Read operations", () => { it("should pass through all read operations to the real API", async () => { mockTodoistApi.getTasks.mockResolvedValue([mockTask] as any); mockTodoistApi.getProjects.mockResolvedValue([mockProject] as any); mockTodoistApi.getLabels.mockResolvedValue([mockLabel] as any); const tasks = await dryRunWrapper.getTasks(); const projects = await dryRunWrapper.getProjects(); const labels = await dryRunWrapper.getLabels(); expect(mockTodoistApi.getTasks).toHaveBeenCalled(); expect(mockTodoistApi.getProjects).toHaveBeenCalled(); expect(mockTodoistApi.getLabels).toHaveBeenCalled(); expect(tasks).toEqual([mockTask]); expect(projects).toEqual([mockProject]); expect(labels).toEqual([mockLabel]); }); }); }); describe("createTodoistClient", () => { let originalEnv: string | undefined; beforeEach(() => { originalEnv = process.env.DRYRUN; jest.spyOn(console, "error").mockImplementation(); }); afterEach(() => { process.env.DRYRUN = originalEnv; jest.restoreAllMocks(); }); it("should return unwrapped TodoistApi when dry-run is disabled", () => { process.env.DRYRUN = "false"; const client = createTodoistClient("test-token"); expect(client).toBeInstanceOf(TodoistApi); expect(client).not.toBeInstanceOf(DryRunWrapper); expect(console.error).not.toHaveBeenCalled(); }); it("should return unwrapped TodoistApi when DRYRUN is not set", () => { delete process.env.DRYRUN; const client = createTodoistClient("test-token"); expect(client).toBeInstanceOf(TodoistApi); expect(client).not.toBeInstanceOf(DryRunWrapper); expect(console.error).not.toHaveBeenCalled(); }); it("should return wrapped DryRunWrapper when dry-run is enabled", () => { process.env.DRYRUN = "true"; const client = createTodoistClient("test-token"); expect(client).toBeInstanceOf(DryRunWrapper); expect(console.error).toHaveBeenCalledWith( "[DRY-RUN] Dry-run mode enabled - mutations will be simulated" ); }); });

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/greirson/mcp-todoist'

If you have feedback or need assistance with the MCP directory API, please join our Discord server