add missing unit test suite

This commit is contained in:
Jarek Krochmalski committed 2026-08-22 09:09:50 +02:00
1 parent 7227b334f9
commit 5f9e6bde3c
160 files changed
+19260 -1164

No files matched your search

+157
View File
@@ -0,0 +1,157 @@
import { describe, test, expect } from 'bun:test';
import { Cron } from 'croner';
import { getNextRun, isValidCron } from '../src/lib/server/scheduler/cron-utils';
/**
* Tests that our cron utilities use AND logic for day-of-month + day-of-week,
* matching user expectations ("first Saturday") rather than Vixie cron OR logic.
*/
describe('getNextRun: AND logic for day-of-month + day-of-week', () => {
test('first Saturday of the month: 0 3 1-7 * 6', () => {
const next = getNextRun('0 3 1-7 * 6');
expect(next).not.toBeNull();
expect(next!.getDay()).toBe(6); // Saturday
expect(next!.getDate()).toBeGreaterThanOrEqual(1);
expect(next!.getDate()).toBeLessThanOrEqual(7);
});
test('first Monday of the month: 0 9 1-7 * 1', () => {
const next = getNextRun('0 9 1-7 * 1');
expect(next).not.toBeNull();
expect(next!.getDay()).toBe(1); // Monday
expect(next!.getDate()).toBeGreaterThanOrEqual(1);
expect(next!.getDate()).toBeLessThanOrEqual(7);
});
test('1st of month AND Monday: 0 0 1 * 1', () => {
const next = getNextRun('0 0 1 * 1');
expect(next).not.toBeNull();
expect(next!.getDate()).toBe(1);
expect(next!.getDay()).toBe(1);
});
test('day-of-week only: 0 3 * * 1 — next run is a Monday', () => {
const next = getNextRun('0 3 * * 1');
expect(next).not.toBeNull();
expect(next!.getDay()).toBe(1);
});
test('day-of-month only: 0 3 15 * * — next run is the 15th', () => {
const next = getNextRun('0 3 15 * *');
expect(next).not.toBeNull();
expect(next!.getDate()).toBe(15);
});
test('daily at 3am: 0 3 * * *', () => {
const next = getNextRun('0 3 * * *');
expect(next).not.toBeNull();
expect(next!.getHours()).toBe(3);
expect(next!.getMinutes()).toBe(0);
});
test('every 30 minutes: */30 * * * *', () => {
const next = getNextRun('*/30 * * * *');
expect(next).not.toBeNull();
const mins = next!.getMinutes();
expect(mins === 0 || mins === 30).toBe(true);
});
test('invalid expression returns null', () => {
expect(getNextRun('not a cron')).toBeNull();
});
test('timezone parameter works', () => {
const next = getNextRun('0 3 * * *', 'Europe/Warsaw');
expect(next).not.toBeNull();
});
});
describe('isValidCron', () => {
test('valid 5-field expressions', () => {
expect(isValidCron('0 3 * * *')).toBe(true);
expect(isValidCron('*/30 * * * *')).toBe(true);
expect(isValidCron('0 3 1-7 * 6')).toBe(true);
expect(isValidCron('0 0 1 * 1')).toBe(true);
expect(isValidCron('5 4 * * 0')).toBe(true);
});
test('valid 6-field expressions (with seconds)', () => {
expect(isValidCron('0 0 3 * * *')).toBe(true);
expect(isValidCron('30 */5 * * * *')).toBe(true);
expect(isValidCron('0 0 0 1 * *')).toBe(true);
});
test('invalid expressions', () => {
expect(isValidCron('not a cron')).toBe(false);
expect(isValidCron('* * *')).toBe(false);
expect(isValidCron('60 * * * *')).toBe(false);
expect(isValidCron('* 25 * * *')).toBe(false);
});
});
describe('Schedule type detection from cron expression', () => {
// Mirrors logic from api/auto-update/[containerName]/+server.ts
function detectScheduleType(cronExpression: string): 'daily' | 'weekly' | 'custom' {
const parts = cronExpression.split(' ');
if (parts.length === 5) {
const [, , day, month, dow] = parts;
if (dow !== '*' && day === '*' && month === '*') return 'weekly';
if (day === '*' && month === '*' && dow === '*') return 'daily';
}
return 'custom';
}
test('detects daily schedule', () => {
expect(detectScheduleType('0 3 * * *')).toBe('daily');
expect(detectScheduleType('30 12 * * *')).toBe('daily');
});
test('detects weekly schedule', () => {
expect(detectScheduleType('0 3 * * 1')).toBe('weekly');
expect(detectScheduleType('0 0 * * 0')).toBe('weekly');
expect(detectScheduleType('0 3 * * 6')).toBe('weekly');
});
test('6-field expression is always custom', () => {
expect(detectScheduleType('0 0 3 * * *')).toBe('custom');
expect(detectScheduleType('30 0 3 * * 1')).toBe('custom');
});
test('monthly expressions are custom', () => {
expect(detectScheduleType('0 3 1 * *')).toBe('custom');
expect(detectScheduleType('0 0 15 6 *')).toBe('custom');
});
test('combined day-of-month and day-of-week is custom', () => {
expect(detectScheduleType('0 3 1-7 * 6')).toBe('custom');
});
test('every-N-minutes is daily (all day fields are wildcard)', () => {
expect(detectScheduleType('*/30 * * * *')).toBe('daily');
});
});
describe('AND vs OR verification', () => {
test('getNextRun produces different result than croner default for combined fields', () => {
// croner default (legacyMode:true) uses OR — our getNextRun should use AND
// With OR, "0 3 1-7 * 6" fires on days 1-7 OR any Saturday (~11/month)
// With AND, it fires only on Saturdays that are also days 1-7 (~1/month)
const orJob = new Cron('0 3 1-7 * 6'); // default = OR logic
const orRuns = orJob.nextRuns(12);
orJob.stop();
// Our getNextRun uses AND — verify all 12 runs satisfy both constraints
// We can't call getNextRun 12 times (it always returns the same next),
// but we can verify the single next run satisfies AND
const andNext = getNextRun('0 3 1-7 * 6');
expect(andNext).not.toBeNull();
expect(andNext!.getDay()).toBe(6);
expect(andNext!.getDate()).toBeGreaterThanOrEqual(1);
expect(andNext!.getDate()).toBeLessThanOrEqual(7);
// OR logic produces runs that are NOT all Saturdays
const nonSaturdayOrRuns = orRuns.filter((d) => d.getDay() !== 6);
expect(nonSaturdayOrRuns.length).toBeGreaterThan(0);
});
});