Skip to main content
Glama
pshempel

MCP Time Server Node

by pshempel
subtractTime.test.ts16 kB
import { subtractTime } from '../../src/tools/subtractTime'; import type { SubtractTimeResult } from '../../src/types'; import { DateParsingError, ValidationError, TimezoneError, } from '../../src/adapters/mcp-sdk/errors'; // Mock the cache module jest.mock('../../src/cache/timeCache', () => ({ cache: { get: jest.fn(), set: jest.fn(), }, CacheTTL: { CURRENT_TIME: 1, TIMEZONE_CONVERT: 300, CALCULATIONS: 3600, BUSINESS_DAYS: 86400, }, })); // Import the mocked cache import { cache } from '../../src/cache/timeCache'; // Mock the config module jest.mock('../../src/utils/config', () => ({ getConfig: jest.fn().mockReturnValue({ defaultTimezone: 'UTC', }), })); import { getConfig } from '../../src/utils/config'; const mockedGetConfig = getConfig as jest.MockedFunction<typeof getConfig>; describe('subtractTime', () => { const mockedCache = cache as jest.Mocked<typeof cache>; beforeEach(() => { jest.clearAllMocks(); jest.useFakeTimers(); jest.setSystemTime(new Date('2025-07-18T17:30:00.000Z')); // Reset config mock to UTC default mockedGetConfig.mockReturnValue({ defaultTimezone: 'UTC' }); }); afterEach(() => { jest.useRealTimers(); jest.restoreAllMocks(); }); describe('Basic functionality', () => { it('should subtract years from a date', () => { mockedCache.get.mockReturnValue(undefined); const result = subtractTime({ time: '2025-01-15T10:30:00Z', amount: 1, unit: 'years', }); expect(result.original).toBe('2025-01-15T10:30:00.000Z'); expect(result.result).toBe('2024-01-15T10:30:00.000Z'); expect(result.unix_original).toBe(1736937000); expect(result.unix_result).toBe(1705314600); }); it('should subtract months from a date', () => { mockedCache.get.mockReturnValue(undefined); const result = subtractTime({ time: '2025-01-15T10:30:00Z', amount: 2, unit: 'months', }); expect(result.original).toBe('2025-01-15T10:30:00.000Z'); expect(result.result).toBe('2024-11-15T10:30:00.000Z'); }); it('should subtract days from a date', () => { mockedCache.get.mockReturnValue(undefined); const result = subtractTime({ time: '2025-01-15T10:30:00Z', amount: 10, unit: 'days', }); expect(result.original).toBe('2025-01-15T10:30:00.000Z'); expect(result.result).toBe('2025-01-05T10:30:00.000Z'); }); it('should subtract hours from a date', () => { mockedCache.get.mockReturnValue(undefined); const result = subtractTime({ time: '2025-01-15T10:30:00Z', amount: 5, unit: 'hours', }); expect(result.original).toBe('2025-01-15T10:30:00.000Z'); expect(result.result).toBe('2025-01-15T05:30:00.000Z'); }); it('should subtract minutes from a date', () => { mockedCache.get.mockReturnValue(undefined); const result = subtractTime({ time: '2025-01-15T10:30:00Z', amount: 45, unit: 'minutes', }); expect(result.original).toBe('2025-01-15T10:30:00.000Z'); expect(result.result).toBe('2025-01-15T09:45:00.000Z'); }); it('should subtract seconds from a date', () => { mockedCache.get.mockReturnValue(undefined); const result = subtractTime({ time: '2025-01-15T10:30:00Z', amount: 30, unit: 'seconds', }); expect(result.original).toBe('2025-01-15T10:30:00.000Z'); expect(result.result).toBe('2025-01-15T10:29:30.000Z'); }); }); describe('Timezone handling', () => { it('should handle timezone-aware subtraction', () => { mockedCache.get.mockReturnValue(undefined); const result = subtractTime({ time: '2025-01-15T10:30:00', amount: 1, unit: 'days', timezone: 'America/New_York', }); expect(result.original).toBe('2025-01-15T10:30:00.000-05:00'); expect(result.result).toBe('2025-01-14T10:30:00.000-05:00'); }); it('should handle DST transitions when subtracting across boundaries', () => { mockedCache.get.mockReturnValue(undefined); // March 15 - 2 months crosses DST boundary const result = subtractTime({ time: '2025-03-15T10:30:00Z', amount: 2, unit: 'months', timezone: 'UTC', }); expect(result.original).toBe('2025-03-15T10:30:00.000Z'); expect(result.result).toBe('2025-01-15T11:30:00.000Z'); // DST adjustment }); it('should handle input with explicit timezone offset', () => { mockedCache.get.mockReturnValue(undefined); const result = subtractTime({ time: '2025-01-15T12:00:00+05:30', amount: 1, unit: 'days', timezone: 'America/New_York', // Should be ignored }); expect(result.original).toBe('2025-01-15T12:00:00.000+05:30'); expect(result.result).toBe('2025-01-14T12:00:00.000+05:30'); }); it('should display result in requested timezone', () => { mockedCache.get.mockReturnValue(undefined); const result = subtractTime({ time: '2025-01-15T10:00:00Z', amount: 5, unit: 'hours', timezone: 'Asia/Tokyo', }); expect(result.original).toBe('2025-01-15T19:00:00.000+09:00'); expect(result.result).toBe('2025-01-15T14:00:00.000+09:00'); }); }); describe('Edge cases', () => { it('should handle negative amounts (addition)', () => { mockedCache.get.mockReturnValue(undefined); const result = subtractTime({ time: '2025-01-15T10:30:00Z', amount: -10, unit: 'days', }); expect(result.original).toBe('2025-01-15T10:30:00.000Z'); expect(result.result).toBe('2025-01-25T10:30:00.000Z'); // Added 10 days }); it('should handle month-end correctly', () => { mockedCache.get.mockReturnValue(undefined); const result = subtractTime({ time: '2025-03-31T12:00:00Z', amount: 1, unit: 'months', }); expect(result.original).toBe('2025-03-31T12:00:00.000Z'); expect(result.result).toBe('2025-02-28T13:00:00.000Z'); // Feb 28, not Mar 3 }); it('should handle leap year correctly', () => { mockedCache.get.mockReturnValue(undefined); const result = subtractTime({ time: '2024-02-29T12:00:00Z', amount: 1, unit: 'years', }); expect(result.original).toBe('2024-02-29T12:00:00.000Z'); expect(result.result).toBe('2023-02-28T12:00:00.000Z'); // Feb 28 in non-leap year }); it('should handle date-only input', () => { mockedCache.get.mockReturnValue(undefined); const result = subtractTime({ time: '2025-01-15', amount: 1, unit: 'days', timezone: 'UTC', }); expect(result.original).toBe('2025-01-15T00:00:00.000Z'); expect(result.result).toBe('2025-01-14T00:00:00.000Z'); }); it('should handle Unix timestamp input', () => { mockedCache.get.mockReturnValue(undefined); const result = subtractTime({ time: '1736937000', // 2025-01-15T10:30:00Z amount: 1, unit: 'hours', }); expect(result.original).toBe('2025-01-15T10:30:00.000Z'); expect(result.result).toBe('2025-01-15T09:30:00.000Z'); expect(result.unix_result).toBe(1736933400); }); it('should handle zero amount', () => { mockedCache.get.mockReturnValue(undefined); const result = subtractTime({ time: '2025-01-15T10:30:00Z', amount: 0, unit: 'days', }); expect(result.original).toBe('2025-01-15T10:30:00.000Z'); expect(result.result).toBe('2025-01-15T10:30:00.000Z'); expect(result.unix_original).toBe(result.unix_result); }); it('should handle year boundary crossing', () => { mockedCache.get.mockReturnValue(undefined); const result = subtractTime({ time: '2025-01-05T10:30:00Z', amount: 10, unit: 'days', }); expect(result.original).toBe('2025-01-05T10:30:00.000Z'); expect(result.result).toBe('2024-12-26T10:30:00.000Z'); // Previous year }); }); describe('Error handling', () => { it('should throw error for invalid time format', () => { mockedCache.get.mockReturnValue(undefined); expect(() => subtractTime({ time: 'not-a-date', amount: 1, unit: 'days', }) ).toThrow(); try { subtractTime({ time: 'not-a-date', amount: 1, unit: 'days', }); } catch (error: any) { expect(error).toBeInstanceOf(DateParsingError); expect(error.code).toBe('DATE_PARSING_ERROR'); expect(error.message).toContain('Invalid'); expect(error.details).toBeDefined(); } }); it('should throw error for invalid unit', () => { mockedCache.get.mockReturnValue(undefined); expect(() => subtractTime({ time: '2025-01-15T10:30:00Z', amount: 1, unit: 'fortnights' as any, }) ).toThrow(); try { subtractTime({ time: '2025-01-15T10:30:00Z', amount: 1, unit: 'fortnights' as any, }); } catch (error: any) { expect(error).toBeInstanceOf(ValidationError); expect(error.code).toBe('VALIDATION_ERROR'); expect(error.message).toContain('Invalid'); expect(error.details).toBeDefined(); } }); it('should throw error for invalid timezone', () => { mockedCache.get.mockReturnValue(undefined); expect(() => subtractTime({ time: '2025-01-15T10:30:00', amount: 1, unit: 'days', timezone: 'Invalid/Zone', }) ).toThrow(); try { subtractTime({ time: '2025-01-15T10:30:00', amount: 1, unit: 'days', timezone: 'Invalid/Zone', }); } catch (error: any) { expect(error).toBeInstanceOf(TimezoneError); expect(error.code).toBe('TIMEZONE_ERROR'); expect(error.message).toContain('Invalid timezone'); expect(error.invalidTimezone).toBe('Invalid/Zone'); } }); it('should throw error for NaN amount', () => { mockedCache.get.mockReturnValue(undefined); expect(() => subtractTime({ time: '2025-01-15T10:30:00Z', amount: NaN, unit: 'days', }) ).toThrow(); try { subtractTime({ time: '2025-01-15T10:30:00Z', amount: NaN, unit: 'days', }); } catch (error: any) { expect(error).toBeInstanceOf(ValidationError); expect(error.code).toBe('VALIDATION_ERROR'); expect(error.message).toContain('Invalid'); expect(error.details).toBeDefined(); } }); }); describe('Caching', () => { it('should cache results for 1 hour', () => { mockedCache.get.mockReturnValue(undefined); subtractTime({ time: '2025-01-15T10:30:00Z', amount: 1, unit: 'days', }); expect(mockedCache.set).toHaveBeenCalledWith( expect.stringMatching(/^[a-f0-9]{64}$/), expect.any(Object), 3600 // 1 hour ); }); it('should return cached result if available', () => { const cachedResult: SubtractTimeResult = { original: '2025-01-15T10:30:00.000Z', result: '2025-01-14T10:30:00.000Z', unix_original: 1736937000, unix_result: 1736850600, }; mockedCache.get.mockReturnValue(cachedResult); const result = subtractTime({ time: '2025-01-15T10:30:00Z', amount: 1, unit: 'days', }); expect(result).toEqual(cachedResult); expect(mockedCache.set).not.toHaveBeenCalled(); }); it('should use system timezone in cache key when no timezone specified', () => { mockedCache.get.mockReturnValue(undefined); mockedGetConfig.mockReturnValue({ defaultTimezone: 'America/New_York' }); subtractTime({ time: '2025-01-15T10:30:00', amount: 2, unit: 'hours', }); // Cache key should include the system timezone expect(mockedCache.set).toHaveBeenCalledWith( expect.stringMatching(/^[a-f0-9]{64}$/), expect.any(Object), 3600 ); }); it('should use different cache keys for different parameters', () => { mockedCache.get.mockReturnValue(undefined); subtractTime({ time: '2025-01-15T10:30:00Z', amount: 1, unit: 'days', }); subtractTime({ time: '2025-01-15T10:30:00Z', amount: 1, unit: 'days', timezone: 'America/New_York', }); const calls = mockedCache.set.mock.calls; expect(calls[0][0]).not.toBe(calls[1][0]); // Different cache keys }); }); describe('Output format verification', () => { it('should always include all required fields', () => { mockedCache.get.mockReturnValue(undefined); const result = subtractTime({ time: '2025-01-15T10:30:00Z', amount: 1, unit: 'hours', }); // All fields must be present expect(result).toHaveProperty('original'); expect(result).toHaveProperty('result'); expect(result).toHaveProperty('unix_original'); expect(result).toHaveProperty('unix_result'); // Types should be correct expect(typeof result.original).toBe('string'); expect(typeof result.result).toBe('string'); expect(typeof result.unix_original).toBe('number'); expect(typeof result.unix_result).toBe('number'); }); it('should format times consistently with milliseconds', () => { mockedCache.get.mockReturnValue(undefined); const result = subtractTime({ time: '2025-01-15T10:30:00', amount: 1, unit: 'days', timezone: 'America/New_York', }); // Should have .000 milliseconds format expect(result.original).toMatch(/\.\d{3}/); expect(result.result).toMatch(/\.\d{3}/); }); }); describe('System timezone defaults', () => { it('should use system timezone when no timezone parameter provided', () => { mockedCache.get.mockReturnValue(undefined); mockedGetConfig.mockReturnValue({ defaultTimezone: 'America/New_York' }); const result = subtractTime({ time: '2025-01-20T12:00:00', amount: 1, unit: 'hours', }); // Should interpret input time as America/New_York time expect(result.original).toBe('2025-01-20T12:00:00.000-05:00'); expect(result.result).toBe('2025-01-20T11:00:00.000-05:00'); }); it('should use explicit timezone parameter over system default', () => { mockedCache.get.mockReturnValue(undefined); mockedGetConfig.mockReturnValue({ defaultTimezone: 'America/New_York' }); const result = subtractTime({ time: '2025-01-20T12:00:00', amount: 1, unit: 'hours', timezone: 'Asia/Tokyo', }); // Should use Asia/Tokyo, not the system default expect(result.original).toBe('2025-01-20T12:00:00.000+09:00'); expect(result.result).toBe('2025-01-20T11:00:00.000+09:00'); }); it('should use UTC when empty string timezone provided', () => { mockedCache.get.mockReturnValue(undefined); mockedGetConfig.mockReturnValue({ defaultTimezone: 'America/New_York' }); const result = subtractTime({ time: '2025-01-20T12:00:00', amount: 1, unit: 'hours', timezone: '', }); // Empty string should mean UTC for backward compatibility expect(result.original).toBe('2025-01-20T12:00:00.000Z'); expect(result.result).toBe('2025-01-20T11:00:00.000Z'); }); }); });

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/pshempel/mcp-time-server-node'

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