Skip to main content
Glama
labels.spec.ts19.6 kB
import type { MockedFunction, Mocked } from 'vitest'; import { getAllLabels, getProjectLabels, getContextLabels, createProjectLabel, } from './labels'; 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('Labels Functions', () => { beforeEach(() => { // Clear all mocks vi.clearAllMocks(); // Reset path mock mockPath.join.mockReturnValue('.cache/labels.json'); mockPath.dirname.mockReturnValue('.cache'); }); describe('getAllLabels', () => { it('should return all labels when API call succeeds', async () => { // arrange const mockLabels = [ { id: '1', name: 'Work', color: 'charcoal', order: 1, is_favorite: false, }, { id: '2', name: 'Personal', color: 'blue', order: 2, is_favorite: true, }, { id: '3', name: 'Urgent', color: 'red', order: 3, is_favorite: false, }, ]; const mockClient = { get: vi.fn().mockResolvedValue({ data: mockLabels }), }; mockGetTodoistClient.mockReturnValue(mockClient); // act const result = await getAllLabels(); // assert expect(result.labels).toHaveLength(3); expect(result.labels[0].id).toBe(1); expect(result.labels[0].name).toBe('Work'); expect(result.labels[0].color).toBe('charcoal'); expect(result.labels[1].name).toBe('Personal'); expect(result.labels[1].is_favorite).toBe(true); expect(result.total_count).toBe(3); expect(mockClient.get).toHaveBeenCalledWith('/labels'); }); it('should handle empty response', async () => { // arrange const mockClient = { get: vi.fn().mockResolvedValue({ data: [] }), }; mockGetTodoistClient.mockReturnValue(mockClient); // act const result = await getAllLabels(); // assert expect(result.labels).toHaveLength(0); expect(result.total_count).toBe(0); expect(mockClient.get).toHaveBeenCalledWith('/labels'); }); it('should handle API errors', async () => { // arrange const mockClient = { get: vi.fn().mockRejectedValue(new Error('API Error')), }; mockGetTodoistClient.mockReturnValue(mockClient); // act const promise = getAllLabels(); // assert await expect(promise).rejects.toThrow( 'Failed to get all labels: API Error' ); expect(mockClient.get).toHaveBeenCalledWith('/labels'); }); it('should return cached labels when cache is fresh', async () => { // arrange const cachedLabels = { labels: [ { id: 1, name: 'Work', color: 'charcoal', order: 1, is_favorite: false, }, { id: 2, name: 'Personal', color: 'blue', order: 2, is_favorite: true, }, ], total_count: 2, cached_at: '2024-01-01T00:00:00Z', }; mockFs.existsSync.mockReturnValue(true); mockFs.statSync.mockReturnValue({ mtime: new Date(), isFile: () => true, } as any); mockFs.readFileSync.mockReturnValue(JSON.stringify(cachedLabels)); // act const result = await getAllLabels(); // assert expect(result).toEqual(cachedLabels); expect(mockFs.readFileSync).toHaveBeenCalledWith( '.cache/labels.json', 'utf8' ); }); it('should fetch from API and cache when cache is stale', async () => { // arrange const mockLabels = [ { id: '1', name: 'Work', color: 'charcoal', order: 1, is_favorite: false, }, { id: '2', name: 'Personal', color: 'blue', order: 2, is_favorite: true, }, ]; const mockClient = { get: vi.fn().mockResolvedValue({ data: mockLabels }), }; mockGetTodoistClient.mockReturnValue(mockClient); mockFs.existsSync.mockReturnValue(true); mockFs.statSync.mockReturnValue({ mtime: new Date(Date.now() - 25 * 60 * 60 * 1000), // 25 hours old isFile: () => true, } as any); mockFs.writeFileSync.mockImplementation(() => undefined); // act const result = await getAllLabels(); // assert expect(result).toEqual({ labels: [ { id: 1, name: 'Work', color: 'charcoal', order: 1, is_favorite: false, }, { id: 2, name: 'Personal', color: 'blue', order: 2, is_favorite: true, }, ], total_count: 2, cached_at: expect.any(String), }); expect(mockClient.get).toHaveBeenCalledWith('/labels'); expect(mockFs.writeFileSync).toHaveBeenCalledWith( '.cache/labels.json', expect.stringContaining('"cached_at"') ); }); it('should create cache directory if it does not exist', async () => { // arrange const mockLabels = [ { id: '1', name: 'Work', color: 'charcoal', order: 1, is_favorite: false, }, ]; const mockClient = { get: vi.fn().mockResolvedValue({ data: mockLabels }), }; mockGetTodoistClient.mockReturnValue(mockClient); mockFs.existsSync.mockReturnValue(false); mockFs.mkdirSync.mockImplementation(() => undefined); mockFs.writeFileSync.mockImplementation(() => undefined); // act const result = await getAllLabels(); // assert expect(mockFs.mkdirSync).toHaveBeenCalledWith('.cache', { recursive: true, }); expect(result).toEqual({ labels: [ { id: 1, name: 'Work', color: 'charcoal', order: 1, is_favorite: false, }, ], total_count: 1, cached_at: expect.any(String), }); }); it('should handle cache file read errors by falling back to API', async () => { // arrange const mockLabels = [ { id: '1', name: 'Work', color: 'charcoal', order: 1, is_favorite: false, }, ]; const mockClient = { get: vi.fn().mockResolvedValue({ data: mockLabels }), }; 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 getAllLabels(); // assert expect(result).toEqual({ labels: [ { id: 1, name: 'Work', color: 'charcoal', order: 1, is_favorite: false, }, ], total_count: 1, cached_at: expect.any(String), }); expect(mockClient.get).toHaveBeenCalledWith('/labels'); }); it('should handle invalid JSON in cache file by falling back to API', async () => { // arrange const mockLabels = [ { id: '1', name: 'Work', color: 'charcoal', order: 1, is_favorite: false, }, ]; const mockClient = { get: vi.fn().mockResolvedValue({ data: mockLabels }), }; 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 getAllLabels(); // assert expect(result).toEqual({ labels: [ { id: 1, name: 'Work', color: 'charcoal', order: 1, is_favorite: false, }, ], total_count: 1, cached_at: expect.any(String), }); expect(mockClient.get).toHaveBeenCalledWith('/labels'); }); 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 = getAllLabels(); // assert await expect(promise).rejects.toThrow( 'Failed to get all labels: API Error' ); }); }); describe('getProjectLabels', () => { it('should return only project labels when API call succeeds', async () => { // arrange const mockLabels = [ { id: '1', name: 'Work', color: 'charcoal', order: 1, is_favorite: false, }, { id: '2', name: 'PROJECT:Website Redesign', color: 'blue', order: 2, is_favorite: true, }, { id: '3', name: 'Urgent', color: 'red', order: 3, is_favorite: false, }, { id: '4', name: 'PROJECT:Mobile App', color: 'green', order: 4, is_favorite: false, }, { id: '5', name: 'PROJECT:Marketing Campaign', color: 'purple', order: 5, is_favorite: true, }, ]; const mockClient = { get: vi.fn().mockResolvedValue({ data: mockLabels }), }; mockGetTodoistClient.mockReturnValue(mockClient); // act const result = await getProjectLabels(); // assert expect(result.labels).toHaveLength(3); expect(result.labels[0].id).toBe(2); expect(result.labels[0].name).toBe('PROJECT:Website Redesign'); expect(result.labels[0].color).toBe('blue'); expect(result.labels[1].name).toBe('PROJECT:Mobile App'); expect(result.labels[2].name).toBe('PROJECT:Marketing Campaign'); expect(result.total_count).toBe(3); expect(mockClient.get).toHaveBeenCalledWith('/labels'); }); it('should handle empty response', async () => { // arrange const mockClient = { get: vi.fn().mockResolvedValue({ data: [] }), }; mockGetTodoistClient.mockReturnValue(mockClient); // act const result = await getProjectLabels(); // assert expect(result.labels).toHaveLength(0); expect(result.total_count).toBe(0); expect(mockClient.get).toHaveBeenCalledWith('/labels'); }); it('should handle response with no project labels', async () => { // arrange const mockLabels = [ { id: '1', name: 'Work', color: 'charcoal', order: 1, is_favorite: false, }, { id: '2', name: 'Personal', color: 'blue', order: 2, is_favorite: true, }, { id: '3', name: 'Urgent', color: 'red', order: 3, is_favorite: false, }, ]; const mockClient = { get: vi.fn().mockResolvedValue({ data: mockLabels }), }; mockGetTodoistClient.mockReturnValue(mockClient); // act const result = await getProjectLabels(); // assert expect(result.labels).toHaveLength(0); expect(result.total_count).toBe(0); expect(mockClient.get).toHaveBeenCalledWith('/labels'); }); it('should handle API errors', async () => { // arrange const mockClient = { get: vi.fn().mockRejectedValue(new Error('API Error')), }; mockGetTodoistClient.mockReturnValue(mockClient); // act const promise = getProjectLabels(); // assert await expect(promise).rejects.toThrow( 'Failed to get project labels: API Error' ); expect(mockClient.get).toHaveBeenCalledWith('/labels'); }); }); describe('getContextLabels', () => { it('should return only context labels when API call succeeds', async () => { // arrange const mockLabels = [ { id: '1', name: 'Work', color: 'charcoal', order: 1, is_favorite: false, }, { id: '2', name: 'PROJECT:Website Redesign', color: 'blue', order: 2, is_favorite: true, }, { id: '3', name: 'context:home', color: 'red', order: 3, is_favorite: false, }, { id: '4', name: 'context:office', color: 'green', order: 4, is_favorite: false, }, { id: '5', name: 'context:mobile', color: 'purple', order: 5, is_favorite: true, }, ]; const mockClient = { get: vi.fn().mockResolvedValue({ data: mockLabels }), }; mockGetTodoistClient.mockReturnValue(mockClient); // act const result = await getContextLabels(); // assert expect(result.labels).toHaveLength(3); expect(result.labels[0].id).toBe(3); expect(result.labels[0].name).toBe('context:home'); expect(result.labels[0].color).toBe('red'); expect(result.labels[1].name).toBe('context:office'); expect(result.labels[2].name).toBe('context:mobile'); expect(result.total_count).toBe(3); expect(mockClient.get).toHaveBeenCalledWith('/labels'); }); it('should handle empty response', async () => { // arrange const mockClient = { get: vi.fn().mockResolvedValue({ data: [] }), }; mockGetTodoistClient.mockReturnValue(mockClient); // act const result = await getContextLabels(); // assert expect(result.labels).toHaveLength(0); expect(result.total_count).toBe(0); expect(mockClient.get).toHaveBeenCalledWith('/labels'); }); it('should handle response with no context labels', async () => { // arrange const mockLabels = [ { id: '1', name: 'Work', color: 'charcoal', order: 1, is_favorite: false, }, { id: '2', name: 'PROJECT:Website Redesign', color: 'blue', order: 2, is_favorite: true, }, { id: '3', name: 'Urgent', color: 'red', order: 3, is_favorite: false, }, ]; const mockClient = { get: vi.fn().mockResolvedValue({ data: mockLabels }), }; mockGetTodoistClient.mockReturnValue(mockClient); // act const result = await getContextLabels(); // assert expect(result.labels).toHaveLength(0); expect(result.total_count).toBe(0); expect(mockClient.get).toHaveBeenCalledWith('/labels'); }); it('should handle API errors', async () => { // arrange const mockClient = { get: vi.fn().mockRejectedValue(new Error('API Error')), }; mockGetTodoistClient.mockReturnValue(mockClient); // act const promise = getContextLabels(); // assert await expect(promise).rejects.toThrow( 'Failed to get context labels: API Error' ); expect(mockClient.get).toHaveBeenCalledWith('/labels'); }); }); describe('createProjectLabel', () => { it('should create a project label successfully', async () => { // arrange const mockCreatedLabel = { id: '123', name: 'PROJECT: New Website', color: 'charcoal', order: 1, is_favorite: false, }; const mockClient = { get: vi.fn(), post: vi.fn().mockResolvedValue({ data: mockCreatedLabel }), }; mockGetTodoistClient.mockReturnValue(mockClient); // act const result = await createProjectLabel('PROJECT: New Website'); // assert expect(result).toEqual({ id: 123, name: 'PROJECT: New Website', color: 'charcoal', order: 1, is_favorite: false, }); expect(mockClient.post).toHaveBeenCalledWith('/labels', { name: 'PROJECT: New Website', color: 'charcoal', }); }); it('should reject input without PROJECT: prefix', async () => { // arrange const mockClient = { get: vi.fn(), post: vi.fn(), }; mockGetTodoistClient.mockReturnValue(mockClient); // act const promise = createProjectLabel('New Website'); // assert await expect(promise).rejects.toThrow( 'Project label name must start with "PROJECT: "' ); expect(mockClient.post).not.toHaveBeenCalled(); }); it('should reject input with PROJECT: prefix but no space', async () => { // arrange const mockClient = { get: vi.fn(), post: vi.fn(), }; mockGetTodoistClient.mockReturnValue(mockClient); // act const promise = createProjectLabel('PROJECT:New Website'); // assert await expect(promise).rejects.toThrow( 'Project label name must start with "PROJECT: "' ); expect(mockClient.post).not.toHaveBeenCalled(); }); it('should handle API errors', async () => { // arrange const mockClient = { get: vi.fn(), post: vi.fn().mockRejectedValue(new Error('API Error')), }; mockGetTodoistClient.mockReturnValue(mockClient); // act const promise = createProjectLabel('PROJECT: Test Project'); // assert await expect(promise).rejects.toThrow( 'Failed to create project label: API Error' ); expect(mockClient.post).toHaveBeenCalledWith('/labels', { name: 'PROJECT: Test Project', color: 'charcoal', }); }); it('should handle empty project name', async () => { // arrange const mockClient = { get: vi.fn(), post: vi.fn().mockRejectedValue(new Error('Invalid name')), }; mockGetTodoistClient.mockReturnValue(mockClient); // act const promise = createProjectLabel('PROJECT: '); // assert await expect(promise).rejects.toThrow( 'Failed to create project label: Invalid name' ); expect(mockClient.post).toHaveBeenCalledWith('/labels', { name: 'PROJECT: ', color: 'charcoal', }); }); }); });

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