import { describe, it, expect } from 'vitest'; import fs from 'fs'; import path from 'path'; import { fileURLToPath } from 'url'; const currentDir = path.dirname(fileURLToPath(import.meta.url)); const servicePath = path.resolve(currentDir, '../../src/services/schedules/schedule.service.ts'); const sharedSchemaPath = path.resolve( currentDir, '../../../packages/shared-schemas/src/schedules-api.schema.ts' ); describe('Sub-minute cadence support: validator', () => { const serviceSrc = fs.readFileSync(servicePath, 'utf8'); const sharedSrc = fs.readFileSync(sharedSchemaPath, 'utf8'); it('service defines an INTERVAL_RE that only accepts seconds', () => { // Must declare an INTERVAL_RE (or equivalent) constant expect(serviceSrc).toMatch(/INTERVAL_RE\s*=\s*\//); // The regex must mention seconds and must NOT include minutes/hours alternations expect(serviceSrc).toMatch(/INTERVAL_RE[\s\S]*?seconds\?/); expect(serviceSrc).not.toMatch(/INTERVAL_RE[\s\S]*?minutes\?/); expect(serviceSrc).not.toMatch(/INTERVAL_RE[\s\S]*?hours\?/); // And be case-insensitive (i flag) expect(serviceSrc).toMatch(/INTERVAL_RE[\s\S]*?\/i\b/); }); it('service validateCronExpression takes the interval branch first', () => { // The validator should match the interval regex and early-return before falling to 5-field check. // Sequence: validateCronExpression(...) -> ... -> INTERVAL_RE -> ... -> return; -> ... -> fields.length expect(serviceSrc).toMatch( /validateCronExpression\b[\s\S]+?INTERVAL_RE[\s\S]+?return\s*;[\s\S]+?fields\.length/ ); }); it('service validator error message mentions both 5-field cron and interval examples', () => { expect(serviceSrc).toMatch(/seconds/i); expect(serviceSrc).toMatch(/\* \* \* \* \*|5 fields/); }); it('service computeNextRunForSchedule has an interval branch using INTERVAL_RE', () => { expect(serviceSrc).toMatch(/computeNextRunForSchedule\b[\s\S]+?INTERVAL_RE[\s\S]+?getTime\(\)/); }); it('service interval next-run uses the seconds multiplier (1_000 ms)', () => { expect(serviceSrc).toMatch(/\b1[_]?000\b/); }); it('shared-schemas cron validator accepts only sub-minute seconds form', () => { // The refine() must accept "30 seconds" but reject minutes/hours. expect(sharedSrc).toMatch(/seconds\?/i); expect(sharedSrc).not.toMatch(/minutes\?\s*\|/i); expect(sharedSrc).not.toMatch(/\|\s*hours\?/i); }); it('shared-schemas error message mentions sub-minute / 1–59 seconds', () => { expect(sharedSrc).toMatch(/seconds|interval/i); expect(sharedSrc).toMatch(/sub-minute|1[\s\S]{0,3}59/i); }); }); // Behavioural sanity checks on the regex shape. These don't import the service // (which needs DatabaseManager); instead, re-derive the same pattern and assert // it accepts the inputs we care about and rejects the ones we don't. // // The service-side regex stays permissive on digits (\d+) so the service can // throw a precise "1–59 seconds" error for out-of-range values; the // shared-schemas regex bounds the digits inline because it only returns a // boolean. describe('Sub-minute cadence regex behaviour (service)', () => { const SERVICE_INTERVAL_RE = /^\s*(\d+)\s+seconds?\s*$/i; it.each([ ['1 second', '1'], ['2 seconds', '2'], ['30 seconds', '30'], ['59 seconds', '59'], [' 30 SECONDS ', '30'], ])('accepts %j as sub-minute interval', (input, n) => { const m = input.match(SERVICE_INTERVAL_RE); expect(m).not.toBeNull(); expect(m?.[1]).toBe(n); }); it.each([ '* * * * *', '*/5 * * * *', '0 9 * * 1-5', '', 'foo bar', '2', '2 days', '2 weeks', '2.5 seconds', '-1 seconds', '1 minute', '5 minutes', '1 hour', '12 hours', '* * * * * *', ])('rejects %j', (input) => { expect(input.match(SERVICE_INTERVAL_RE)).toBeNull(); }); }); describe('Sub-minute cadence regex behaviour (shared-schemas, bounded 1–59)', () => { const SCHEMA_INTERVAL_RE = /^\s*([1-9]|[1-5]\d)\s+seconds?\s*$/i; it.each(['1 second', '2 seconds', '30 seconds', '59 seconds', ' 30 SECONDS '])( 'accepts %j', (input) => { expect(input.match(SCHEMA_INTERVAL_RE)).not.toBeNull(); } ); it.each([ '0 seconds', '60 seconds', '90 seconds', '120 seconds', '1 minute', '5 minutes', '1 hour', '12 hours', '2.5 seconds', '-1 seconds', ])('rejects %j', (input) => { expect(input.match(SCHEMA_INTERVAL_RE)).toBeNull(); }); });