-
Notifications
You must be signed in to change notification settings - Fork 4
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add tests for recurrence generators (#722)
* Add tests for recurrence generators * Move lunartick to dev dependency
- Loading branch information
1 parent
0e57bf2
commit 4882616
Showing
4 changed files
with
226 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -46,6 +46,7 @@ | |
"uuid": "^8.3.2" | ||
}, | ||
"devDependencies": { | ||
"lunartick-deprecated": "npm:@ordermentum/[email protected]", | ||
"@types/bunyan": "1.8.8", | ||
"@types/chai": "4.3.11", | ||
"@types/mocha": "8.2.3", | ||
|
211 changes: 211 additions & 0 deletions
211
packages/scheduler-sequelize/test/recurrence_generators_test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,211 @@ | ||
import moment, { Moment } from 'moment-timezone'; | ||
import { expect } from 'chai'; | ||
import sinon, { SinonSandbox, SinonFakeTimers } from 'sinon'; | ||
import { computeNextRunAt } from '../../scheduler-prisma/src/helpers'; | ||
import lunartick from 'lunartick-deprecated'; | ||
|
||
const MAX_SKEW_MILLI_SECONDS = 60 * 1000; // Max skew b/w comparative dates | ||
|
||
// There are some limitations to using lunartick | ||
// 1. It does not support interval with rules that are monthly and have an interval > 1 (e.g. FREQ=MONTHLY;BYMONTHDAY=17;INTERVAL=2) | ||
// 2. It does not support fortnightly rules (e.g. FREQ=WEEKLY;BYDAY=TH;INTERVAL=2) | ||
// 3. It does not support BYMONTHDAY=-1 (https://github.com/ordermentum/lunartick/blob/develop/src/iterator.js#L110 shifts the date by an extra month) | ||
// 4. It does not support multiple values for BYDAY (e.g. FREQ=WEEKLY;BYDAY=MO,WE,FR) | ||
// 5. It does not support BYSETPOS parameter (e.g. FREQ=DAILY;BYHOUR=8,18;BYMINUTE=30,0;BYSETPOS=1,4) | ||
// 6. It does not chronologically sort BY rules (e.g. Values will be different for FREQ=DAILY;BYHOUR=8,18;BYMINUTE=0,30 and FREQ=DAILY;BYHOUR=8,18;BYMINUTE=30,0 - Note the BYMINUTE prop) | ||
// 7. It does not support -ve BYDAY values (e.g. FREQ=MONTHLY;BYDAY=-2FR) | ||
|
||
|
||
const generateLunartickRecurrence = (interval: string, timezone: string) => { | ||
if (!interval.includes('DTSTART')) { | ||
const rule = lunartick.parse(interval); | ||
rule.tzId = timezone; | ||
const rrule = new lunartick(rule); | ||
return rrule.getNext(new Date()).date.toISOString(); | ||
} else { | ||
const [DTSTART, rrule] = interval.split('\nRRULE:'); | ||
const rule = lunartick.parse(rrule); | ||
rule.tzId = timezone; | ||
rule.dtStart = moment(`${DTSTART.split(':')[1]}+11:00`).toISOString(); | ||
return new lunartick(rule).getNext(new Date()).date.toISOString(); | ||
} | ||
}; | ||
|
||
const generateRRuleRecurrence = (interval: string, timezone: string) => { | ||
return computeNextRunAt(interval, timezone); | ||
}; | ||
|
||
describe('helpers', () => { | ||
let sandbox: SinonSandbox; | ||
let clock: SinonFakeTimers; | ||
|
||
beforeEach(() => { | ||
process.env.TZ = 'Australia/Sydney'; | ||
sandbox = sinon.createSandbox(); | ||
}); | ||
|
||
afterEach(() => { | ||
process.env.TZ = Intl.DateTimeFormat().resolvedOptions().timeZone; | ||
if (clock) clock.restore(); | ||
sandbox.restore(); | ||
}) | ||
|
||
describe('Rrule and lunartick create the same recurrence date with rules that do not have DTSTART', () => { | ||
it('17th of every other month', () => { | ||
const rule = 'FREQ=MONTHLY;BYMONTHDAY=17;INTERVAL=1'; | ||
clock = sinon.useFakeTimers(new Date('2024-01-24T16:00:00Z').getTime()); | ||
const lunartick = generateLunartickRecurrence(rule, 'Australia/Sydney'); | ||
const rrule = generateRRuleRecurrence(rule, 'Australia/Sydney'); | ||
expect(Math.abs(moment(lunartick).valueOf() - moment(rrule).valueOf())).to.be.lessThan(MAX_SKEW_MILLI_SECONDS); | ||
}); | ||
it('every Thursday', () => { | ||
const rule = 'FREQ=WEEKLY;BYDAY=TH;INTERVAL=1'; | ||
clock = sinon.useFakeTimers(new Date('2024-01-24T16:00:00Z').getTime()); | ||
const lunartick = generateLunartickRecurrence(rule, 'Australia/Sydney'); | ||
const rrule = generateRRuleRecurrence(rule, 'Australia/Sydney'); | ||
expect(Math.abs(moment(lunartick).valueOf() - moment(rrule).valueOf())).to.be.lessThan(MAX_SKEW_MILLI_SECONDS); | ||
}); | ||
it('every monday', () => { | ||
const rule = 'FREQ=WEEKLY;BYDAY=MO'; | ||
clock = sinon.useFakeTimers(new Date('2024-01-24T16:00:00Z').getTime()); | ||
const lunartick = generateLunartickRecurrence(rule, 'Australia/Sydney'); | ||
const rrule = generateRRuleRecurrence(rule, 'Australia/Sydney'); | ||
expect(Math.abs(moment(lunartick).valueOf() - moment(rrule).valueOf())).to.be.lessThan(MAX_SKEW_MILLI_SECONDS); | ||
}); | ||
it('daily at 08:30 and 18:00', () => { | ||
const rule = 'FREQ=DAILY;BYHOUR=8,18;BYMINUTE=0,30'; | ||
clock = sinon.useFakeTimers(new Date('2024-01-24T16:00:00Z').getTime()); | ||
const lunartick = generateLunartickRecurrence(rule, 'Australia/Sydney'); | ||
const rrule = generateRRuleRecurrence(rule, 'Australia/Sydney'); | ||
expect(Math.abs(moment(lunartick).valueOf() - moment(rrule).valueOf())).to.be.lessThan(MAX_SKEW_MILLI_SECONDS); | ||
}); | ||
}); | ||
|
||
describe('Rrule and lunartick create the same recurrence date with rules that have DTSTART', () => { | ||
it('17th of every other month', () => { | ||
const rule = 'DTSTART;TZID=Australia/Sydney:20240120T030000\nRRULE:FREQ=MONTHLY;BYMONTHDAY=17;INTERVAL=1;BYHOUR=16;BYMINUTE=0;BYSECOND=0'; | ||
clock = sinon.useFakeTimers(new Date('2024-01-24T16:00:00Z').getTime()); | ||
const lunartick = generateLunartickRecurrence(rule, 'Australia/Sydney'); | ||
const rrule = generateRRuleRecurrence(rule, 'Australia/Sydney'); | ||
expect(Math.abs(moment(lunartick).valueOf() - moment(rrule).valueOf())).to.be.lessThan(MAX_SKEW_MILLI_SECONDS); | ||
}); | ||
it('every Thursday', () => { | ||
const rule = 'DTSTART;TZID=Australia/Sydney:20240120T030000\nRRULE:FREQ=WEEKLY;BYDAY=TH;INTERVAL=1;BYHOUR=16;BYMINUTE=0;BYSECOND=0'; | ||
clock = sinon.useFakeTimers(new Date('2024-01-24T16:00:00Z').getTime()); | ||
const lunartick = generateLunartickRecurrence(rule, 'Australia/Sydney'); | ||
const rrule = generateRRuleRecurrence(rule, 'Australia/Sydney'); | ||
expect(Math.abs(moment(lunartick).valueOf() - moment(rrule).valueOf())).to.be.lessThan(MAX_SKEW_MILLI_SECONDS); | ||
}); | ||
it('every monday', () => { | ||
const rule = 'DTSTART;TZID=Australia/Sydney:20240120T030000\nRRULE:FREQ=WEEKLY;BYDAY=MO;BYHOUR=16;BYMINUTE=0;BYSECOND=0'; | ||
clock = sinon.useFakeTimers(new Date('2024-01-24T16:00:00Z').getTime()); | ||
const lunartick = generateLunartickRecurrence(rule, 'Australia/Sydney'); | ||
const rrule = generateRRuleRecurrence(rule, 'Australia/Sydney'); | ||
expect(Math.abs(moment(lunartick).valueOf() - moment(rrule).valueOf())).to.be.lessThan(MAX_SKEW_MILLI_SECONDS); | ||
}); | ||
it('daily at 08:30 and 18:00', () => { | ||
const rule = 'DTSTART;TZID=Australia/Sydney:20240120T030000\nRRULE:FREQ=DAILY;BYHOUR=8,18;BYMINUTE=0,30;BYSECOND=0'; | ||
clock = sinon.useFakeTimers(new Date('2024-01-24T16:00:00Z').getTime()); | ||
const lunartick = generateLunartickRecurrence(rule, 'Australia/Sydney'); | ||
const rrule = generateRRuleRecurrence(rule, 'Australia/Sydney'); | ||
expect(Math.abs(moment(lunartick).valueOf() - moment(rrule).valueOf())).to.be.lessThan(MAX_SKEW_MILLI_SECONDS); | ||
}); | ||
}); | ||
|
||
describe('Rrule supported rules', () => { | ||
it('Every other thursday respecting start date', () => { | ||
const rule = 'DTSTART;TZID=Australia/Sydney:20240120T030000\nRRULE:FREQ=WEEKLY;INTERVAL=2;BYDAY=TH'; | ||
clock = sinon.useFakeTimers(new Date('2024-02-01T16:00:00Z').getTime()); | ||
const rrule = generateRRuleRecurrence(rule, 'Australia/Sydney'); | ||
expect(rrule).to.eqls('2024-02-14T16:00:00.000Z'); | ||
}); | ||
}); | ||
|
||
describe('Rrule and lunartick create the same recurrence date for rules with date crossing DST thresholds', () => { | ||
describe('DST ending', () => { | ||
it('17th of every other month', () => { | ||
const rule = 'FREQ=MONTHLY;BYMONTHDAY=17;INTERVAL=1;BYHOUR=17'; | ||
clock = sinon.useFakeTimers(new Date('2024-04-06T14:00:00.000Z').getTime()); | ||
const lunartick = generateLunartickRecurrence(rule, 'Australia/Sydney'); | ||
const rrule = generateRRuleRecurrence(rule, 'Australia/Sydney'); | ||
expect(Math.abs(moment(lunartick).valueOf() - moment(rrule).valueOf())).to.be.lessThan(MAX_SKEW_MILLI_SECONDS); | ||
}); | ||
it('every Thursday', () => { | ||
const rule = 'FREQ=WEEKLY;BYDAY=TH;INTERVAL=1;BYHOUR=17'; | ||
clock = sinon.useFakeTimers(new Date('2024-04-06T14:00:00.000Z').getTime()); | ||
const lunartick = generateLunartickRecurrence(rule, 'Australia/Sydney'); | ||
const rrule = generateRRuleRecurrence(rule, 'Australia/Sydney'); | ||
expect(Math.abs(moment(lunartick).valueOf() - moment(rrule).valueOf())).to.be.lessThan(MAX_SKEW_MILLI_SECONDS); | ||
}); | ||
it('every monday', () => { | ||
const rule = 'FREQ=WEEKLY;BYDAY=MO;BYHOUR=17'; | ||
clock = sinon.useFakeTimers(new Date('2024-04-06T14:00:00.000Z').getTime()); | ||
const lunartick = generateLunartickRecurrence(rule, 'Australia/Sydney'); | ||
const rrule = generateRRuleRecurrence(rule, 'Australia/Sydney'); | ||
expect(Math.abs(moment(lunartick).valueOf() - moment(rrule).valueOf())).to.be.lessThan(MAX_SKEW_MILLI_SECONDS); | ||
}); | ||
it('daily at 08:30 and 18:00', () => { | ||
const rule = 'FREQ=DAILY;BYHOUR=8,18;BYMINUTE=0,30'; | ||
clock = sinon.useFakeTimers(new Date('2024-04-06T14:00:00.000Z').getTime()); | ||
const lunartick = generateLunartickRecurrence(rule, 'Australia/Sydney'); | ||
const rrule = generateRRuleRecurrence(rule, 'Australia/Sydney'); | ||
expect(Math.abs(moment(lunartick).valueOf() - moment(rrule).valueOf())).to.be.lessThan(MAX_SKEW_MILLI_SECONDS); | ||
}); | ||
}); | ||
describe('DST start', () => { | ||
it('17th of every other month', () => { | ||
const rule = 'FREQ=MONTHLY;BYMONTHDAY=17;INTERVAL=1;BYHOUR=17'; | ||
clock = sinon.useFakeTimers(new Date('2024-10-05T15:00:00.000Z').getTime()); | ||
const lunartick = generateLunartickRecurrence(rule, 'Australia/Sydney'); | ||
const rrule = generateRRuleRecurrence(rule, 'Australia/Sydney'); | ||
expect(Math.abs(moment(lunartick).valueOf() - moment(rrule).valueOf())).to.be.lessThan(MAX_SKEW_MILLI_SECONDS); | ||
}); | ||
it('every Thursday', () => { | ||
const rule = 'FREQ=WEEKLY;BYDAY=TH;INTERVAL=1;BYHOUR=17'; | ||
clock = sinon.useFakeTimers(new Date('2024-10-05T15:00:00.000Z').getTime()); | ||
const lunartick = generateLunartickRecurrence(rule, 'Australia/Sydney'); | ||
const rrule = generateRRuleRecurrence(rule, 'Australia/Sydney'); | ||
expect(Math.abs(moment(lunartick).valueOf() - moment(rrule).valueOf())).to.be.lessThan(MAX_SKEW_MILLI_SECONDS); | ||
}); | ||
it('every monday', () => { | ||
const rule = 'FREQ=WEEKLY;BYDAY=MO;BYHOUR=17'; | ||
clock = sinon.useFakeTimers(new Date('2024-10-05T15:00:00.000Z').getTime()); | ||
const lunartick = generateLunartickRecurrence(rule, 'Australia/Sydney'); | ||
const rrule = generateRRuleRecurrence(rule, 'Australia/Sydney'); | ||
expect(Math.abs(moment(lunartick).valueOf() - moment(rrule).valueOf())).to.be.lessThan(MAX_SKEW_MILLI_SECONDS); | ||
}); | ||
it('daily at 08:30 and 18:00', () => { | ||
const rule = 'FREQ=DAILY;BYHOUR=8,18;BYMINUTE=0,30'; | ||
clock = sinon.useFakeTimers(new Date('2024-10-05T15:00:00.000Z').getTime()); | ||
const lunartick = generateLunartickRecurrence(rule, 'Australia/Sydney'); | ||
const rrule = generateRRuleRecurrence(rule, 'Australia/Sydney'); | ||
expect(Math.abs(moment(lunartick).valueOf() - moment(rrule).valueOf())).to.be.lessThan(MAX_SKEW_MILLI_SECONDS); | ||
}); | ||
}); | ||
}); | ||
|
||
describe('Lunartick and rrule differences', () => { | ||
[ | ||
'FREQ=MONTHLY;BYMONTHDAY=17;INTERVAL=2', | ||
'FREQ=WEEKLY;BYDAY=TH;INTERVAL=2', | ||
'FREQ=WEEKLY;BYMONTHDAY=-1;INTERVAL=2', | ||
'FREQ=WEEKLY;BYDAY=MO,WE,FR', | ||
'FREQ=DAILY;BYHOUR=8,18;BYMINUTE=30,0;BYSETPOS=1,4', | ||
'FREQ=DAILY;BYHOUR=8,18;BYMINUTE=30,0', | ||
'FREQ=MONTHLY;BYDAY=-2FR', | ||
].forEach(rule => { | ||
it(`Rule: ${rule}`, () => { | ||
clock = sinon.useFakeTimers(new Date('2024-01-24T16:00:00Z').getTime()); | ||
let lunartick; | ||
try { | ||
lunartick = generateLunartickRecurrence(rule, 'Australia/Sydney'); | ||
} catch (e) { | ||
lunartick = null; | ||
} | ||
const rrule = generateRRuleRecurrence(rule, 'Australia/Sydney'); | ||
if (lunartick === null) expect(rrule).to.not.be.null; | ||
else expect(Math.abs(moment(lunartick).valueOf() - moment(rrule).valueOf())).to.be.greaterThan(MAX_SKEW_MILLI_SECONDS); | ||
}); | ||
}); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -5333,6 +5333,13 @@ lru-cache@^6.0.0: | |
dependencies: | ||
yallist "^4.0.0" | ||
|
||
"lunartick-deprecated@npm:@ordermentum/[email protected]": | ||
version "0.0.19" | ||
resolved "https://registry.yarnpkg.com/@ordermentum/lunartick/-/lunartick-0.0.19.tgz#4cc0b0bc56a9377f44e6dfad4238acc020160b92" | ||
integrity sha512-gL03DvxsIvhiG90AMDLo+R0RKWFl4JdGf56c8jaWtxqNpq6fASAcpzwnPjYbZdG284keQ67AJ04IO5gQljrBEQ== | ||
dependencies: | ||
moment-timezone "^0.5.11" | ||
|
||
luxon@^1.26.0: | ||
version "1.28.1" | ||
resolved "https://registry.yarnpkg.com/luxon/-/luxon-1.28.1.tgz#528cdf3624a54506d710290a2341aa8e6e6c61b0" | ||
|
@@ -5512,6 +5519,13 @@ moment-timezone@*, moment-timezone@^0.5.33, moment-timezone@^0.5.43: | |
dependencies: | ||
moment "^2.29.4" | ||
|
||
moment-timezone@^0.5.11: | ||
version "0.5.45" | ||
resolved "https://registry.yarnpkg.com/moment-timezone/-/moment-timezone-0.5.45.tgz#cb685acd56bac10e69d93c536366eb65aa6bcf5c" | ||
integrity sha512-HIWmqA86KcmCAhnMAN0wuDOARV/525R2+lOLotuGFzn4HO+FH+/645z2wx0Dt3iDv6/p61SIvKnDstISainhLQ== | ||
dependencies: | ||
moment "^2.29.4" | ||
|
||
[email protected]: | ||
version "2.30.1" | ||
resolved "https://registry.yarnpkg.com/moment/-/moment-2.30.1.tgz#f8c91c07b7a786e30c59926df530b4eac96974ae" | ||
|