Skip to main content
Glama
timeentry-update.test.ts13.5 kB
/** * Tests for timeentry_update tool */ import { describe, it, expect, beforeEach, vi } from 'vitest'; import { timeentryUpdateTool } from '../../../src/tools/time-entry/timeentry-update.js'; import { createMockClientWrapper } from '../../mocks/client.js'; import { mockTimeEntryUpdateResponse, mockTimeEntryNotFoundError, } from '../../mocks/responses/time-entry.js'; import { mockUnauthorizedError, mockValidationError, mockServerError, } from '../../mocks/errors/freshbooks-errors.js'; describe('timeentry_update tool', () => { let mockClient: ReturnType<typeof createMockClientWrapper>; beforeEach(() => { mockClient = createMockClientWrapper(); vi.clearAllMocks(); }); describe('successful updates', () => { it('should update time entry duration', async () => { const mockResponse = mockTimeEntryUpdateResponse(12345, { duration: 7200 }); mockClient.executeWithRetry.mockImplementation(async (operation, apiCall) => { const client = { timeEntries: { update: vi.fn().mockResolvedValue(mockResponse), }, }; return apiCall(client); }); const result = await timeentryUpdateTool.execute( { accountId: 'ABC123', timeEntryId: 12345, duration: 7200 }, mockClient as any ); expect(result.id).toBe(12345); expect(result.duration).toBe(7200); }); it('should update time entry note', async () => { const mockResponse = mockTimeEntryUpdateResponse(12345, { note: 'Updated work notes', }); mockClient.executeWithRetry.mockImplementation(async (operation, apiCall) => { const client = { timeEntries: { update: vi.fn().mockResolvedValue(mockResponse), }, }; return apiCall(client); }); const result = await timeentryUpdateTool.execute( { accountId: 'ABC123', timeEntryId: 12345, note: 'Updated work notes' }, mockClient as any ); expect(result.note).toBe('Updated work notes'); }); it('should update multiple fields', async () => { const mockResponse = mockTimeEntryUpdateResponse(12345, { duration: 5400, note: 'Updated notes', billable: false, projectId: 999, }); mockClient.executeWithRetry.mockImplementation(async (operation, apiCall) => { const client = { timeEntries: { update: vi.fn().mockResolvedValue(mockResponse), }, }; return apiCall(client); }); const result = await timeentryUpdateTool.execute( { accountId: 'ABC123', timeEntryId: 12345, duration: 5400, note: 'Updated notes', billable: false, projectId: 999, }, mockClient as any ); expect(result.duration).toBe(5400); expect(result.note).toBe('Updated notes'); expect(result.billable).toBe(false); expect(result.projectId).toBe(999); }); it('should stop a running timer (set active=false)', 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 timeentryUpdateTool.execute( { accountId: 'ABC123', timeEntryId: 12345, active: false }, mockClient as any ); expect(result.active).toBe(false); expect(result.isLogged).toBe(true); }); it('should update billable status', async () => { const mockResponse = mockTimeEntryUpdateResponse(12345, { billable: false }); mockClient.executeWithRetry.mockImplementation(async (operation, apiCall) => { const client = { timeEntries: { update: vi.fn().mockResolvedValue(mockResponse), }, }; return apiCall(client); }); const result = await timeentryUpdateTool.execute( { accountId: 'ABC123', timeEntryId: 12345, billable: false }, mockClient as any ); expect(result.billable).toBe(false); }); it('should update project association', async () => { const mockResponse = mockTimeEntryUpdateResponse(12345, { projectId: 555 }); mockClient.executeWithRetry.mockImplementation(async (operation, apiCall) => { const client = { timeEntries: { update: vi.fn().mockResolvedValue(mockResponse), }, }; return apiCall(client); }); const result = await timeentryUpdateTool.execute( { accountId: 'ABC123', timeEntryId: 12345, projectId: 555 }, mockClient as any ); expect(result.projectId).toBe(555); }); it('should remove project association (set to null)', async () => { const mockResponse = mockTimeEntryUpdateResponse(12345, { projectId: null }); mockClient.executeWithRetry.mockImplementation(async (operation, apiCall) => { const client = { timeEntries: { update: vi.fn().mockResolvedValue(mockResponse), }, }; return apiCall(client); }); const result = await timeentryUpdateTool.execute( { accountId: 'ABC123', timeEntryId: 12345, projectId: null }, mockClient as any ); expect(result.projectId).toBeNull(); }); it('should update startedAt timestamp', async () => { const newStartTime = '2024-02-01T14:00:00Z'; const mockResponse = mockTimeEntryUpdateResponse(12345, { startedAt: newStartTime, }); mockClient.executeWithRetry.mockImplementation(async (operation, apiCall) => { const client = { timeEntries: { update: vi.fn().mockResolvedValue(mockResponse), }, }; return apiCall(client); }); const result = await timeentryUpdateTool.execute( { accountId: 'ABC123', timeEntryId: 12345, startedAt: newStartTime }, mockClient as any ); expect(result.startedAt).toBe(newStartTime); }); it('should update internal flag', async () => { const mockResponse = mockTimeEntryUpdateResponse(12345, { internal: true }); mockClient.executeWithRetry.mockImplementation(async (operation, apiCall) => { const client = { timeEntries: { update: vi.fn().mockResolvedValue(mockResponse), }, }; return apiCall(client); }); const result = await timeentryUpdateTool.execute( { accountId: 'ABC123', timeEntryId: 12345, internal: true }, mockClient as any ); expect(result.internal).toBe(true); }); }); describe('validation errors', () => { it('should require at least one field to update', async () => { await expect( timeentryUpdateTool.execute( { accountId: 'ABC123', timeEntryId: 12345 }, mockClient as any ) ).rejects.toThrow(); }); it('should require accountId', async () => { await expect( timeentryUpdateTool.execute( { timeEntryId: 12345, duration: 3600 } as any, mockClient as any ) ).rejects.toThrow(); }); it('should require timeEntryId', async () => { await expect( timeentryUpdateTool.execute( { accountId: 'ABC123', duration: 3600 } as any, mockClient as any ) ).rejects.toThrow(); }); it('should reject negative duration', async () => { await expect( timeentryUpdateTool.execute( { accountId: 'ABC123', timeEntryId: 12345, duration: -100 }, mockClient as any ) ).rejects.toThrow(); }); it('should handle validation error from API', async () => { mockClient.executeWithRetry.mockImplementation(async (operation, apiCall) => { const client = { timeEntries: { update: vi.fn().mockResolvedValue( mockValidationError('duration', 'Invalid duration value') ), }, }; return apiCall(client); }); await expect( timeentryUpdateTool.execute( { accountId: 'ABC123', timeEntryId: 12345, duration: 3600 }, mockClient as any ) ).rejects.toThrow(); }); }); describe('error handling', () => { it('should handle 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( timeentryUpdateTool.execute( { accountId: 'ABC123', timeEntryId: 99999, duration: 3600 }, 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( timeentryUpdateTool.execute( { accountId: 'ABC123', timeEntryId: 12345, duration: 3600 }, 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( timeentryUpdateTool.execute( { accountId: 'ABC123', timeEntryId: 12345, duration: 3600 }, mockClient as any ) ).rejects.toThrow(); }); it('should handle null data response', async () => { mockClient.executeWithRetry.mockImplementation(async (operation, apiCall) => { const client = { timeEntries: { update: vi.fn().mockResolvedValue({ ok: true, data: null }), }, }; return apiCall(client); }); await expect( timeentryUpdateTool.execute( { accountId: 'ABC123', timeEntryId: 12345, duration: 3600 }, mockClient as any ) ).rejects.toThrow(); }); }); describe('edge cases', () => { it('should update duration to zero', async () => { const mockResponse = mockTimeEntryUpdateResponse(12345, { duration: 0 }); mockClient.executeWithRetry.mockImplementation(async (operation, apiCall) => { const client = { timeEntries: { update: vi.fn().mockResolvedValue(mockResponse), }, }; return apiCall(client); }); const result = await timeentryUpdateTool.execute( { accountId: 'ABC123', timeEntryId: 12345, duration: 0 }, mockClient as any ); expect(result.duration).toBe(0); }); it('should update note to empty string', async () => { const mockResponse = mockTimeEntryUpdateResponse(12345, { note: '' }); mockClient.executeWithRetry.mockImplementation(async (operation, apiCall) => { const client = { timeEntries: { update: vi.fn().mockResolvedValue(mockResponse), }, }; return apiCall(client); }); const result = await timeentryUpdateTool.execute( { accountId: 'ABC123', timeEntryId: 12345, note: '' }, mockClient as any ); expect(result.note).toBe(''); }); it('should update note with unicode characters', async () => { const unicodeNote = 'テスト 🎉 special chars'; const mockResponse = mockTimeEntryUpdateResponse(12345, { note: unicodeNote }); mockClient.executeWithRetry.mockImplementation(async (operation, apiCall) => { const client = { timeEntries: { update: vi.fn().mockResolvedValue(mockResponse), }, }; return apiCall(client); }); const result = await timeentryUpdateTool.execute( { accountId: 'ABC123', timeEntryId: 12345, note: unicodeNote }, mockClient as any ); expect(result.note).toBe(unicodeNote); }); it('should update very large duration value', async () => { const largeDuration = 86400 * 365; // 1 year const mockResponse = mockTimeEntryUpdateResponse(12345, { duration: largeDuration, }); mockClient.executeWithRetry.mockImplementation(async (operation, apiCall) => { const client = { timeEntries: { update: vi.fn().mockResolvedValue(mockResponse), }, }; return apiCall(client); }); const result = await timeentryUpdateTool.execute( { accountId: 'ABC123', timeEntryId: 12345, duration: largeDuration }, mockClient as any ); expect(result.duration).toBe(largeDuration); }); }); });

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