Skip to main content
Glama
task-update.spec.ts25 kB
import axios from 'axios'; import type { MockedFunction } from 'vitest'; import { vi, describe, it, expect, beforeEach } from 'vitest'; import { updateTask, uncompleteTask, moveTask, completeTask, createTask, } from './task-update'; import { getTodoistClient, getTodoistV1Client } from '../client'; import { getTaskName, setTaskName } from '../cache/task-cache'; import { addTaskRenameComment } from './comments'; import { getTaskById } from './task-retrieval'; import { listProjects } from '../projects/projects'; import { isBrianSharedProject } from '../../utils/project-filters'; import { TodoistProject } from '../../types'; // Mock the client module vi.mock('../client'); vi.mock('../cache/task-cache'); vi.mock('./comments'); vi.mock('./task-retrieval'); vi.mock('../projects/projects'); // Don't mock isBrianSharedProject since it's a pure function const mockGetTodoistClient = vi.mocked(getTodoistClient); const mockGetTodoistV1Client = vi.mocked(getTodoistV1Client); const mockGetTaskName = vi.mocked(getTaskName); const mockSetTaskName = vi.mocked(setTaskName); const mockAddTaskRenameComment = vi.mocked(addTaskRenameComment); const mockGetTaskById = vi.mocked(getTaskById); const mockListProjects = vi.mocked(listProjects); describe('Task Update Functions', () => { beforeEach(() => { vi.clearAllMocks(); }); describe('updateTask', () => { it('should update task title successfully', async () => { // arrange const mockClient = { get: vi.fn(), post: vi.fn().mockResolvedValue({ data: { id: '123', content: 'Updated Task Title' }, }), }; mockGetTodoistClient.mockReturnValue(mockClient); // act const result = await updateTask({ taskId: '123', title: 'Updated Task Title', }); // assert expect(result).toContain('Task updated successfully'); expect(mockClient.post).toHaveBeenCalledWith('/tasks/123', { content: 'Updated Task Title', }); }); it('should update task description successfully', async () => { // arrange const mockClient = { get: vi.fn(), post: vi.fn().mockResolvedValue({ data: { id: '123', description: 'Updated description' }, }), }; mockGetTodoistClient.mockReturnValue(mockClient); // act const result = await updateTask({ taskId: '123', description: 'Updated description', }); // assert expect(result).toContain('Task updated successfully'); expect(mockClient.post).toHaveBeenCalledWith('/tasks/123', { description: 'Updated description', }); }); it('should update task labels successfully', async () => { // arrange const mockClient = { get: vi.fn(), post: vi.fn().mockResolvedValue({ data: { id: '123', labels: ['work', 'urgent'] }, }), }; mockGetTodoistClient.mockReturnValue(mockClient); // act const result = await updateTask({ taskId: '123', labels: ['work', 'urgent'], }); // assert expect(result).toContain('Task updated successfully'); expect(mockClient.post).toHaveBeenCalledWith('/tasks/123', { labels: ['work', 'urgent'], }); }); it('should update task priority successfully', async () => { // arrange const mockClient = { get: vi.fn(), post: vi.fn().mockResolvedValue({ data: { id: '123', priority: 3 } }), }; mockGetTodoistClient.mockReturnValue(mockClient); // act const result = await updateTask({ taskId: '123', priority: 3, }); // assert expect(result).toContain('Task updated successfully'); expect(mockClient.post).toHaveBeenCalledWith('/tasks/123', { priority: 3, }); }); it('should update task due string successfully', async () => { // arrange const mockClient = { get: vi.fn(), post: vi.fn().mockResolvedValue({ data: { id: '123', due: { string: 'next Monday' } }, }), }; mockGetTodoistClient.mockReturnValue(mockClient); // act const result = await updateTask({ taskId: '123', dueString: 'next Monday', }); // assert expect(result).toContain('Task updated successfully'); expect(mockClient.post).toHaveBeenCalledWith('/tasks/123', { due_string: 'next Monday', }); }); it('should update multiple fields successfully', async () => { // arrange const mockClient = { get: vi.fn(), post: vi.fn().mockResolvedValue({ data: { id: '123', content: 'Updated Title', description: 'Updated description', priority: 2, }, }), }; mockGetTodoistClient.mockReturnValue(mockClient); // act const result = await updateTask({ taskId: '123', title: 'Updated Title', description: 'Updated description', priority: 2, }); // assert expect(result).toContain('Task updated successfully'); expect(mockClient.post).toHaveBeenCalledWith('/tasks/123', { content: 'Updated Title', description: 'Updated description', priority: 2, }); }); it('should handle API error gracefully', async () => { // arrange const mockClient = { get: vi.fn(), post: vi.fn().mockRejectedValue(new Error('API Error')), }; mockGetTodoistClient.mockReturnValue(mockClient); // act const promise = updateTask({ taskId: '123', title: 'Updated Title', }); // assert await expect(promise).rejects.toThrow('Failed to update task'); }); it('should only include provided fields in the update request', async () => { // arrange const mockClient = { get: vi.fn(), post: vi .fn() .mockResolvedValue({ data: { id: '123', content: 'Updated Title' } }), }; mockGetTodoistClient.mockReturnValue(mockClient); // act const result = await updateTask({ taskId: '123', title: 'Updated Title', // Note: other fields are not provided }); // assert expect(result).toContain('Task updated successfully'); expect(mockClient.post).toHaveBeenCalledWith('/tasks/123', { content: 'Updated Title', }); // Ensure no undefined fields are sent expect(mockClient.post).not.toHaveBeenCalledWith( expect.any(String), expect.objectContaining({ description: undefined, labels: undefined, priority: undefined, due_date: undefined, }) ); }); describe('when updating task title', () => { it('should fetch old title from cache when updating title', async () => { // arrange const mockClient = { get: vi.fn(), post: vi.fn().mockResolvedValue({ data: { id: '123', content: 'New Task Title' }, }), }; mockGetTodoistClient.mockReturnValue(mockClient); mockGetTaskName.mockResolvedValue('Old Task Title'); // act const result = await updateTask({ taskId: '123', title: 'New Task Title', }); // assert expect(mockGetTaskName).toHaveBeenCalledWith('123'); }); it('should create rename comment when title is updated', async () => { // arrange const mockClient = { get: vi.fn(), post: vi.fn().mockResolvedValue({ data: { id: '123', content: 'New Task Title' }, }), }; mockGetTodoistClient.mockReturnValue(mockClient); mockGetTaskName.mockResolvedValue('Old Task Title'); mockAddTaskRenameComment.mockResolvedValue({ id: 1, content: 'Task renamed from `Old Task Title` to `New Task Title`', posted: '2024-01-01T00:00:00Z', posted_uid: 'user123', attachment: null, }); // act const result = await updateTask({ taskId: '123', title: 'New Task Title', }); // assert expect(mockAddTaskRenameComment).toHaveBeenCalledWith( '123', 'Old Task Title', 'New Task Title' ); }); it('should update task cache with new title after successful update', async () => { // arrange const mockClient = { get: vi.fn(), post: vi.fn().mockResolvedValue({ data: { id: '123', content: 'New Task Title' }, }), }; mockGetTodoistClient.mockReturnValue(mockClient); mockGetTaskName.mockResolvedValue('Old Task Title'); mockAddTaskRenameComment.mockResolvedValue({ id: 1, content: 'Task renamed from `Old Task Title` to `New Task Title`', posted: '2024-01-01T00:00:00Z', posted_uid: 'user123', attachment: null, }); // act const result = await updateTask({ taskId: '123', title: 'New Task Title', }); // assert expect(mockSetTaskName).toHaveBeenCalledWith('123', 'New Task Title'); }); it('should not create rename comment when title is not being updated', async () => { // arrange const mockClient = { get: vi.fn(), post: vi.fn().mockResolvedValue({ data: { id: '123', description: 'Updated description' }, }), }; mockGetTodoistClient.mockReturnValue(mockClient); // act const result = await updateTask({ taskId: '123', description: 'Updated description', }); // assert expect(mockAddTaskRenameComment).not.toHaveBeenCalled(); expect(mockGetTaskName).not.toHaveBeenCalled(); expect(mockSetTaskName).not.toHaveBeenCalled(); }); it('should handle cache fetch error gracefully when updating title', async () => { // arrange const mockClient = { get: vi.fn(), post: vi.fn().mockResolvedValue({ data: { id: '123', content: 'New Task Title' }, }), }; mockGetTodoistClient.mockReturnValue(mockClient); mockGetTaskName.mockRejectedValue(new Error('Cache error')); // act const promise = updateTask({ taskId: '123', title: 'New Task Title', }); // assert await expect(promise).rejects.toThrow('Failed to update task'); }); it('should handle comment creation error gracefully when updating title', async () => { // arrange const mockClient = { get: vi.fn(), post: vi.fn().mockResolvedValue({ data: { id: '123', content: 'New Task Title' }, }), }; mockGetTodoistClient.mockReturnValue(mockClient); mockGetTaskName.mockResolvedValue('Old Task Title'); mockAddTaskRenameComment.mockRejectedValue( new Error('Comment creation failed') ); // act const promise = updateTask({ taskId: '123', title: 'New Task Title', }); // assert await expect(promise).rejects.toThrow('Failed to update task'); }); }); }); describe('uncompleteTask', () => { it('should uncomplete a task successfully', async () => { // arrange const mockClient = { get: vi.fn(), post: vi.fn().mockResolvedValue({ data: {} }), }; mockGetTodoistClient.mockReturnValue(mockClient); const taskId = '12345'; // act const result = await uncompleteTask(taskId); // assert expect(result).toBeUndefined(); expect(mockClient.post).toHaveBeenCalledWith(`/tasks/${taskId}/reopen`); }); it('should handle API errors gracefully', async () => { // arrange const mockClient = { get: vi.fn(), post: vi.fn().mockRejectedValue(new Error('API Error')), }; mockGetTodoistClient.mockReturnValue(mockClient); const taskId = '12345'; // act const promise = uncompleteTask(taskId); // assert await expect(promise).rejects.toThrow( 'Failed to uncomplete task: API Error' ); }); it('should handle network errors gracefully', async () => { // arrange const mockClient = { get: vi.fn(), post: vi.fn().mockRejectedValue(new Error('Network Error')), }; mockGetTodoistClient.mockReturnValue(mockClient); const taskId = '12345'; // act const promise = uncompleteTask(taskId); // assert await expect(promise).rejects.toThrow( 'Failed to uncomplete task: Network Error' ); }); }); describe('moveTask', () => { it('should successfully move a task to a new project', async () => { // arrange const mockTaskIdMapping = [ { new_id: 'v1_task_id_123', old_id: 'task_123' }, ]; const mockProjectIdMapping = [ { new_id: 'v1_project_id_456', old_id: 'project_456' }, ]; const mockMoveResponse = { success: true }; const mockV1Client = { get: vi .fn() .mockResolvedValueOnce({ data: mockTaskIdMapping }) .mockResolvedValueOnce({ data: mockProjectIdMapping }), post: vi.fn().mockResolvedValue({ data: mockMoveResponse }), }; mockGetTodoistV1Client.mockReturnValue(mockV1Client); // act await moveTask('task_123', 'project_456'); // assert // Function should complete without throwing an error expect(mockV1Client.get).toHaveBeenCalledWith( '/api/v1/id_mappings/tasks/task_123' ); expect(mockV1Client.get).toHaveBeenCalledWith( '/api/v1/id_mappings/projects/project_456' ); expect(mockV1Client.post).toHaveBeenCalledWith( '/api/v1/tasks/v1_task_id_123/move', { project_id: 'v1_project_id_456', } ); }); it('should handle task ID mapping error', async () => { // arrange const mockV1Client = { get: vi.fn().mockRejectedValue(new Error('Task not found')), post: vi.fn(), }; mockGetTodoistV1Client.mockReturnValue(mockV1Client); // act const promise = moveTask('invalid_task', 'project_456'); // assert await expect(promise).rejects.toThrow( 'Failed to move task: Failed to convert ID: Task not found' ); expect(mockV1Client.get).toHaveBeenCalledWith( '/api/v1/id_mappings/tasks/invalid_task' ); }); it('should handle project ID mapping error', async () => { // arrange const mockTaskIdMapping = [ { new_id: 'v1_task_id_123', old_id: 'task_123' }, ]; const mockV1Client = { get: vi .fn() .mockResolvedValueOnce({ data: mockTaskIdMapping }) .mockRejectedValueOnce(new Error('Project not found')), post: vi.fn(), }; mockGetTodoistV1Client.mockReturnValue(mockV1Client); // act const promise = moveTask('task_123', 'invalid_project'); // assert await expect(promise).rejects.toThrow( 'Failed to move task: Failed to convert ID: Project not found' ); expect(mockV1Client.get).toHaveBeenCalledWith( '/api/v1/id_mappings/tasks/task_123' ); expect(mockV1Client.get).toHaveBeenCalledWith( '/api/v1/id_mappings/projects/invalid_project' ); }); it('should handle move operation error', async () => { // arrange const mockTaskIdMapping = [ { new_id: 'v1_task_id_123', old_id: 'task_123' }, ]; const mockProjectIdMapping = [ { new_id: 'v1_project_id_456', old_id: 'project_456' }, ]; const mockV1Client = { get: vi .fn() .mockResolvedValueOnce({ data: mockTaskIdMapping }) .mockResolvedValueOnce({ data: mockProjectIdMapping }), post: vi.fn().mockRejectedValue(new Error('Move operation failed')), }; mockGetTodoistV1Client.mockReturnValue(mockV1Client); // act const promise = moveTask('task_123', 'project_456'); // assert await expect(promise).rejects.toThrow( 'Failed to move task: Move operation failed' ); expect(mockV1Client.get).toHaveBeenCalledWith( '/api/v1/id_mappings/tasks/task_123' ); expect(mockV1Client.get).toHaveBeenCalledWith( '/api/v1/id_mappings/projects/project_456' ); expect(mockV1Client.post).toHaveBeenCalledWith( '/api/v1/tasks/v1_task_id_123/move', { project_id: 'v1_project_id_456', } ); }); it('should handle axios error responses', async () => { // arrange const axiosError = new axios.AxiosError('API Error'); axiosError.response = { data: { error: 'Invalid task ID' }, status: 400, statusText: 'Bad Request', headers: {}, config: {} as any, }; const mockV1Client = { get: vi.fn().mockRejectedValue(axiosError), post: vi.fn(), }; mockGetTodoistV1Client.mockReturnValue(mockV1Client); // act const promise = moveTask('invalid_task', 'project_456'); // assert await expect(promise).rejects.toThrow( 'Failed to move task: Failed to convert ID: API Error' ); }); }); describe('completeTask', () => { beforeEach(() => { // Set up default mocks for the new service calls mockGetTaskById.mockResolvedValue({ id: '123', project_id: '456', content: 'Test task', description: '', is_completed: false, labels: [], priority: 1, due: null, url: 'https://todoist.com/task/123', comment_count: 0, created_at: '2024-01-01T00:00:00Z', updated_at: '2024-01-01T00:00:00Z', }); mockListProjects.mockResolvedValue({ projects: [ { id: '456', name: 'Personal Project', url: 'https://todoist.com/project/456', is_favorite: false, is_inbox_project: false, } as TodoistProject, ], total_count: 1, }); }); it('should complete a task successfully', async () => { // arrange const mockClient = { get: vi.fn(), post: vi.fn().mockResolvedValue({ data: { ok: true } }), }; mockGetTodoistClient.mockReturnValue(mockClient); // act const result = await completeTask('123'); // assert expect(result).toBe('Task completed successfully'); expect(mockClient.post).toHaveBeenCalledWith('/tasks/123/close'); }); it('should handle API error when completing task', async () => { // arrange const mockClient = { get: vi.fn(), post: vi.fn().mockRejectedValue(new Error('API Error')), }; mockGetTodoistClient.mockReturnValue(mockClient); // act const promise = completeTask('123'); // assert await expect(promise).rejects.toThrow( 'Failed to complete task: API Error' ); }); it('should handle client without post method', async () => { // arrange const mockClient = { get: vi.fn(), }; mockGetTodoistClient.mockReturnValue(mockClient); // act const promise = completeTask('123'); // assert await expect(promise).rejects.toThrow( 'Failed to complete task: POST method not available on client' ); }); it('should complete a task successfully when not in Brian shared project', async () => { // arrange const mockClient = { get: vi.fn(), post: vi.fn().mockResolvedValue({ data: { ok: true } }), }; mockGetTodoistClient.mockReturnValue(mockClient); // act const result = await completeTask('123'); // assert expect(result).toBe('Task completed successfully'); }); it('should throw exception when task is in Brian shared project', async () => { // arrange const mockClient = { get: vi.fn(), post: vi.fn(), }; mockGetTodoistClient.mockReturnValue(mockClient); mockGetTaskById.mockResolvedValue({ id: '123', project_id: '789', content: 'Test task', description: '', is_completed: false, labels: [], priority: 1, due: null, url: 'https://todoist.com/task/123', comment_count: 0, created_at: '2024-01-01T00:00:00Z', updated_at: '2024-01-01T00:00:00Z', }); mockListProjects.mockResolvedValue({ projects: [ { id: '789', name: 'Brian inbox - per Becky', url: 'https://todoist.com/project/789', is_favorite: false, is_inbox_project: true, } as TodoistProject, ], total_count: 1, }); // act const promise = completeTask('123'); // assert await expect(promise).rejects.toThrow( 'This task is in a Brian shared project. Please use the tool for completing Becky tasks instead.' ); }); }); describe('createTask', () => { it('should create a task successfully with all parameters', async () => { // arrange const mockClient = { get: vi.fn(), post: vi.fn().mockResolvedValue({ data: { id: '12345', content: 'Test task', description: 'Test description', project_id: '67890', labels: ['test-label'], priority: 3, due_date: '2024-01-15', }, }), }; mockGetTodoistClient.mockReturnValue(mockClient); // act const result = await createTask({ title: 'Test task', description: 'Test description', projectId: '67890', labels: ['test-label'], priority: 3, dueDate: '2024-01-15', }); // assert expect(result).toContain('Task created successfully'); expect(result).toContain('Test task'); expect(mockClient.post).toHaveBeenCalledWith('/tasks', { content: 'Test task', description: 'Test description', project_id: '67890', labels: ['test-label'], priority: 3, due_date: '2024-01-15', }); }); it('should create a task with minimal required parameters', async () => { // arrange const mockClient = { get: vi.fn(), post: vi.fn().mockResolvedValue({ data: { id: '12345', content: 'Simple task', }, }), }; mockGetTodoistClient.mockReturnValue(mockClient); // act const result = await createTask({ title: 'Simple task', }); // assert expect(result).toContain('Task created successfully'); expect(result).toContain('Simple task'); expect(mockClient.post).toHaveBeenCalledWith('/tasks', { content: 'Simple task', }); }); it('should handle API errors gracefully', async () => { // arrange const mockClient = { get: vi.fn(), post: vi.fn().mockRejectedValue(new Error('API Error')), }; mockGetTodoistClient.mockReturnValue(mockClient); // act const promise = createTask({ title: 'Test task', }); // assert await expect(promise).rejects.toThrow('Failed to create task: API Error'); }); it('should handle axios errors with response data', async () => { // arrange const axiosError = { isAxiosError: true, response: { data: { error: 'Invalid project ID', }, }, message: 'Request failed', }; const mockClient = { get: vi.fn(), post: vi.fn().mockRejectedValue(axiosError), }; mockGetTodoistClient.mockReturnValue(mockClient); // act const promise = createTask({ title: 'Test task', projectId: 'invalid-id', }); // assert await expect(promise).rejects.toThrow( 'Failed to create task: Invalid project ID' ); }); it('should handle unknown errors', async () => { // arrange const mockClient = { get: vi.fn(), post: vi.fn().mockRejectedValue('Unknown error'), }; mockGetTodoistClient.mockReturnValue(mockClient); // act const promise = createTask({ title: 'Test task', }); // assert await expect(promise).rejects.toThrow( 'Failed to create task: Unknown error' ); }); }); });

Latest Blog Posts

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/bkotos/todoist-mcp'

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