import { isValidCronExpression, parseCronExpression } from './cron-parser' import { getNextExecutionTime, getNextExecutionTimes } from './execution-time-calculator' import type { ScheduleTriggerNodeType } from '../types' // Comprehensive integration tests for cron-parser and execution-time-calculator compatibility describe('cron-parser + execution-time-calculator integration', () => { beforeAll(() => { jest.useFakeTimers() jest.setSystemTime(new Date('2024-01-15T10:00:00Z')) }) afterAll(() => { jest.useRealTimers() }) const createCronData = (overrides: Partial = {}): ScheduleTriggerNodeType => ({ id: 'test-cron', type: 'schedule-trigger', mode: 'cron', frequency: 'daily', timezone: 'UTC', ...overrides, }) describe('backward compatibility validation', () => { it('maintains exact behavior for legacy cron expressions', () => { const legacyExpressions = [ '15 10 1 * *', // Monthly 1st at 10:15 '0 0 * * 0', // Weekly Sunday midnight '*/5 * * * *', // Every 5 minutes '0 9-17 * * 1-5', // Business hours weekdays '30 14 * * 1', // Monday 14:30 '0 0 1,15 * *', // 1st and 15th midnight ] legacyExpressions.forEach((expression) => { // Test direct cron-parser usage const directResult = parseCronExpression(expression, 'UTC') expect(directResult).toHaveLength(5) expect(isValidCronExpression(expression)).toBe(true) // Test through execution-time-calculator const data = createCronData({ cron_expression: expression }) const calculatorResult = getNextExecutionTimes(data, 5) expect(calculatorResult).toHaveLength(5) // Results should be identical directResult.forEach((directDate, index) => { const calcDate = calculatorResult[index] expect(calcDate.getTime()).toBe(directDate.getTime()) expect(calcDate.getHours()).toBe(directDate.getHours()) expect(calcDate.getMinutes()).toBe(directDate.getMinutes()) }) }) }) it('validates timezone handling consistency', () => { const timezones = ['UTC', 'America/New_York', 'Asia/Tokyo', 'Europe/London'] const expression = '0 12 * * *' // Daily noon timezones.forEach((timezone) => { // Direct cron-parser call const directResult = parseCronExpression(expression, timezone) // Through execution-time-calculator const data = createCronData({ cron_expression: expression, timezone }) const calculatorResult = getNextExecutionTimes(data, 5) expect(directResult).toHaveLength(5) expect(calculatorResult).toHaveLength(5) // All results should show noon (12:00) in their respective timezone directResult.forEach(date => expect(date.getHours()).toBe(12)) calculatorResult.forEach(date => expect(date.getHours()).toBe(12)) // Cross-validation: results should be identical directResult.forEach((directDate, index) => { expect(calculatorResult[index].getTime()).toBe(directDate.getTime()) }) }) }) it('error handling consistency', () => { const invalidExpressions = [ '', // Empty string ' ', // Whitespace only '60 10 1 * *', // Invalid minute '15 25 1 * *', // Invalid hour '15 10 32 * *', // Invalid day '15 10 1 13 *', // Invalid month '15 10 1', // Too few fields '15 10 1 * * *', // Too many fields 'invalid expression', // Completely invalid ] invalidExpressions.forEach((expression) => { // Direct cron-parser calls expect(isValidCronExpression(expression)).toBe(false) expect(parseCronExpression(expression, 'UTC')).toEqual([]) // Through execution-time-calculator const data = createCronData({ cron_expression: expression }) const result = getNextExecutionTimes(data, 5) expect(result).toEqual([]) // getNextExecutionTime should return '--' for invalid cron const timeString = getNextExecutionTime(data) expect(timeString).toBe('--') }) }) }) describe('enhanced features integration', () => { it('month and day abbreviations work end-to-end', () => { const enhancedExpressions = [ { expr: '0 9 1 JAN *', month: 0, day: 1, hour: 9 }, // January 1st 9 AM { expr: '0 15 * * MON', weekday: 1, hour: 15 }, // Monday 3 PM { expr: '30 10 15 JUN,DEC *', month: [5, 11], day: 15, hour: 10, minute: 30 }, // Jun/Dec 15th { expr: '0 12 * JAN-MAR *', month: [0, 1, 2], hour: 12 }, // Q1 noon ] enhancedExpressions.forEach(({ expr, month, day, weekday, hour, minute = 0 }) => { // Validate through both paths expect(isValidCronExpression(expr)).toBe(true) const directResult = parseCronExpression(expr, 'UTC') const data = createCronData({ cron_expression: expr }) const calculatorResult = getNextExecutionTimes(data, 3) expect(directResult.length).toBeGreaterThan(0) expect(calculatorResult.length).toBeGreaterThan(0) // Validate expected properties const validateDate = (date: Date) => { expect(date.getHours()).toBe(hour) expect(date.getMinutes()).toBe(minute) if (month !== undefined) { if (Array.isArray(month)) expect(month).toContain(date.getMonth()) else expect(date.getMonth()).toBe(month) } if (day !== undefined) expect(date.getDate()).toBe(day) if (weekday !== undefined) expect(date.getDay()).toBe(weekday) } directResult.forEach(validateDate) calculatorResult.forEach(validateDate) }) }) it('predefined expressions work through execution-time-calculator', () => { const predefExpressions = [ { expr: '@daily', hour: 0, minute: 0 }, { expr: '@weekly', hour: 0, minute: 0, weekday: 0 }, // Sunday { expr: '@monthly', hour: 0, minute: 0, day: 1 }, // 1st of month { expr: '@yearly', hour: 0, minute: 0, month: 0, day: 1 }, // Jan 1st ] predefExpressions.forEach(({ expr, hour, minute, weekday, day, month }) => { expect(isValidCronExpression(expr)).toBe(true) const data = createCronData({ cron_expression: expr }) const result = getNextExecutionTimes(data, 3) expect(result.length).toBeGreaterThan(0) result.forEach((date) => { expect(date.getHours()).toBe(hour) expect(date.getMinutes()).toBe(minute) if (weekday !== undefined) expect(date.getDay()).toBe(weekday) if (day !== undefined) expect(date.getDate()).toBe(day) if (month !== undefined) expect(date.getMonth()).toBe(month) }) }) }) it('special characters integration', () => { const specialExpressions = [ '0 9 ? * 1', // ? wildcard for day '0 12 * * 7', // Sunday as 7 '0 15 L * *', // Last day of month ] specialExpressions.forEach((expr) => { // Should validate and parse successfully expect(isValidCronExpression(expr)).toBe(true) const directResult = parseCronExpression(expr, 'UTC') const data = createCronData({ cron_expression: expr }) const calculatorResult = getNextExecutionTimes(data, 2) expect(directResult.length).toBeGreaterThan(0) expect(calculatorResult.length).toBeGreaterThan(0) // Results should be consistent expect(calculatorResult[0].getHours()).toBe(directResult[0].getHours()) expect(calculatorResult[0].getMinutes()).toBe(directResult[0].getMinutes()) }) }) }) describe('DST and timezone edge cases', () => { it('handles DST transitions consistently', () => { // Test around DST spring forward (March 2024) jest.setSystemTime(new Date('2024-03-08T10:00:00Z')) const expression = '0 2 * * *' // 2 AM daily (problematic during DST) const timezone = 'America/New_York' const directResult = parseCronExpression(expression, timezone) const data = createCronData({ cron_expression: expression, timezone }) const calculatorResult = getNextExecutionTimes(data, 5) expect(directResult.length).toBeGreaterThan(0) expect(calculatorResult.length).toBeGreaterThan(0) // Both should handle DST gracefully // During DST spring forward, 2 AM becomes 3 AM - this is correct behavior directResult.forEach(date => expect([2, 3]).toContain(date.getHours())) calculatorResult.forEach(date => expect([2, 3]).toContain(date.getHours())) // Results should be identical directResult.forEach((directDate, index) => { expect(calculatorResult[index].getTime()).toBe(directDate.getTime()) }) }) it('complex timezone scenarios', () => { const scenarios = [ { tz: 'Asia/Kolkata', expr: '30 14 * * *', expectedHour: 14, expectedMinute: 30 }, // UTC+5:30 { tz: 'Australia/Adelaide', expr: '0 8 * * *', expectedHour: 8, expectedMinute: 0 }, // UTC+9:30/+10:30 { tz: 'Pacific/Kiritimati', expr: '0 12 * * *', expectedHour: 12, expectedMinute: 0 }, // UTC+14 ] scenarios.forEach(({ tz, expr, expectedHour, expectedMinute }) => { const directResult = parseCronExpression(expr, tz) const data = createCronData({ cron_expression: expr, timezone: tz }) const calculatorResult = getNextExecutionTimes(data, 2) expect(directResult.length).toBeGreaterThan(0) expect(calculatorResult.length).toBeGreaterThan(0) // Validate expected time directResult.forEach((date) => { expect(date.getHours()).toBe(expectedHour) expect(date.getMinutes()).toBe(expectedMinute) }) calculatorResult.forEach((date) => { expect(date.getHours()).toBe(expectedHour) expect(date.getMinutes()).toBe(expectedMinute) }) // Cross-validate consistency expect(calculatorResult[0].getTime()).toBe(directResult[0].getTime()) }) }) }) describe('performance and reliability', () => { it('handles high-frequency expressions efficiently', () => { const highFreqExpressions = [ '*/1 * * * *', // Every minute '*/5 * * * *', // Every 5 minutes '0,15,30,45 * * * *', // Every 15 minutes ] highFreqExpressions.forEach((expr) => { const start = performance.now() // Test both direct and through calculator const directResult = parseCronExpression(expr, 'UTC') const data = createCronData({ cron_expression: expr }) const calculatorResult = getNextExecutionTimes(data, 5) const end = performance.now() expect(directResult).toHaveLength(5) expect(calculatorResult).toHaveLength(5) expect(end - start).toBeLessThan(100) // Should be fast // Results should be consistent directResult.forEach((directDate, index) => { expect(calculatorResult[index].getTime()).toBe(directDate.getTime()) }) }) }) it('stress test with complex expressions', () => { const complexExpressions = [ '15,45 8-18 1,15 JAN-MAR MON-FRI', // Business hours, specific days, Q1, weekdays '0 */2 ? * SUN#1,SUN#3', // First and third Sunday, every 2 hours '30 9 L * *', // Last day of month, 9:30 AM ] complexExpressions.forEach((expr) => { if (isValidCronExpression(expr)) { const directResult = parseCronExpression(expr, 'America/New_York') const data = createCronData({ cron_expression: expr, timezone: 'America/New_York', }) const calculatorResult = getNextExecutionTimes(data, 3) expect(directResult.length).toBeGreaterThan(0) expect(calculatorResult.length).toBeGreaterThan(0) // Validate consistency where results exist const minLength = Math.min(directResult.length, calculatorResult.length) for (let i = 0; i < minLength; i++) expect(calculatorResult[i].getTime()).toBe(directResult[i].getTime()) } }) }) }) describe('format compatibility', () => { it('getNextExecutionTime formatting consistency', () => { const testCases = [ { expr: '0 9 * * *', timezone: 'UTC' }, { expr: '30 14 * * 1-5', timezone: 'America/New_York' }, { expr: '@daily', timezone: 'Asia/Tokyo' }, ] testCases.forEach(({ expr, timezone }) => { const data = createCronData({ cron_expression: expr, timezone }) const timeString = getNextExecutionTime(data) // Should return a formatted time string, not '--' expect(timeString).not.toBe('--') expect(typeof timeString).toBe('string') expect(timeString.length).toBeGreaterThan(0) // Should contain expected format elements expect(timeString).toMatch(/\d+:\d+/) // Time format expect(timeString).toMatch(/AM|PM/) // 12-hour format expect(timeString).toMatch(/\d{4}/) // Year }) }) }) })