Skip to content

Commit 4e6a05c

Browse files
Create utility for validating cron string (#1034)
* utility for validating cron string Signed-off-by: Assem Hafez <[email protected]> * Rename test file Signed-off-by: Assem Hafez <[email protected]> * fix type check Signed-off-by: Assem Hafez <[email protected]> * fix type check Signed-off-by: Assem Hafez <[email protected]> * fix type check Signed-off-by: Assem Hafez <[email protected]> * fix type check Signed-off-by: Assem Hafez <[email protected]> --------- Signed-off-by: Assem Hafez <[email protected]> Co-authored-by: Adhitya Mamallan <[email protected]>
1 parent c140c96 commit 4e6a05c

File tree

6 files changed

+312
-0
lines changed

6 files changed

+312
-0
lines changed

package-lock.json

Lines changed: 52 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
"@tanstack/react-query-next-experimental": "^5.45.0",
4545
"baseui": "^14.0.0",
4646
"copy-to-clipboard": "^3.3.3",
47+
"cron-validate": "^1.5.2",
4748
"dayjs": "^1.11.13",
4849
"lodash": "^4.17.21",
4950
"lossless-json": "^4.0.2",
Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
import cron, { type CronData } from 'cron-validate';
2+
import { type Err, type Valid } from 'cron-validate/lib/result';
3+
import { type InputOptions } from 'cron-validate/lib/types';
4+
5+
import { cronValidate } from '../cron-validate';
6+
import {
7+
CRON_VALIDATE_CADENCE_PRESET_ID,
8+
CRON_VALIDATE_CADENCE_PRESET,
9+
} from '../cron-validate.constants';
10+
11+
// Mock the cron-validate library to test our wrapper behavior
12+
jest.mock('cron-validate');
13+
jest.mock('cron-validate/lib/option');
14+
15+
describe('cronValidate', () => {
16+
beforeEach(() => {
17+
jest.clearAllMocks();
18+
});
19+
20+
it('should have registered the Cadence preset', () => {
21+
// Since the module is already loaded, we can't test the registration call directly
22+
// Instead, we verify that the constants are properly defined and would be used for registration
23+
expect(CRON_VALIDATE_CADENCE_PRESET_ID).toBe('cadence');
24+
expect(CRON_VALIDATE_CADENCE_PRESET).toEqual(
25+
expect.objectContaining({
26+
presetId: 'cadence',
27+
useSeconds: false,
28+
useYears: false,
29+
useAliases: true,
30+
useBlankDay: false,
31+
allowOnlyOneBlankDayField: false,
32+
allowStepping: true,
33+
mustHaveBlankDayField: false,
34+
useLastDayOfMonth: false,
35+
useLastDayOfWeek: false,
36+
useNearestWeekday: false,
37+
useNthWeekdayOfMonth: false,
38+
})
39+
);
40+
});
41+
it('should have correct field validation ranges in preset', () => {
42+
expect(CRON_VALIDATE_CADENCE_PRESET.minutes).toEqual({
43+
minValue: 0,
44+
maxValue: 59,
45+
});
46+
expect(CRON_VALIDATE_CADENCE_PRESET.hours).toEqual({
47+
minValue: 0,
48+
maxValue: 23,
49+
});
50+
expect(CRON_VALIDATE_CADENCE_PRESET.daysOfMonth).toEqual({
51+
minValue: 0,
52+
maxValue: 31,
53+
});
54+
expect(CRON_VALIDATE_CADENCE_PRESET.months).toEqual({
55+
minValue: 0,
56+
maxValue: 12,
57+
});
58+
expect(CRON_VALIDATE_CADENCE_PRESET.daysOfWeek).toEqual({
59+
minValue: 0,
60+
maxValue: 6, // Limited to 6 instead of 7 for Cadence
61+
});
62+
});
63+
64+
it('should call cron-validate with Cadence preset by default', () => {
65+
const { mockResult, mockCron } = setup();
66+
67+
const cronString = '0 12 * * *';
68+
const result = cronValidate(cronString);
69+
70+
expect(mockCron).toHaveBeenCalledWith(cronString, {
71+
preset: CRON_VALIDATE_CADENCE_PRESET_ID,
72+
});
73+
expect(result).toBe(mockResult);
74+
});
75+
76+
it('should merge custom options with preset', () => {
77+
const cronString = '0 12 * * *';
78+
const customOptions = {
79+
override: {
80+
minutes: {
81+
lowerLimit: 5,
82+
upperLimit: 55,
83+
},
84+
},
85+
};
86+
const { mockResult, mockCron, result } = setup({
87+
value: cronString,
88+
options: customOptions,
89+
});
90+
91+
expect(mockCron).toHaveBeenCalledWith(cronString, {
92+
override: {
93+
minutes: {
94+
lowerLimit: 5,
95+
upperLimit: 55,
96+
},
97+
},
98+
preset: CRON_VALIDATE_CADENCE_PRESET_ID,
99+
});
100+
expect(result).toBe(mockResult);
101+
});
102+
103+
it('should return the exact result from cron-validate library', () => {
104+
const { mockResult, result } = setup({
105+
mockIsValid: false,
106+
mockErrors: ['Test error message'],
107+
mockCronValue: {
108+
minutes: '0',
109+
hours: '12',
110+
daysOfMonth: '1',
111+
months: '1',
112+
daysOfWeek: '1',
113+
},
114+
});
115+
116+
expect(result).toBe(mockResult);
117+
});
118+
119+
it('should handle empty options parameter', () => {
120+
const { mockCron } = setup({
121+
value: '*/5 * * * *',
122+
});
123+
124+
expect(mockCron).toHaveBeenCalledWith('*/5 * * * *', {
125+
preset: CRON_VALIDATE_CADENCE_PRESET_ID,
126+
});
127+
});
128+
129+
it('should handle undefined options parameter', () => {
130+
const { mockCron } = setup({
131+
value: '0 0 * * 1-5',
132+
});
133+
134+
expect(mockCron).toHaveBeenCalledWith('0 0 * * 1-5', {
135+
preset: CRON_VALIDATE_CADENCE_PRESET_ID,
136+
});
137+
});
138+
139+
it('should allow overriding the preset when custom preset option is provided', () => {
140+
const options: InputOptions = {
141+
preset: 'some-other-preset',
142+
override: {
143+
minutes: {
144+
lowerLimit: 0,
145+
upperLimit: 30,
146+
},
147+
},
148+
};
149+
const value = '0 12 * * *';
150+
const { mockCron } = setup({
151+
value,
152+
options,
153+
});
154+
155+
expect(mockCron).toHaveBeenCalledWith(value, options);
156+
});
157+
});
158+
159+
type CronValidateResult = Valid<CronData, string[]> | Err<CronData, string[]>;
160+
type SetupOptions = {
161+
mockIsValid?: ReturnType<CronValidateResult['isValid']>;
162+
mockErrors?: ReturnType<CronValidateResult['getError']>;
163+
mockCronValue?: ReturnType<CronValidateResult['getValue']>;
164+
value?: string;
165+
options?: InputOptions;
166+
};
167+
168+
function setup({
169+
mockIsValid = true,
170+
mockErrors = [],
171+
mockCronValue = {
172+
minutes: '0',
173+
hours: '12',
174+
daysOfMonth: '1',
175+
months: '1',
176+
daysOfWeek: '1',
177+
},
178+
value = '0 12 * * *',
179+
options,
180+
}: SetupOptions = {}) {
181+
const mockResult: Partial<CronValidateResult> = {
182+
isValid: jest.fn(() => mockIsValid),
183+
isError: jest.fn(() => !mockIsValid),
184+
getError: jest.fn(() => mockErrors),
185+
getValue: jest.fn(() => mockCronValue),
186+
};
187+
188+
const mockCron = cron as jest.MockedFunction<typeof cron>;
189+
mockCron.mockReturnValue(mockResult as CronValidateResult);
190+
191+
const result = cronValidate(value, options);
192+
193+
return { mockResult, mockCron, cronValidate, result };
194+
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
export const CRON_VALIDATE_CADENCE_PRESET_ID = 'cadence';
2+
3+
export const CRON_VALIDATE_CADENCE_PRESET = {
4+
presetId: CRON_VALIDATE_CADENCE_PRESET_ID,
5+
useSeconds: false,
6+
useYears: false,
7+
useAliases: true,
8+
useBlankDay: false,
9+
allowOnlyOneBlankDayField: false,
10+
allowStepping: true,
11+
mustHaveBlankDayField: false,
12+
useLastDayOfMonth: false,
13+
useLastDayOfWeek: false,
14+
useNearestWeekday: false,
15+
useNthWeekdayOfMonth: false,
16+
seconds: {
17+
minValue: 0,
18+
maxValue: 59,
19+
},
20+
minutes: {
21+
minValue: 0,
22+
maxValue: 59,
23+
},
24+
hours: {
25+
minValue: 0,
26+
maxValue: 23,
27+
},
28+
daysOfMonth: {
29+
minValue: 0,
30+
maxValue: 31,
31+
},
32+
months: {
33+
minValue: 0,
34+
maxValue: 12,
35+
},
36+
daysOfWeek: {
37+
minValue: 0,
38+
maxValue: 6, // limiting to 6 instead of 7, 7 is not standard
39+
},
40+
years: {
41+
minValue: 1970,
42+
maxValue: 2099,
43+
},
44+
};
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import cron from 'cron-validate';
2+
import { registerOptionPreset } from 'cron-validate/lib/option';
3+
import { type InputOptions } from 'cron-validate/lib/types';
4+
5+
import {
6+
CRON_VALIDATE_CADENCE_PRESET,
7+
CRON_VALIDATE_CADENCE_PRESET_ID,
8+
} from './cron-validate.constants';
9+
10+
registerOptionPreset(
11+
CRON_VALIDATE_CADENCE_PRESET_ID,
12+
CRON_VALIDATE_CADENCE_PRESET
13+
);
14+
15+
export const cronValidate = (cronString: string, options?: InputOptions) => {
16+
return cron(cronString, {
17+
preset: CRON_VALIDATE_CADENCE_PRESET_ID,
18+
...options,
19+
});
20+
};
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { type CronData } from 'cron-validate';

0 commit comments

Comments
 (0)