Skip to main content
Glama
timer-stop.test.ts11.9 kB
/** * Tests for timer_stop tool */ import { describe, it, expect, beforeEach, vi } from 'vitest'; import { timerStopTool, timerStopHandler } from '../../../src/tools/timer/timer-stop.js'; import { createMockClientWrapper } from '../../mocks/client.js'; import { mockTimeEntryUpdateResponse, mockTimeEntryNotFoundError, } from '../../mocks/responses/time-entry.js'; import { mockUnauthorizedError, mockServerError, mockNoActiveTimerError, } from '../../mocks/errors/freshbooks-errors.js'; describe('timer_stop tool', () => { let mockClient: ReturnType<typeof createMockClientWrapper>; beforeEach(() => { mockClient = createMockClientWrapper(); vi.clearAllMocks(); }); describe('successful timer stop', () => { it('should stop a running timer', async () => { const mockResponse = mockTimeEntryUpdateResponse(12345, { active: false, isLogged: true, duration: 3600, // Auto-calculated by FreshBooks }); mockClient.executeWithRetry.mockImplementation(async (operation, apiCall) => { const client = { timeEntries: { update: vi.fn().mockResolvedValue(mockResponse), }, }; return apiCall(client); }); const result = await timerStopHandler( { accountId: 'ABC123', timeEntryId: 12345 }, { accountId: 'ABC123', client: mockClient as any } ); expect(result.active).toBe(false); expect(result.isLogged).toBe(true); expect(result.duration).toBe(3600); }); it('should stop timer and update note', async () => { const mockResponse = mockTimeEntryUpdateResponse(12345, { active: false, isLogged: true, duration: 3600, note: 'Completed feature development', }); mockClient.executeWithRetry.mockImplementation(async (operation, apiCall) => { const client = { timeEntries: { update: vi.fn().mockResolvedValue(mockResponse), }, }; return apiCall(client); }); const result = await timerStopHandler( { accountId: 'ABC123', timeEntryId: 12345, note: 'Completed feature development', }, { accountId: 'ABC123', client: mockClient as any } ); expect(result.active).toBe(false); expect(result.note).toBe('Completed feature development'); }); it('should stop timer with auto-calculated duration', async () => { const mockResponse = mockTimeEntryUpdateResponse(12345, { active: false, isLogged: true, duration: 7200, // 2 hours calculated by FreshBooks }); mockClient.executeWithRetry.mockImplementation(async (operation, apiCall) => { const client = { timeEntries: { update: vi.fn().mockResolvedValue(mockResponse), }, }; return apiCall(client); }); const result = await timerStopHandler( { accountId: 'ABC123', timeEntryId: 12345 }, { accountId: 'ABC123', client: mockClient as any } ); expect(result.duration).toBe(7200); expect(result.active).toBe(false); }); it('should preserve other timer fields when stopping', async () => { const mockResponse = mockTimeEntryUpdateResponse(12345, { active: false, isLogged: true, duration: 3600, projectId: 100, clientId: 200, billable: true, note: 'Previous note', }); mockClient.executeWithRetry.mockImplementation(async (operation, apiCall) => { const client = { timeEntries: { update: vi.fn().mockResolvedValue(mockResponse), }, }; return apiCall(client); }); const result = await timerStopHandler( { accountId: 'ABC123', timeEntryId: 12345 }, { accountId: 'ABC123', client: mockClient as any } ); expect(result.projectId).toBe(100); expect(result.clientId).toBe(200); expect(result.billable).toBe(true); expect(result.note).toBe('Previous note'); }); it('should handle stopping timer with very long duration', async () => { const longDuration = 86400 * 7; // 7 days in seconds const mockResponse = mockTimeEntryUpdateResponse(12345, { active: false, isLogged: true, duration: longDuration, }); mockClient.executeWithRetry.mockImplementation(async (operation, apiCall) => { const client = { timeEntries: { update: vi.fn().mockResolvedValue(mockResponse), }, }; return apiCall(client); }); const result = await timerStopHandler( { accountId: 'ABC123', timeEntryId: 12345 }, { accountId: 'ABC123', client: mockClient as any } ); expect(result.duration).toBe(longDuration); }); }); describe('error handling', () => { it('should handle timer not found error', async () => { const mockResponse = mockTimeEntryNotFoundError(99999); mockClient.executeWithRetry.mockImplementation(async (operation, apiCall) => { const client = { timeEntries: { update: vi.fn().mockResolvedValue(mockResponse), }, }; return apiCall(client); }); await expect( timerStopHandler( { accountId: 'ABC123', timeEntryId: 99999 }, { accountId: 'ABC123', client: mockClient as any } ) ).rejects.toThrow(); }); it('should handle no active timer error', async () => { mockClient.executeWithRetry.mockImplementation(async (operation, apiCall) => { const client = { timeEntries: { update: vi.fn().mockResolvedValue(mockNoActiveTimerError()), }, }; return apiCall(client); }); await expect( timerStopHandler( { accountId: 'ABC123', timeEntryId: 12345 }, { accountId: 'ABC123', client: mockClient as any } ) ).rejects.toThrow(); }); it('should handle unauthorized error', async () => { mockClient.executeWithRetry.mockImplementation(async (operation, apiCall) => { const client = { timeEntries: { update: vi.fn().mockResolvedValue(mockUnauthorizedError()), }, }; return apiCall(client); }); await expect( timerStopHandler( { accountId: 'ABC123', timeEntryId: 12345 }, { accountId: 'ABC123', client: mockClient as any } ) ).rejects.toThrow(); }); it('should handle server error', async () => { mockClient.executeWithRetry.mockImplementation(async (operation, apiCall) => { const client = { timeEntries: { update: vi.fn().mockResolvedValue(mockServerError()), }, }; return apiCall(client); }); await expect( timerStopHandler( { accountId: 'ABC123', timeEntryId: 12345 }, { accountId: 'ABC123', client: mockClient as any } ) ).rejects.toThrow(); }); }); describe('input validation', () => { it('should require accountId', async () => { await expect( timerStopHandler( { timeEntryId: 12345 } as any, { client: mockClient as any } as any ) ).rejects.toThrow(); }); it('should require timeEntryId', async () => { await expect( timerStopHandler( { accountId: 'ABC123' } as any, { accountId: 'ABC123', client: mockClient as any } ) ).rejects.toThrow(); }); it('should reject empty accountId', async () => { await expect( timerStopHandler( { accountId: '', timeEntryId: 12345 }, { accountId: '', client: mockClient as any } ) ).rejects.toThrow(); }); it('should reject non-numeric timeEntryId', async () => { await expect( timerStopHandler( { accountId: 'ABC123', timeEntryId: 'invalid' as any }, { accountId: 'ABC123', client: mockClient as any } ) ).rejects.toThrow(); }); }); describe('edge cases', () => { it('should handle empty note update', async () => { const mockResponse = mockTimeEntryUpdateResponse(12345, { active: false, isLogged: true, duration: 3600, note: '', }); mockClient.executeWithRetry.mockImplementation(async (operation, apiCall) => { const client = { timeEntries: { update: vi.fn().mockResolvedValue(mockResponse), }, }; return apiCall(client); }); const result = await timerStopHandler( { accountId: 'ABC123', timeEntryId: 12345, note: '' }, { accountId: 'ABC123', client: mockClient as any } ); expect(result.note).toBe(''); }); it('should handle unicode characters in note', async () => { const unicodeNote = 'テスト 🎉 completed'; const mockResponse = mockTimeEntryUpdateResponse(12345, { active: false, isLogged: true, duration: 3600, note: unicodeNote, }); mockClient.executeWithRetry.mockImplementation(async (operation, apiCall) => { const client = { timeEntries: { update: vi.fn().mockResolvedValue(mockResponse), }, }; return apiCall(client); }); const result = await timerStopHandler( { accountId: 'ABC123', timeEntryId: 12345, note: unicodeNote }, { accountId: 'ABC123', client: mockClient as any } ); expect(result.note).toBe(unicodeNote); }); it('should handle timer with zero duration (stopped immediately)', async () => { const mockResponse = mockTimeEntryUpdateResponse(12345, { active: false, isLogged: true, duration: 0, }); mockClient.executeWithRetry.mockImplementation(async (operation, apiCall) => { const client = { timeEntries: { update: vi.fn().mockResolvedValue(mockResponse), }, }; return apiCall(client); }); const result = await timerStopHandler( { accountId: 'ABC123', timeEntryId: 12345 }, { accountId: 'ABC123', client: mockClient as any } ); expect(result.duration).toBe(0); }); it('should handle very large timeEntryId', async () => { const largeId = 999999999; const mockResponse = mockTimeEntryUpdateResponse(largeId, { active: false, isLogged: true, duration: 3600, }); mockClient.executeWithRetry.mockImplementation(async (operation, apiCall) => { const client = { timeEntries: { update: vi.fn().mockResolvedValue(mockResponse), }, }; return apiCall(client); }); const result = await timerStopHandler( { accountId: 'ABC123', timeEntryId: largeId }, { accountId: 'ABC123', client: mockClient as any } ); expect(result.id).toBe(largeId); }); it('should handle idempotent stop (stopping already stopped timer)', async () => { const mockResponse = mockTimeEntryUpdateResponse(12345, { active: false, isLogged: true, duration: 3600, }); mockClient.executeWithRetry.mockImplementation(async (operation, apiCall) => { const client = { timeEntries: { update: vi.fn().mockResolvedValue(mockResponse), }, }; return apiCall(client); }); const result = await timerStopHandler( { accountId: 'ABC123', timeEntryId: 12345 }, { accountId: 'ABC123', client: mockClient as any } ); expect(result.active).toBe(false); expect(result.isLogged).toBe(true); }); }); });

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/Good-Samaritan-Software-LLC/freshbooks-mcp'

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