Skip to main content
Glama
projects.spec.ts12.1 kB
import { listProjects } from '../projects/projects'; import type { MockedFunction, Mocked } from 'vitest'; import { getTodoistClient } from '../client'; import fs from 'fs'; import path from 'path'; // Mock the client module vi.mock('../client'); // Mock fs module vi.mock('fs'); vi.mock('path'); const mockGetTodoistClient = getTodoistClient as MockedFunction< typeof getTodoistClient >; const mockFs = fs as Mocked<typeof fs>; const mockPath = path as Mocked<typeof path>; describe('Projects Service', () => { beforeEach(() => { // Clear all mocks vi.clearAllMocks(); // Reset path mock mockPath.join.mockReturnValue('.cache/projects.json'); mockPath.dirname.mockReturnValue('.cache'); }); describe('listProjects', () => { it('should list all projects successfully', async () => { // arrange const mockProjects = [ { id: '1', name: 'Personal', color: 'charcoal', order: 1, comment_count: 0, is_shared: false, is_favorite: false, is_inbox_project: true, is_team_inbox: false, view_style: 'list', url: 'https://todoist.com/project/1', created_at: '2023-01-01T00:00:00Z', updated_at: '2023-01-01T00:00:00Z', }, { id: '2', name: 'Work', color: 'blue', order: 2, comment_count: 0, is_shared: false, is_favorite: true, is_inbox_project: false, is_team_inbox: false, view_style: 'list', url: 'https://todoist.com/project/2', created_at: '2023-01-01T00:00:00Z', updated_at: '2023-01-01T00:00:00Z', }, ]; const mockClient = { get: vi.fn().mockResolvedValue({ data: mockProjects }), post: vi.fn(), }; mockGetTodoistClient.mockReturnValue(mockClient); mockFs.existsSync.mockReturnValue(false); mockFs.mkdirSync.mockImplementation(() => undefined); mockFs.writeFileSync.mockImplementation(() => undefined); // act const result = await listProjects(); // assert expect(result.projects).toHaveLength(2); expect(result.projects[0]).toMatchObject({ id: '1', name: 'Personal', is_inbox_project: true, is_favorite: false, }); expect(result.projects[1]).toMatchObject({ id: '2', name: 'Work', is_inbox_project: false, is_favorite: true, }); expect(result.total_count).toBe(2); expect(result.cached_at).toBeDefined(); expect(mockClient.get).toHaveBeenCalledWith('/projects'); }); it('should handle empty projects list', async () => { // arrange const mockClient = { get: vi.fn().mockResolvedValue({ data: [] }), }; mockGetTodoistClient.mockReturnValue(mockClient); mockFs.existsSync.mockReturnValue(false); mockFs.mkdirSync.mockImplementation(() => undefined); mockFs.writeFileSync.mockImplementation(() => undefined); // act const result = await listProjects(); // assert expect(result).toEqual({ projects: [], total_count: 0, cached_at: expect.any(String), }); }); it('should handle API errors gracefully', async () => { // arrange const mockClient = { get: vi.fn().mockRejectedValue(new Error('API Error')), }; mockGetTodoistClient.mockReturnValue(mockClient); mockFs.existsSync.mockReturnValue(false); mockFs.mkdirSync.mockImplementation(() => undefined); // act const promise = listProjects(); // assert await expect(promise).rejects.toThrow( 'Failed to list projects: API Error' ); }); it('should throw error when client creation fails', async () => { // arrange mockGetTodoistClient.mockImplementation(() => { throw new Error('TODOIST_API_TOKEN environment variable is required'); }); mockFs.existsSync.mockReturnValue(false); // act const promise = listProjects(); // assert await expect(promise).rejects.toThrow( 'TODOIST_API_TOKEN environment variable is required' ); }); it('should fetch from API and cache when no cache file exists', async () => { // arrange const mockProjects = [ { id: '1', name: 'Personal', color: 'charcoal', order: 1, comment_count: 0, is_shared: false, is_favorite: false, is_inbox_project: true, is_team_inbox: false, view_style: 'list', url: 'https://todoist.com/project/1', created_at: '2023-01-01T00:00:00Z', updated_at: '2023-01-01T00:00:00Z', }, ]; const mockClient = { get: vi.fn().mockResolvedValue({ data: mockProjects }), }; mockGetTodoistClient.mockReturnValue(mockClient); mockFs.existsSync.mockReturnValue(false); mockFs.mkdirSync.mockImplementation(() => undefined); mockFs.writeFileSync.mockImplementation(() => undefined); // act const result = await listProjects(); // assert expect(result.projects).toHaveLength(1); expect(result.projects[0]).toMatchObject({ id: '1', name: 'Personal', is_inbox_project: true, is_favorite: false, }); expect(result.total_count).toBe(1); expect(result.cached_at).toBeDefined(); expect(mockClient.get).toHaveBeenCalledWith('/projects'); expect(mockFs.existsSync).toHaveBeenCalledWith('.cache'); expect(mockFs.mkdirSync).toHaveBeenCalledWith('.cache', { recursive: true, }); expect(mockFs.writeFileSync).toHaveBeenCalledWith( '.cache/projects.json', expect.stringContaining('"projects"') ); }); it('should read from cache when cache file is less than one day old', async () => { // arrange const cachedData = { projects: [ { id: 1, name: 'Cached Project', url: 'https://todoist.com/project/1', is_favorite: true, is_inbox_project: false, }, ], total_count: 1, cached_at: new Date().toISOString(), }; mockFs.existsSync.mockReturnValue(true); mockFs.statSync.mockReturnValue({ mtime: new Date(), isFile: () => true, } as any); mockFs.readFileSync.mockReturnValue(JSON.stringify(cachedData)); // act const result = await listProjects(); // assert expect(result).toEqual(cachedData); expect(mockFs.readFileSync).toHaveBeenCalledWith( '.cache/projects.json', 'utf8' ); }); it('should fetch from API when cache file is older than one day', async () => { // arrange const mockProjects = [ { id: '2', name: 'New Project', color: 'blue', order: 1, comment_count: 0, is_shared: false, is_favorite: true, is_inbox_project: false, is_team_inbox: false, view_style: 'list', url: 'https://todoist.com/project/2', created_at: '2023-01-01T00:00:00Z', updated_at: '2023-01-01T00:00:00Z', }, ]; const mockClient = { get: vi.fn().mockResolvedValue({ data: mockProjects }), }; mockGetTodoistClient.mockReturnValue(mockClient); mockFs.existsSync.mockReturnValue(true); mockFs.statSync.mockReturnValue({ mtime: new Date(Date.now() - 2 * 24 * 60 * 60 * 1000), // 2 days ago isFile: () => true, } as any); mockFs.writeFileSync.mockImplementation(() => undefined); // act const result = await listProjects(); // assert expect(result.projects).toHaveLength(1); expect(result.projects[0]).toMatchObject({ id: '2', name: 'New Project', is_inbox_project: false, is_favorite: true, }); expect(result.total_count).toBe(1); expect(result.cached_at).toBeDefined(); expect(mockClient.get).toHaveBeenCalledWith('/projects'); expect(mockFs.writeFileSync).toHaveBeenCalledWith( '.cache/projects.json', expect.stringContaining('"projects"') ); }); it('should handle cache file read errors by falling back to API', async () => { // arrange const mockProjects = [ { id: '3', name: 'Fallback Project', color: 'red', order: 1, comment_count: 0, is_shared: false, is_favorite: false, is_inbox_project: true, is_team_inbox: false, view_style: 'list', url: 'https://todoist.com/project/3', created_at: '2023-01-01T00:00:00Z', updated_at: '2023-01-01T00:00:00Z', }, ]; const mockClient = { get: vi.fn().mockResolvedValue({ data: mockProjects }), }; mockGetTodoistClient.mockReturnValue(mockClient); mockFs.existsSync.mockReturnValue(true); mockFs.statSync.mockReturnValue({ mtime: new Date(), isFile: () => true, } as any); mockFs.readFileSync.mockImplementation(() => { throw new Error('File read error'); }); mockFs.writeFileSync.mockImplementation(() => undefined); // act const result = await listProjects(); // assert expect(result.projects).toHaveLength(1); expect(result.projects[0]).toMatchObject({ id: '3', name: 'Fallback Project', is_inbox_project: true, is_favorite: false, }); expect(result.total_count).toBe(1); expect(result.cached_at).toBeDefined(); expect(mockClient.get).toHaveBeenCalledWith('/projects'); }); it('should handle invalid JSON in cache file by falling back to API', async () => { // arrange const mockProjects = [ { id: '4', name: 'JSON Error Project', color: 'green', order: 1, comment_count: 0, is_shared: false, is_favorite: false, is_inbox_project: false, is_team_inbox: false, view_style: 'list', url: 'https://todoist.com/project/4', created_at: '2023-01-01T00:00:00Z', updated_at: '2023-01-01T00:00:00Z', }, ]; const mockClient = { get: vi.fn().mockResolvedValue({ data: mockProjects }), }; mockGetTodoistClient.mockReturnValue(mockClient); mockFs.existsSync.mockReturnValue(true); mockFs.statSync.mockReturnValue({ mtime: new Date(), isFile: () => true, } as any); mockFs.readFileSync.mockReturnValue('invalid json content'); mockFs.writeFileSync.mockImplementation(() => undefined); // act const result = await listProjects(); // assert expect(result.projects).toHaveLength(1); expect(result.projects[0]).toMatchObject({ id: '4', name: 'JSON Error Project', is_inbox_project: false, is_favorite: false, }); expect(result.total_count).toBe(1); expect(result.cached_at).toBeDefined(); expect(mockClient.get).toHaveBeenCalledWith('/projects'); }); it('should handle API errors when cache is invalid', async () => { // arrange const mockClient = { get: vi.fn().mockRejectedValue(new Error('API Error')), }; mockGetTodoistClient.mockReturnValue(mockClient); mockFs.existsSync.mockReturnValue(true); mockFs.statSync.mockReturnValue({ mtime: new Date(), isFile: () => true, } as any); mockFs.readFileSync.mockReturnValue('invalid json content'); // act const promise = listProjects(); // assert await expect(promise).rejects.toThrow( 'Failed to list projects: API 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