@@ -11,6 +11,7 @@ import {
1111 getSubBlockValue ,
1212 parseCronToHumanReadable ,
1313 parseTimeString ,
14+ validateCronExpression ,
1415} from '@/lib/schedules/utils'
1516
1617describe ( 'Schedule Utilities' , ( ) => {
@@ -102,6 +103,7 @@ describe('Schedule Utilities', () => {
102103 weeklyTime : [ 12 , 0 ] ,
103104 monthlyDay : 15 ,
104105 monthlyTime : [ 14 , 30 ] ,
106+ cronExpression : null ,
105107 } )
106108 } )
107109
@@ -127,6 +129,7 @@ describe('Schedule Utilities', () => {
127129 weeklyTime : [ 9 , 0 ] , // Default
128130 monthlyDay : 1 , // Default
129131 monthlyTime : [ 9 , 0 ] , // Default
132+ cronExpression : null ,
130133 } )
131134 } )
132135 } )
@@ -143,6 +146,7 @@ describe('Schedule Utilities', () => {
143146 monthlyDay : 15 ,
144147 monthlyTime : [ 14 , 30 ] as [ number , number ] ,
145148 timezone : 'UTC' ,
149+ cronExpression : null ,
146150 }
147151
148152 // Minutes (every 15 minutes)
@@ -196,6 +200,7 @@ describe('Schedule Utilities', () => {
196200 monthlyDay : 15 ,
197201 monthlyTime : [ 14 , 30 ] as [ number , number ] ,
198202 timezone : 'UTC' ,
203+ cronExpression : null ,
199204 }
200205
201206 expect ( generateCronExpression ( 'minutes' , standardScheduleValues ) ) . toBe ( '*/15 * * * *' )
@@ -230,6 +235,7 @@ describe('Schedule Utilities', () => {
230235 weeklyTime : [ 9 , 0 ] as [ number , number ] ,
231236 monthlyDay : 1 ,
232237 monthlyTime : [ 9 , 0 ] as [ number , number ] ,
238+ cronExpression : null ,
233239 }
234240
235241 const nextRun = calculateNextRunTime ( 'minutes' , scheduleValues )
@@ -254,6 +260,7 @@ describe('Schedule Utilities', () => {
254260 weeklyTime : [ 9 , 0 ] as [ number , number ] ,
255261 monthlyDay : 1 ,
256262 monthlyTime : [ 9 , 0 ] as [ number , number ] ,
263+ cronExpression : null ,
257264 }
258265
259266 const nextRun = calculateNextRunTime ( 'minutes' , scheduleValues )
@@ -275,6 +282,7 @@ describe('Schedule Utilities', () => {
275282 weeklyTime : [ 9 , 0 ] as [ number , number ] ,
276283 monthlyDay : 1 ,
277284 monthlyTime : [ 9 , 0 ] as [ number , number ] ,
285+ cronExpression : null ,
278286 }
279287
280288 const nextRun = calculateNextRunTime ( 'hourly' , scheduleValues )
@@ -297,6 +305,7 @@ describe('Schedule Utilities', () => {
297305 weeklyTime : [ 9 , 0 ] as [ number , number ] ,
298306 monthlyDay : 1 ,
299307 monthlyTime : [ 9 , 0 ] as [ number , number ] ,
308+ cronExpression : null ,
300309 }
301310
302311 const nextRun = calculateNextRunTime ( 'daily' , scheduleValues )
@@ -320,6 +329,7 @@ describe('Schedule Utilities', () => {
320329 weeklyTime : [ 10 , 0 ] as [ number , number ] ,
321330 monthlyDay : 1 ,
322331 monthlyTime : [ 9 , 0 ] as [ number , number ] ,
332+ cronExpression : null ,
323333 }
324334
325335 const nextRun = calculateNextRunTime ( 'weekly' , scheduleValues )
@@ -342,6 +352,7 @@ describe('Schedule Utilities', () => {
342352 weeklyTime : [ 9 , 0 ] as [ number , number ] ,
343353 monthlyDay : 15 ,
344354 monthlyTime : [ 14 , 30 ] as [ number , number ] ,
355+ cronExpression : null ,
345356 }
346357
347358 const nextRun = calculateNextRunTime ( 'monthly' , scheduleValues )
@@ -366,6 +377,7 @@ describe('Schedule Utilities', () => {
366377 weeklyTime : [ 9 , 0 ] as [ number , number ] ,
367378 monthlyDay : 1 ,
368379 monthlyTime : [ 9 , 0 ] as [ number , number ] ,
380+ cronExpression : null ,
369381 }
370382
371383 // Last ran 10 minutes ago
@@ -393,6 +405,7 @@ describe('Schedule Utilities', () => {
393405 weeklyTime : [ 9 , 0 ] as [ number , number ] ,
394406 monthlyDay : 1 ,
395407 monthlyTime : [ 9 , 0 ] as [ number , number ] ,
408+ cronExpression : null ,
396409 }
397410
398411 const nextRun = calculateNextRunTime ( 'minutes' , scheduleValues )
@@ -413,6 +426,7 @@ describe('Schedule Utilities', () => {
413426 weeklyTime : [ 9 , 0 ] as [ number , number ] ,
414427 monthlyDay : 1 ,
415428 monthlyTime : [ 9 , 0 ] as [ number , number ] ,
429+ cronExpression : null ,
416430 }
417431
418432 const nextRun = calculateNextRunTime ( 'minutes' , scheduleValues )
@@ -423,6 +437,50 @@ describe('Schedule Utilities', () => {
423437 } )
424438 } )
425439
440+ describe ( 'validateCronExpression' , ( ) => {
441+ it . concurrent ( 'should validate correct cron expressions' , ( ) => {
442+ expect ( validateCronExpression ( '0 9 * * *' ) ) . toEqual ( {
443+ isValid : true ,
444+ nextRun : expect . any ( Date ) ,
445+ } )
446+ expect ( validateCronExpression ( '*/15 * * * *' ) ) . toEqual ( {
447+ isValid : true ,
448+ nextRun : expect . any ( Date ) ,
449+ } )
450+ expect ( validateCronExpression ( '30 14 15 * *' ) ) . toEqual ( {
451+ isValid : true ,
452+ nextRun : expect . any ( Date ) ,
453+ } )
454+ } )
455+
456+ it . concurrent ( 'should reject invalid cron expressions' , ( ) => {
457+ expect ( validateCronExpression ( 'invalid' ) ) . toEqual ( {
458+ isValid : false ,
459+ error : expect . stringContaining ( 'invalid' ) ,
460+ } )
461+ expect ( validateCronExpression ( '60 * * * *' ) ) . toEqual ( {
462+ isValid : false ,
463+ error : expect . any ( String ) ,
464+ } )
465+ expect ( validateCronExpression ( '' ) ) . toEqual ( {
466+ isValid : false ,
467+ error : 'Cron expression cannot be empty' ,
468+ } )
469+ expect ( validateCronExpression ( ' ' ) ) . toEqual ( {
470+ isValid : false ,
471+ error : 'Cron expression cannot be empty' ,
472+ } )
473+ } )
474+
475+ it . concurrent ( 'should detect impossible cron expressions' , ( ) => {
476+ // This would be February 31st - impossible date
477+ expect ( validateCronExpression ( '0 0 31 2 *' ) ) . toEqual ( {
478+ isValid : false ,
479+ error : 'Cron expression produces no future occurrences' ,
480+ } )
481+ } )
482+ } )
483+
426484 describe ( 'parseCronToHumanReadable' , ( ) => {
427485 it . concurrent ( 'should parse common cron patterns' , ( ) => {
428486 expect ( parseCronToHumanReadable ( '* * * * *' ) ) . toBe ( 'Every minute' )
0 commit comments