Skip to content

Commit ecc6714

Browse files
author
Bill
committed
test(utils): add cronParser and flattenToLeafPaths tests
1 parent 24195e9 commit ecc6714

3 files changed

Lines changed: 252 additions & 0 deletions

File tree

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { describe, expect, it } from 'vitest';
2+
import { DANGEROUS_KEYS } from '../../src/api/utils/dangerousKeys.js';
3+
4+
describe('dangerousKeys', () => {
5+
it('should contain __proto__', () => {
6+
expect(DANGEROUS_KEYS.has('__proto__')).toBe(true);
7+
});
8+
9+
it('should contain constructor', () => {
10+
expect(DANGEROUS_KEYS.has('constructor')).toBe(true);
11+
});
12+
13+
it('should contain prototype', () => {
14+
expect(DANGEROUS_KEYS.has('prototype')).toBe(true);
15+
});
16+
17+
it('should not contain safe keys', () => {
18+
expect(DANGEROUS_KEYS.has('safeKey')).toBe(false);
19+
expect(DANGEROUS_KEYS.has('name')).toBe(false);
20+
expect(DANGEROUS_KEYS.has('id')).toBe(false);
21+
});
22+
23+
it('should be a Set', () => {
24+
expect(DANGEROUS_KEYS).toBeInstanceOf(Set);
25+
expect(DANGEROUS_KEYS.size).toBe(3);
26+
});
27+
});

tests/utils/cronParser.test.js

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
import { describe, expect, it } from 'vitest';
2+
import { getNextCronRun, parseCron } from '../../src/utils/cronParser.js';
3+
4+
describe('parseCron', () => {
5+
describe('wildcards', () => {
6+
it('should expand * to full range for minute (0-59)', () => {
7+
const result = parseCron('* * * * *');
8+
expect(result.minute).toHaveLength(60);
9+
expect(result.minute[0]).toBe(0);
10+
expect(result.minute[59]).toBe(59);
11+
});
12+
13+
it('should expand * to full range for hour (0-23)', () => {
14+
const result = parseCron('* * * * *');
15+
expect(result.hour).toHaveLength(24);
16+
expect(result.hour[0]).toBe(0);
17+
expect(result.hour[23]).toBe(23);
18+
});
19+
});
20+
21+
describe('single values', () => {
22+
it('should parse single values for all fields', () => {
23+
const result = parseCron('30 14 15 6 3');
24+
expect(result.minute).toEqual([30]);
25+
expect(result.hour).toEqual([14]);
26+
expect(result.day).toEqual([15]);
27+
expect(result.month).toEqual([6]);
28+
expect(result.weekday).toEqual([3]);
29+
});
30+
});
31+
32+
describe('lists', () => {
33+
it('should parse comma-separated values', () => {
34+
const result = parseCron('0,15,30,45 * * * *');
35+
expect(result.minute).toEqual([0, 15, 30, 45]);
36+
});
37+
});
38+
39+
describe('ranges', () => {
40+
it('should parse range expressions', () => {
41+
const result = parseCron('0-5 * * * *');
42+
expect(result.minute).toEqual([0, 1, 2, 3, 4, 5]);
43+
});
44+
});
45+
46+
describe('steps', () => {
47+
it('should parse step expressions with wildcard base', () => {
48+
const result = parseCron('*/15 * * * *');
49+
expect(result.minute).toEqual([0, 15, 30, 45]);
50+
});
51+
52+
it('should parse step expressions with numeric base', () => {
53+
const result = parseCron('10/5 * * * *');
54+
expect(result.minute).toEqual([10, 15, 20, 25, 30, 35, 40, 45, 50, 55]);
55+
});
56+
});
57+
58+
describe('validation', () => {
59+
it('should reject expressions with wrong number of fields', () => {
60+
expect(() => parseCron('* * * *')).toThrow('expected 5 fields');
61+
expect(() => parseCron('* * * * * *')).toThrow('expected 5 fields');
62+
});
63+
64+
it('should reject out-of-range values', () => {
65+
expect(() => parseCron('60 * * * *')).toThrow('Invalid cron value');
66+
expect(() => parseCron('24 * * * *')).toThrow('Invalid cron value');
67+
expect(() => parseCron('* 25 * * *')).toThrow('Invalid cron value');
68+
});
69+
70+
it('should reject invalid range (start > end)', () => {
71+
expect(() => parseCron('30-20 * * * *')).toThrow('Invalid cron range');
72+
});
73+
74+
it('should reject invalid step values', () => {
75+
expect(() => parseCron('*/0 * * * *')).toThrow('Invalid cron step');
76+
});
77+
});
78+
});
79+
80+
describe('getNextCronRun', () => {
81+
it('should find next occurrence of daily cron', () => {
82+
const cron = '0 12 * * *'; // Every day at noon
83+
const from = new Date('2024-06-15T10:00:00Z');
84+
const next = getNextCronRun(cron, from);
85+
86+
expect(next.getHours()).toBe(12);
87+
expect(next.getMinutes()).toBe(0);
88+
expect(next.getDate()).toBe(15);
89+
});
90+
91+
it('should advance to next day if time has passed', () => {
92+
const cron = '0 12 * * *'; // Every day at noon
93+
const from = new Date('2024-06-15T14:00:00Z');
94+
const next = getNextCronRun(cron, from);
95+
96+
expect(next.getDate()).toBe(16);
97+
expect(next.getHours()).toBe(12);
98+
});
99+
100+
it('should handle hourly cron', () => {
101+
const cron = '30 * * * *'; // Every hour at minute 30
102+
const from = new Date('2024-06-15T10:00:00Z');
103+
const next = getNextCronRun(cron, from);
104+
105+
expect(next.getMinutes()).toBe(30);
106+
expect(next.getHours()).toBe(10);
107+
});
108+
109+
it('should throw if no match within 2 years', () => {
110+
// Impossible cron: Feb 30th
111+
const cron = '0 0 30 2 *';
112+
const from = new Date('2024-01-01T00:00:00Z');
113+
114+
expect(() => getNextCronRun(cron, from)).toThrow('No matching cron time found');
115+
});
116+
});
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
import { describe, expect, it } from 'vitest';
2+
import { flattenToLeafPaths } from '../../src/utils/flattenToLeafPaths.js';
3+
4+
describe('flattenToLeafPaths', () => {
5+
describe('basic flattening', () => {
6+
it('should flatten a simple object with primitive values', () => {
7+
const obj = { a: 1, b: 'test', c: true };
8+
const result = flattenToLeafPaths(obj, 'root');
9+
10+
expect(result).toHaveLength(3);
11+
expect(result).toContainEqual(['root.a', 1]);
12+
expect(result).toContainEqual(['root.b', 'test']);
13+
expect(result).toContainEqual(['root.c', true]);
14+
});
15+
16+
it('should flatten nested objects with dot notation', () => {
17+
const obj = { level1: { level2: { level3: 'deep' } } };
18+
const result = flattenToLeafPaths(obj, 'config');
19+
20+
expect(result).toHaveLength(1);
21+
expect(result).toContainEqual(['config.level1.level2.level3', 'deep']);
22+
});
23+
24+
it('should handle mixed nesting depths', () => {
25+
const obj = {
26+
shallow: 'value',
27+
nested: { child: 'childValue' },
28+
deep: { a: { b: { c: 'deepest' } } },
29+
};
30+
const result = flattenToLeafPaths(obj, 'obj');
31+
32+
expect(result).toHaveLength(3);
33+
expect(result).toContainEqual(['obj.shallow', 'value']);
34+
expect(result).toContainEqual(['obj.nested.child', 'childValue']);
35+
expect(result).toContainEqual(['obj.deep.a.b.c', 'deepest']);
36+
});
37+
});
38+
39+
describe('arrays', () => {
40+
it('should treat arrays as leaf values', () => {
41+
const obj = { items: [1, 2, 3] };
42+
const result = flattenToLeafPaths(obj, 'data');
43+
44+
expect(result).toHaveLength(1);
45+
expect(result).toContainEqual(['data.items', [1, 2, 3]]);
46+
});
47+
48+
it('should not recurse into array elements', () => {
49+
const obj = { nested: { arr: [{ a: 1 }, { b: 2 }] } };
50+
const result = flattenToLeafPaths(obj, 'x');
51+
52+
expect(result).toHaveLength(1);
53+
expect(result[0][0]).toBe('x.nested.arr');
54+
expect(Array.isArray(result[0][1])).toBe(true);
55+
});
56+
});
57+
58+
describe('dangerous keys', () => {
59+
it('should skip __proto__', () => {
60+
const obj = { safe: 'value', __proto__: 'malicious' };
61+
const result = flattenToLeafPaths(obj, 'test');
62+
63+
expect(result).toHaveLength(1);
64+
expect(result).toContainEqual(['test.safe', 'value']);
65+
});
66+
67+
it('should skip constructor', () => {
68+
const obj = { data: 'ok', constructor: 'bad' };
69+
const result = flattenToLeafPaths(obj, 'cfg');
70+
71+
expect(result).toHaveLength(1);
72+
expect(result).toContainEqual(['cfg.data', 'ok']);
73+
});
74+
75+
it('should skip prototype', () => {
76+
const obj = { value: 123, prototype: 'ignore' };
77+
const result = flattenToLeafPaths(obj, 'root');
78+
79+
expect(result).toHaveLength(1);
80+
expect(result).toContainEqual(['root.value', 123]);
81+
});
82+
});
83+
84+
describe('edge cases', () => {
85+
it('should handle empty objects', () => {
86+
const obj = {};
87+
const result = flattenToLeafPaths(obj, 'empty');
88+
89+
expect(result).toHaveLength(0);
90+
});
91+
92+
it('should handle null values', () => {
93+
const obj = { a: null, b: { c: null } };
94+
const result = flattenToLeafPaths(obj, 'x');
95+
96+
expect(result).toHaveLength(2);
97+
expect(result).toContainEqual(['x.a', null]);
98+
expect(result).toContainEqual(['x.b.c', null]);
99+
});
100+
101+
it('should handle empty prefix', () => {
102+
const obj = { key: 'value' };
103+
const result = flattenToLeafPaths(obj, '');
104+
105+
expect(result).toHaveLength(1);
106+
expect(result).toContainEqual(['.key', 'value']);
107+
});
108+
});
109+
});

0 commit comments

Comments
 (0)