dify/web/app/components/workflow/nodes/trigger-schedule/utils/cron-parser.spec.ts
2025-08-18 09:23:16 +08:00

234 lines
8.4 KiB
TypeScript

import { isValidCronExpression, parseCronExpression } from './cron-parser'
describe('cron-parser', () => {
describe('isValidCronExpression', () => {
test('validates correct cron expressions', () => {
expect(isValidCronExpression('15 10 1 * *')).toBe(true)
expect(isValidCronExpression('0 0 * * 0')).toBe(true)
expect(isValidCronExpression('*/5 * * * *')).toBe(true)
expect(isValidCronExpression('0 9-17 * * 1-5')).toBe(true)
expect(isValidCronExpression('30 14 * * 1')).toBe(true)
expect(isValidCronExpression('0 0 1,15 * *')).toBe(true)
})
test('rejects invalid cron expressions', () => {
expect(isValidCronExpression('')).toBe(false)
expect(isValidCronExpression('15 10 1')).toBe(false) // Not enough fields
expect(isValidCronExpression('15 10 1 * * *')).toBe(false) // Too many fields
expect(isValidCronExpression('60 10 1 * *')).toBe(false) // Invalid minute
expect(isValidCronExpression('15 25 1 * *')).toBe(false) // Invalid hour
expect(isValidCronExpression('15 10 32 * *')).toBe(false) // Invalid day
expect(isValidCronExpression('15 10 1 13 *')).toBe(false) // Invalid month
expect(isValidCronExpression('15 10 1 * 7')).toBe(false) // Invalid day of week
})
test('handles edge cases', () => {
expect(isValidCronExpression(' 15 10 1 * * ')).toBe(true) // Whitespace
expect(isValidCronExpression('0 0 29 2 *')).toBe(true) // Feb 29 (valid in leap years)
expect(isValidCronExpression('59 23 31 12 6')).toBe(true) // Max values
})
})
describe('parseCronExpression', () => {
beforeEach(() => {
// Mock current time to make tests deterministic
jest.useFakeTimers()
jest.setSystemTime(new Date('2024-01-15T10:00:00Z'))
})
afterEach(() => {
jest.useRealTimers()
})
test('parses monthly expressions correctly', () => {
const result = parseCronExpression('15 10 1 * *') // 1st day of every month at 10:15
expect(result).toHaveLength(5)
expect(result[0].getDate()).toBe(1) // February 1st
expect(result[0].getHours()).toBe(10)
expect(result[0].getMinutes()).toBe(15)
expect(result[1].getDate()).toBe(1) // March 1st
expect(result[2].getDate()).toBe(1) // April 1st
})
test('parses weekly expressions correctly', () => {
const result = parseCronExpression('30 14 * * 1') // Every Monday at 14:30
expect(result).toHaveLength(5)
// Should find next 5 Mondays
result.forEach((date) => {
expect(date.getDay()).toBe(1) // Monday
expect(date.getHours()).toBe(14)
expect(date.getMinutes()).toBe(30)
})
})
test('parses daily expressions correctly', () => {
const result = parseCronExpression('0 9 * * *') // Every day at 9:00
expect(result).toHaveLength(5)
result.forEach((date) => {
expect(date.getHours()).toBe(9)
expect(date.getMinutes()).toBe(0)
})
// Should be consecutive days (starting from tomorrow since current time is 10:00)
for (let i = 1; i < result.length; i++) {
const prevDate = new Date(result[i - 1])
const currDate = new Date(result[i])
const dayDiff = (currDate.getTime() - prevDate.getTime()) / (1000 * 60 * 60 * 24)
expect(dayDiff).toBe(1)
}
})
test('handles complex cron expressions with ranges', () => {
const result = parseCronExpression('0 9-17 * * 1-5') // Weekdays, 9-17 hours
expect(result).toHaveLength(5)
result.forEach((date) => {
expect(date.getDay()).toBeGreaterThanOrEqual(1) // Monday
expect(date.getDay()).toBeLessThanOrEqual(5) // Friday
expect(date.getHours()).toBeGreaterThanOrEqual(9)
expect(date.getHours()).toBeLessThanOrEqual(17)
expect(date.getMinutes()).toBe(0)
})
})
test('handles step expressions', () => {
const result = parseCronExpression('*/15 * * * *') // Every 15 minutes
expect(result).toHaveLength(5)
result.forEach((date) => {
expect(date.getMinutes() % 15).toBe(0)
})
})
test('handles list expressions', () => {
const result = parseCronExpression('0 0 1,15 * *') // 1st and 15th of each month
expect(result).toHaveLength(5)
result.forEach((date) => {
expect([1, 15]).toContain(date.getDate())
expect(date.getHours()).toBe(0)
expect(date.getMinutes()).toBe(0)
})
})
test('handles expressions that span multiple months', () => {
// Test with an expression that might not have many matches in current month
const result = parseCronExpression('0 12 29 * *') // 29th of each month at noon
expect(result.length).toBeGreaterThan(0)
expect(result.length).toBeLessThanOrEqual(5)
result.forEach((date) => {
expect(date.getDate()).toBe(29)
expect(date.getHours()).toBe(12)
expect(date.getMinutes()).toBe(0)
})
})
test('returns empty array for invalid expressions', () => {
expect(parseCronExpression('')).toEqual([])
expect(parseCronExpression('invalid')).toEqual([])
expect(parseCronExpression('60 10 1 * *')).toEqual([])
expect(parseCronExpression('15 25 1 * *')).toEqual([])
})
test('handles edge case: February 29th in non-leap years', () => {
// Set to a non-leap year
jest.setSystemTime(new Date('2023-01-15T10:00:00Z'))
const result = parseCronExpression('0 12 29 2 *') // Feb 29th at noon
// Should return empty or skip 2023 and find 2024
if (result.length > 0) {
result.forEach((date) => {
expect(date.getMonth()).toBe(1) // February
expect(date.getDate()).toBe(29)
// Should be in a leap year
const year = date.getFullYear()
expect(year % 4).toBe(0)
})
}
})
test('sorts results chronologically', () => {
const result = parseCronExpression('0 */6 * * *') // Every 6 hours
expect(result).toHaveLength(5)
for (let i = 1; i < result.length; i++)
expect(result[i].getTime()).toBeGreaterThan(result[i - 1].getTime())
})
test('excludes past times', () => {
// Set current time to 15:30
jest.setSystemTime(new Date('2024-01-15T15:30:00Z'))
const result = parseCronExpression('0 10 * * *') // Daily at 10:00
expect(result).toHaveLength(5)
result.forEach((date) => {
expect(date.getTime()).toBeGreaterThan(Date.now())
})
// First result should be tomorrow since today's 10:00 has passed
expect(result[0].getDate()).toBe(16)
})
test('handles midnight expressions correctly', () => {
const result = parseCronExpression('0 0 * * *') // Daily at midnight
expect(result).toHaveLength(5)
result.forEach((date) => {
expect(date.getHours()).toBe(0)
expect(date.getMinutes()).toBe(0)
})
})
test('handles year boundary correctly', () => {
// Set to end of December
jest.setSystemTime(new Date('2024-12-30T10:00:00Z'))
const result = parseCronExpression('0 12 1 * *') // 1st of every month at noon
expect(result).toHaveLength(5)
// Should include January 1st of next year
const nextYear = result.find(date => date.getFullYear() === 2025)
expect(nextYear).toBeDefined()
if (nextYear) {
expect(nextYear.getMonth()).toBe(0) // January
expect(nextYear.getDate()).toBe(1)
}
})
})
describe('performance tests', () => {
test('performs well for complex expressions', () => {
const start = performance.now()
// Test multiple complex expressions
const expressions = [
'*/5 9-17 * * 1-5', // Every 5 minutes, weekdays, business hours
'0 */2 1,15 * *', // Every 2 hours on 1st and 15th
'30 14 * * 1,3,5', // Mon, Wed, Fri at 14:30
'15,45 8-18 * * 1-5', // 15 and 45 minutes past the hour, weekdays
]
expressions.forEach((expr) => {
const result = parseCronExpression(expr)
expect(result).toHaveLength(5)
})
// Test quarterly expression separately (may return fewer than 5 results)
const quarterlyResult = parseCronExpression('0 0 1 */3 *') // First day of every 3rd month
expect(quarterlyResult.length).toBeGreaterThan(0)
expect(quarterlyResult.length).toBeLessThanOrEqual(5)
const end = performance.now()
// Should complete within reasonable time (less than 100ms for all expressions)
expect(end - start).toBeLessThan(100)
})
})
})