SCRAPE-13: Create node-cron job initialization (#18)
Co-authored-by: Stephen Minakian <stephenminakian@gmail.com> Co-committed-by: Stephen Minakian <stephenminakian@gmail.com>
This commit is contained in:
@ -1,13 +1,46 @@
|
||||
/**
|
||||
* Tests for scraperJob.js - In-process mutex (isScraperRunning flag)
|
||||
* Tests for scraperJob.js - In-process mutex and scheduler
|
||||
* Following TDD - these tests are written BEFORE implementation
|
||||
*/
|
||||
|
||||
// Mock node-cron before requiring scraperJob
|
||||
jest.mock('node-cron', () => ({
|
||||
validate: jest.fn(),
|
||||
schedule: jest.fn()
|
||||
}));
|
||||
|
||||
// Mock the scraper logger
|
||||
jest.mock('../../services/scraperLogger', () => ({
|
||||
createLogger: jest.fn(() => ({
|
||||
info: jest.fn(),
|
||||
warn: jest.fn(),
|
||||
error: jest.fn()
|
||||
}))
|
||||
}));
|
||||
|
||||
// Mock uuid
|
||||
jest.mock('uuid', () => ({
|
||||
v4: jest.fn(() => 'mock-uuid-1234')
|
||||
}));
|
||||
|
||||
// Mock scraperService
|
||||
jest.mock('../../services/scraperService', () => ({
|
||||
runScrape: jest.fn(() => Promise.resolve({ status: 'success' }))
|
||||
}));
|
||||
|
||||
const cron = require('node-cron');
|
||||
const { createLogger } = require('../../services/scraperLogger');
|
||||
const { runScrape } = require('../../services/scraperService');
|
||||
|
||||
const {
|
||||
isScraperRunning,
|
||||
getCurrentJobId,
|
||||
acquireLock,
|
||||
releaseLock
|
||||
releaseLock,
|
||||
initializeScheduler,
|
||||
stopScheduler,
|
||||
getScheduleExpression,
|
||||
getNextScheduledRun
|
||||
} = require('../../jobs/scraperJob');
|
||||
|
||||
describe('scraperJob - mutex lock', () => {
|
||||
@ -147,5 +180,684 @@ describe('scraperJob - mutex lock', () => {
|
||||
test('should export releaseLock as a function', () => {
|
||||
expect(typeof releaseLock).toBe('function');
|
||||
});
|
||||
|
||||
test('should export initializeScheduler as a function', () => {
|
||||
expect(typeof initializeScheduler).toBe('function');
|
||||
});
|
||||
|
||||
test('should export stopScheduler as a function', () => {
|
||||
expect(typeof stopScheduler).toBe('function');
|
||||
});
|
||||
|
||||
test('should export getScheduleExpression as a function', () => {
|
||||
expect(typeof getScheduleExpression).toBe('function');
|
||||
});
|
||||
|
||||
test('should export getNextScheduledRun as a function', () => {
|
||||
expect(typeof getNextScheduledRun).toBe('function');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('scraperJob - scheduler', () => {
|
||||
const mockDb = { collection: jest.fn() };
|
||||
let mockLogger;
|
||||
|
||||
beforeEach(() => {
|
||||
// Reset all mocks
|
||||
jest.clearAllMocks();
|
||||
releaseLock();
|
||||
if (typeof stopScheduler === 'function') {
|
||||
stopScheduler();
|
||||
}
|
||||
|
||||
// Setup mock logger
|
||||
mockLogger = {
|
||||
info: jest.fn(),
|
||||
warn: jest.fn(),
|
||||
error: jest.fn()
|
||||
};
|
||||
createLogger.mockReturnValue(mockLogger);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
releaseLock();
|
||||
if (typeof stopScheduler === 'function') {
|
||||
stopScheduler();
|
||||
}
|
||||
// Reset config module to defaults between tests
|
||||
jest.resetModules();
|
||||
});
|
||||
|
||||
describe('initializeScheduler()', () => {
|
||||
test('should not initialize when SCRAPER_ENABLED is false', () => {
|
||||
// We need to re-require with modified config
|
||||
jest.resetModules();
|
||||
|
||||
// Mock config with SCRAPER_ENABLED = false
|
||||
jest.doMock('../../config/scraper', () => ({
|
||||
SCRAPER_ENABLED: false,
|
||||
SCRAPER_SCHEDULE: '0 6 * * *',
|
||||
SCRAPER_TIMEZONE: 'UTC'
|
||||
}));
|
||||
|
||||
// Re-mock the dependencies
|
||||
jest.doMock('node-cron', () => ({
|
||||
validate: jest.fn(),
|
||||
schedule: jest.fn()
|
||||
}));
|
||||
jest.doMock('../../services/scraperLogger', () => ({
|
||||
createLogger: jest.fn(() => ({
|
||||
info: jest.fn(),
|
||||
warn: jest.fn(),
|
||||
error: jest.fn()
|
||||
}))
|
||||
}));
|
||||
jest.doMock('../../services/scraperService', () => ({
|
||||
runScrape: jest.fn()
|
||||
}));
|
||||
jest.doMock('uuid', () => ({
|
||||
v4: jest.fn(() => 'mock-uuid')
|
||||
}));
|
||||
|
||||
const freshCron = require('node-cron');
|
||||
const { createLogger: freshCreateLogger } = require('../../services/scraperLogger');
|
||||
const freshLogger = { info: jest.fn(), warn: jest.fn(), error: jest.fn() };
|
||||
freshCreateLogger.mockReturnValue(freshLogger);
|
||||
|
||||
const { initializeScheduler: freshInit } = require('../../jobs/scraperJob');
|
||||
|
||||
freshInit(mockDb);
|
||||
|
||||
expect(freshCron.schedule).not.toHaveBeenCalled();
|
||||
expect(freshLogger.info).toHaveBeenCalledWith('Scraper scheduling is disabled');
|
||||
});
|
||||
|
||||
test('should validate cron expression with cron.validate()', () => {
|
||||
jest.resetModules();
|
||||
|
||||
jest.doMock('../../config/scraper', () => ({
|
||||
SCRAPER_ENABLED: true,
|
||||
SCRAPER_SCHEDULE: '0 6 * * *',
|
||||
SCRAPER_TIMEZONE: 'UTC'
|
||||
}));
|
||||
jest.doMock('node-cron', () => ({
|
||||
validate: jest.fn(() => true),
|
||||
schedule: jest.fn(() => ({ stop: jest.fn() }))
|
||||
}));
|
||||
jest.doMock('../../services/scraperLogger', () => ({
|
||||
createLogger: jest.fn(() => ({
|
||||
info: jest.fn(), warn: jest.fn(), error: jest.fn()
|
||||
}))
|
||||
}));
|
||||
jest.doMock('../../services/scraperService', () => ({
|
||||
runScrape: jest.fn()
|
||||
}));
|
||||
jest.doMock('uuid', () => ({
|
||||
v4: jest.fn(() => 'mock-uuid')
|
||||
}));
|
||||
|
||||
const freshCron = require('node-cron');
|
||||
const { initializeScheduler: freshInit } = require('../../jobs/scraperJob');
|
||||
|
||||
freshInit(mockDb);
|
||||
|
||||
expect(freshCron.validate).toHaveBeenCalledWith('0 6 * * *');
|
||||
});
|
||||
|
||||
test('should fall back to default schedule 0 6 * * * if cron expression is invalid', () => {
|
||||
jest.resetModules();
|
||||
|
||||
const mockConfig = {
|
||||
SCRAPER_ENABLED: true,
|
||||
SCRAPER_SCHEDULE: 'invalid-cron',
|
||||
SCRAPER_TIMEZONE: 'UTC'
|
||||
};
|
||||
jest.doMock('../../config/scraper', () => mockConfig);
|
||||
jest.doMock('node-cron', () => ({
|
||||
validate: jest.fn(() => false),
|
||||
schedule: jest.fn(() => ({ stop: jest.fn() }))
|
||||
}));
|
||||
const freshLogger = { info: jest.fn(), warn: jest.fn(), error: jest.fn() };
|
||||
jest.doMock('../../services/scraperLogger', () => ({
|
||||
createLogger: jest.fn(() => freshLogger)
|
||||
}));
|
||||
jest.doMock('../../services/scraperService', () => ({
|
||||
runScrape: jest.fn()
|
||||
}));
|
||||
jest.doMock('uuid', () => ({
|
||||
v4: jest.fn(() => 'mock-uuid')
|
||||
}));
|
||||
|
||||
const freshCron = require('node-cron');
|
||||
const { initializeScheduler: freshInit } = require('../../jobs/scraperJob');
|
||||
|
||||
freshInit(mockDb);
|
||||
|
||||
// After fallback, schedule should be called with default
|
||||
expect(freshCron.schedule).toHaveBeenCalledWith(
|
||||
'0 6 * * *',
|
||||
expect.any(Function),
|
||||
expect.objectContaining({ timezone: 'UTC' })
|
||||
);
|
||||
expect(freshLogger.error).toHaveBeenCalledWith(
|
||||
'Invalid cron schedule expression',
|
||||
expect.objectContaining({ schedule: 'invalid-cron' })
|
||||
);
|
||||
expect(freshLogger.warn).toHaveBeenCalledWith(
|
||||
expect.stringContaining('Falling back to default schedule')
|
||||
);
|
||||
});
|
||||
|
||||
test('should enforce minimum 1-hour interval - adjust too-frequent schedules', () => {
|
||||
jest.resetModules();
|
||||
|
||||
const mockConfig = {
|
||||
SCRAPER_ENABLED: true,
|
||||
SCRAPER_SCHEDULE: '*/5 * * * *', // Every 5 minutes - too frequent
|
||||
SCRAPER_TIMEZONE: 'UTC'
|
||||
};
|
||||
jest.doMock('../../config/scraper', () => mockConfig);
|
||||
jest.doMock('node-cron', () => ({
|
||||
validate: jest.fn(() => true),
|
||||
schedule: jest.fn(() => ({ stop: jest.fn() }))
|
||||
}));
|
||||
const freshLogger = { info: jest.fn(), warn: jest.fn(), error: jest.fn() };
|
||||
jest.doMock('../../services/scraperLogger', () => ({
|
||||
createLogger: jest.fn(() => freshLogger)
|
||||
}));
|
||||
jest.doMock('../../services/scraperService', () => ({
|
||||
runScrape: jest.fn()
|
||||
}));
|
||||
jest.doMock('uuid', () => ({
|
||||
v4: jest.fn(() => 'mock-uuid')
|
||||
}));
|
||||
|
||||
const freshCron = require('node-cron');
|
||||
const { initializeScheduler: freshInit } = require('../../jobs/scraperJob');
|
||||
|
||||
freshInit(mockDb);
|
||||
|
||||
// Should adjust to hourly
|
||||
expect(freshCron.schedule).toHaveBeenCalledWith(
|
||||
'0 * * * *',
|
||||
expect.any(Function),
|
||||
expect.objectContaining({ timezone: 'UTC' })
|
||||
);
|
||||
expect(freshLogger.warn).toHaveBeenCalledWith(
|
||||
expect.stringContaining('less than 1 hour'),
|
||||
expect.objectContaining({ originalSchedule: '*/5 * * * *' })
|
||||
);
|
||||
});
|
||||
|
||||
test('should enforce minimum 1-hour interval for every-minute schedule', () => {
|
||||
jest.resetModules();
|
||||
|
||||
const mockConfig = {
|
||||
SCRAPER_ENABLED: true,
|
||||
SCRAPER_SCHEDULE: '* * * * *', // Every minute
|
||||
SCRAPER_TIMEZONE: 'UTC'
|
||||
};
|
||||
jest.doMock('../../config/scraper', () => mockConfig);
|
||||
jest.doMock('node-cron', () => ({
|
||||
validate: jest.fn(() => true),
|
||||
schedule: jest.fn(() => ({ stop: jest.fn() }))
|
||||
}));
|
||||
const freshLogger = { info: jest.fn(), warn: jest.fn(), error: jest.fn() };
|
||||
jest.doMock('../../services/scraperLogger', () => ({
|
||||
createLogger: jest.fn(() => freshLogger)
|
||||
}));
|
||||
jest.doMock('../../services/scraperService', () => ({
|
||||
runScrape: jest.fn()
|
||||
}));
|
||||
jest.doMock('uuid', () => ({
|
||||
v4: jest.fn(() => 'mock-uuid')
|
||||
}));
|
||||
|
||||
const freshCron = require('node-cron');
|
||||
const { initializeScheduler: freshInit } = require('../../jobs/scraperJob');
|
||||
|
||||
freshInit(mockDb);
|
||||
|
||||
expect(freshCron.schedule).toHaveBeenCalledWith(
|
||||
'0 * * * *',
|
||||
expect.any(Function),
|
||||
expect.objectContaining({ timezone: 'UTC' })
|
||||
);
|
||||
});
|
||||
|
||||
test('should create cron job with configured timezone', () => {
|
||||
jest.resetModules();
|
||||
|
||||
jest.doMock('../../config/scraper', () => ({
|
||||
SCRAPER_ENABLED: true,
|
||||
SCRAPER_SCHEDULE: '0 6 * * *',
|
||||
SCRAPER_TIMEZONE: 'America/Denver'
|
||||
}));
|
||||
jest.doMock('node-cron', () => ({
|
||||
validate: jest.fn(() => true),
|
||||
schedule: jest.fn(() => ({ stop: jest.fn() }))
|
||||
}));
|
||||
jest.doMock('../../services/scraperLogger', () => ({
|
||||
createLogger: jest.fn(() => ({
|
||||
info: jest.fn(), warn: jest.fn(), error: jest.fn()
|
||||
}))
|
||||
}));
|
||||
jest.doMock('../../services/scraperService', () => ({
|
||||
runScrape: jest.fn()
|
||||
}));
|
||||
jest.doMock('uuid', () => ({
|
||||
v4: jest.fn(() => 'mock-uuid')
|
||||
}));
|
||||
|
||||
const freshCron = require('node-cron');
|
||||
const { initializeScheduler: freshInit } = require('../../jobs/scraperJob');
|
||||
|
||||
freshInit(mockDb);
|
||||
|
||||
expect(freshCron.schedule).toHaveBeenCalledWith(
|
||||
'0 6 * * *',
|
||||
expect.any(Function),
|
||||
expect.objectContaining({ timezone: 'America/Denver' })
|
||||
);
|
||||
});
|
||||
|
||||
test('should log initialization with schedule and next run time', () => {
|
||||
jest.resetModules();
|
||||
|
||||
jest.doMock('../../config/scraper', () => ({
|
||||
SCRAPER_ENABLED: true,
|
||||
SCRAPER_SCHEDULE: '0 6 * * *',
|
||||
SCRAPER_TIMEZONE: 'UTC'
|
||||
}));
|
||||
jest.doMock('node-cron', () => ({
|
||||
validate: jest.fn(() => true),
|
||||
schedule: jest.fn(() => ({ stop: jest.fn() }))
|
||||
}));
|
||||
const freshLogger = { info: jest.fn(), warn: jest.fn(), error: jest.fn() };
|
||||
jest.doMock('../../services/scraperLogger', () => ({
|
||||
createLogger: jest.fn(() => freshLogger)
|
||||
}));
|
||||
jest.doMock('../../services/scraperService', () => ({
|
||||
runScrape: jest.fn()
|
||||
}));
|
||||
jest.doMock('uuid', () => ({
|
||||
v4: jest.fn(() => 'mock-uuid')
|
||||
}));
|
||||
|
||||
const { initializeScheduler: freshInit } = require('../../jobs/scraperJob');
|
||||
|
||||
freshInit(mockDb);
|
||||
|
||||
expect(freshLogger.info).toHaveBeenCalledWith(
|
||||
'Scraper scheduler initialized',
|
||||
expect.objectContaining({
|
||||
schedule: '0 6 * * *',
|
||||
timezone: 'UTC'
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
test('should allow valid hourly schedule without adjustment', () => {
|
||||
jest.resetModules();
|
||||
|
||||
jest.doMock('../../config/scraper', () => ({
|
||||
SCRAPER_ENABLED: true,
|
||||
SCRAPER_SCHEDULE: '0 */2 * * *', // Every 2 hours - acceptable
|
||||
SCRAPER_TIMEZONE: 'UTC'
|
||||
}));
|
||||
jest.doMock('node-cron', () => ({
|
||||
validate: jest.fn(() => true),
|
||||
schedule: jest.fn(() => ({ stop: jest.fn() }))
|
||||
}));
|
||||
jest.doMock('../../services/scraperLogger', () => ({
|
||||
createLogger: jest.fn(() => ({
|
||||
info: jest.fn(), warn: jest.fn(), error: jest.fn()
|
||||
}))
|
||||
}));
|
||||
jest.doMock('../../services/scraperService', () => ({
|
||||
runScrape: jest.fn()
|
||||
}));
|
||||
jest.doMock('uuid', () => ({
|
||||
v4: jest.fn(() => 'mock-uuid')
|
||||
}));
|
||||
|
||||
const freshCron = require('node-cron');
|
||||
const { initializeScheduler: freshInit } = require('../../jobs/scraperJob');
|
||||
|
||||
freshInit(mockDb);
|
||||
|
||||
// Should keep the original schedule, not adjust
|
||||
expect(freshCron.schedule).toHaveBeenCalledWith(
|
||||
'0 */2 * * *',
|
||||
expect.any(Function),
|
||||
expect.any(Object)
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('cron job callback', () => {
|
||||
test('should acquire mutex lock before running scraper', async () => {
|
||||
jest.resetModules();
|
||||
|
||||
jest.doMock('../../config/scraper', () => ({
|
||||
SCRAPER_ENABLED: true,
|
||||
SCRAPER_SCHEDULE: '0 6 * * *',
|
||||
SCRAPER_TIMEZONE: 'UTC'
|
||||
}));
|
||||
|
||||
let capturedCallback;
|
||||
jest.doMock('node-cron', () => ({
|
||||
validate: jest.fn(() => true),
|
||||
schedule: jest.fn((expr, cb) => {
|
||||
capturedCallback = cb;
|
||||
return { stop: jest.fn() };
|
||||
})
|
||||
}));
|
||||
|
||||
const freshRunScrape = jest.fn(() => Promise.resolve({ status: 'success' }));
|
||||
jest.doMock('../../services/scraperService', () => ({
|
||||
runScrape: freshRunScrape
|
||||
}));
|
||||
jest.doMock('../../services/scraperLogger', () => ({
|
||||
createLogger: jest.fn(() => ({
|
||||
info: jest.fn(), warn: jest.fn(), error: jest.fn()
|
||||
}))
|
||||
}));
|
||||
jest.doMock('uuid', () => ({
|
||||
v4: jest.fn(() => 'scheduled-job-id')
|
||||
}));
|
||||
|
||||
const { initializeScheduler: freshInit, isScraperRunning: freshIsRunning } = require('../../jobs/scraperJob');
|
||||
|
||||
freshInit(mockDb);
|
||||
|
||||
// Execute the cron callback
|
||||
await capturedCallback();
|
||||
|
||||
// runScrape should have been called
|
||||
expect(freshRunScrape).toHaveBeenCalledWith(
|
||||
mockDb,
|
||||
expect.objectContaining({ trigger: 'scheduled', jobId: 'scheduled-job-id' })
|
||||
);
|
||||
});
|
||||
|
||||
test('should release mutex lock after completion in finally block', async () => {
|
||||
jest.resetModules();
|
||||
|
||||
jest.doMock('../../config/scraper', () => ({
|
||||
SCRAPER_ENABLED: true,
|
||||
SCRAPER_SCHEDULE: '0 6 * * *',
|
||||
SCRAPER_TIMEZONE: 'UTC'
|
||||
}));
|
||||
|
||||
let capturedCallback;
|
||||
jest.doMock('node-cron', () => ({
|
||||
validate: jest.fn(() => true),
|
||||
schedule: jest.fn((expr, cb) => {
|
||||
capturedCallback = cb;
|
||||
return { stop: jest.fn() };
|
||||
})
|
||||
}));
|
||||
|
||||
const freshRunScrape = jest.fn(() => Promise.resolve({ status: 'success' }));
|
||||
jest.doMock('../../services/scraperService', () => ({
|
||||
runScrape: freshRunScrape
|
||||
}));
|
||||
jest.doMock('../../services/scraperLogger', () => ({
|
||||
createLogger: jest.fn(() => ({
|
||||
info: jest.fn(), warn: jest.fn(), error: jest.fn()
|
||||
}))
|
||||
}));
|
||||
jest.doMock('uuid', () => ({
|
||||
v4: jest.fn(() => 'job-release-test')
|
||||
}));
|
||||
|
||||
const { initializeScheduler: freshInit, isScraperRunning: freshIsRunning } = require('../../jobs/scraperJob');
|
||||
|
||||
freshInit(mockDb);
|
||||
await capturedCallback();
|
||||
|
||||
// Lock should be released after completion
|
||||
expect(freshIsRunning()).toBe(false);
|
||||
});
|
||||
|
||||
test('should release mutex lock even when scraper throws an error', async () => {
|
||||
jest.resetModules();
|
||||
|
||||
jest.doMock('../../config/scraper', () => ({
|
||||
SCRAPER_ENABLED: true,
|
||||
SCRAPER_SCHEDULE: '0 6 * * *',
|
||||
SCRAPER_TIMEZONE: 'UTC'
|
||||
}));
|
||||
|
||||
let capturedCallback;
|
||||
jest.doMock('node-cron', () => ({
|
||||
validate: jest.fn(() => true),
|
||||
schedule: jest.fn((expr, cb) => {
|
||||
capturedCallback = cb;
|
||||
return { stop: jest.fn() };
|
||||
})
|
||||
}));
|
||||
|
||||
const freshRunScrape = jest.fn(() => Promise.reject(new Error('Scrape failed')));
|
||||
jest.doMock('../../services/scraperService', () => ({
|
||||
runScrape: freshRunScrape
|
||||
}));
|
||||
jest.doMock('../../services/scraperLogger', () => ({
|
||||
createLogger: jest.fn(() => ({
|
||||
info: jest.fn(), warn: jest.fn(), error: jest.fn()
|
||||
}))
|
||||
}));
|
||||
jest.doMock('uuid', () => ({
|
||||
v4: jest.fn(() => 'job-error-test')
|
||||
}));
|
||||
|
||||
const { initializeScheduler: freshInit, isScraperRunning: freshIsRunning } = require('../../jobs/scraperJob');
|
||||
|
||||
freshInit(mockDb);
|
||||
await capturedCallback();
|
||||
|
||||
// Lock should be released even after error
|
||||
expect(freshIsRunning()).toBe(false);
|
||||
});
|
||||
|
||||
test('should skip run if scraper is already in progress', async () => {
|
||||
jest.resetModules();
|
||||
|
||||
jest.doMock('../../config/scraper', () => ({
|
||||
SCRAPER_ENABLED: true,
|
||||
SCRAPER_SCHEDULE: '0 6 * * *',
|
||||
SCRAPER_TIMEZONE: 'UTC'
|
||||
}));
|
||||
|
||||
let capturedCallback;
|
||||
jest.doMock('node-cron', () => ({
|
||||
validate: jest.fn(() => true),
|
||||
schedule: jest.fn((expr, cb) => {
|
||||
capturedCallback = cb;
|
||||
return { stop: jest.fn() };
|
||||
})
|
||||
}));
|
||||
|
||||
const freshRunScrape = jest.fn(() => Promise.resolve({ status: 'success' }));
|
||||
jest.doMock('../../services/scraperService', () => ({
|
||||
runScrape: freshRunScrape
|
||||
}));
|
||||
const freshLogger = { info: jest.fn(), warn: jest.fn(), error: jest.fn() };
|
||||
jest.doMock('../../services/scraperLogger', () => ({
|
||||
createLogger: jest.fn(() => freshLogger)
|
||||
}));
|
||||
jest.doMock('uuid', () => ({
|
||||
v4: jest.fn(() => 'job-skip-test')
|
||||
}));
|
||||
|
||||
const { initializeScheduler: freshInit, acquireLock: freshAcquire } = require('../../jobs/scraperJob');
|
||||
|
||||
freshInit(mockDb);
|
||||
|
||||
// Simulate an already-running scraper
|
||||
freshAcquire('existing-job');
|
||||
|
||||
await capturedCallback();
|
||||
|
||||
// runScrape should NOT have been called since lock was already held
|
||||
expect(freshRunScrape).not.toHaveBeenCalled();
|
||||
expect(freshLogger.warn).toHaveBeenCalledWith(
|
||||
expect.stringContaining('already in progress')
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('stopScheduler()', () => {
|
||||
test('should stop the scheduled cron job', () => {
|
||||
jest.resetModules();
|
||||
|
||||
const mockStop = jest.fn();
|
||||
jest.doMock('../../config/scraper', () => ({
|
||||
SCRAPER_ENABLED: true,
|
||||
SCRAPER_SCHEDULE: '0 6 * * *',
|
||||
SCRAPER_TIMEZONE: 'UTC'
|
||||
}));
|
||||
jest.doMock('node-cron', () => ({
|
||||
validate: jest.fn(() => true),
|
||||
schedule: jest.fn(() => ({ stop: mockStop }))
|
||||
}));
|
||||
jest.doMock('../../services/scraperLogger', () => ({
|
||||
createLogger: jest.fn(() => ({
|
||||
info: jest.fn(), warn: jest.fn(), error: jest.fn()
|
||||
}))
|
||||
}));
|
||||
jest.doMock('../../services/scraperService', () => ({
|
||||
runScrape: jest.fn()
|
||||
}));
|
||||
jest.doMock('uuid', () => ({
|
||||
v4: jest.fn(() => 'mock-uuid')
|
||||
}));
|
||||
|
||||
const { initializeScheduler: freshInit, stopScheduler: freshStop } = require('../../jobs/scraperJob');
|
||||
|
||||
freshInit(mockDb);
|
||||
freshStop();
|
||||
|
||||
expect(mockStop).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('should be safe to call when no scheduler is active', () => {
|
||||
expect(() => stopScheduler()).not.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('getScheduleExpression()', () => {
|
||||
test('should return "disabled" when SCRAPER_ENABLED is false', () => {
|
||||
jest.resetModules();
|
||||
|
||||
jest.doMock('../../config/scraper', () => ({
|
||||
SCRAPER_ENABLED: false,
|
||||
SCRAPER_SCHEDULE: '0 6 * * *',
|
||||
SCRAPER_TIMEZONE: 'UTC'
|
||||
}));
|
||||
jest.doMock('node-cron', () => ({ validate: jest.fn(), schedule: jest.fn() }));
|
||||
jest.doMock('../../services/scraperLogger', () => ({
|
||||
createLogger: jest.fn(() => ({ info: jest.fn(), warn: jest.fn(), error: jest.fn() }))
|
||||
}));
|
||||
jest.doMock('../../services/scraperService', () => ({ runScrape: jest.fn() }));
|
||||
jest.doMock('uuid', () => ({ v4: jest.fn(() => 'mock-uuid') }));
|
||||
|
||||
const { getScheduleExpression: freshGetExpr } = require('../../jobs/scraperJob');
|
||||
|
||||
expect(freshGetExpr()).toBe('disabled');
|
||||
});
|
||||
|
||||
test('should return the configured cron expression when enabled', () => {
|
||||
jest.resetModules();
|
||||
|
||||
jest.doMock('../../config/scraper', () => ({
|
||||
SCRAPER_ENABLED: true,
|
||||
SCRAPER_SCHEDULE: '30 8 * * *',
|
||||
SCRAPER_TIMEZONE: 'UTC'
|
||||
}));
|
||||
jest.doMock('node-cron', () => ({ validate: jest.fn(), schedule: jest.fn() }));
|
||||
jest.doMock('../../services/scraperLogger', () => ({
|
||||
createLogger: jest.fn(() => ({ info: jest.fn(), warn: jest.fn(), error: jest.fn() }))
|
||||
}));
|
||||
jest.doMock('../../services/scraperService', () => ({ runScrape: jest.fn() }));
|
||||
jest.doMock('uuid', () => ({ v4: jest.fn(() => 'mock-uuid') }));
|
||||
|
||||
const { getScheduleExpression: freshGetExpr } = require('../../jobs/scraperJob');
|
||||
|
||||
expect(freshGetExpr()).toBe('30 8 * * *');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getNextScheduledRun()', () => {
|
||||
test('should return null when SCRAPER_ENABLED is false', () => {
|
||||
jest.resetModules();
|
||||
|
||||
jest.doMock('../../config/scraper', () => ({
|
||||
SCRAPER_ENABLED: false,
|
||||
SCRAPER_SCHEDULE: '0 6 * * *',
|
||||
SCRAPER_TIMEZONE: 'UTC'
|
||||
}));
|
||||
jest.doMock('node-cron', () => ({ validate: jest.fn(), schedule: jest.fn() }));
|
||||
jest.doMock('../../services/scraperLogger', () => ({
|
||||
createLogger: jest.fn(() => ({ info: jest.fn(), warn: jest.fn(), error: jest.fn() }))
|
||||
}));
|
||||
jest.doMock('../../services/scraperService', () => ({ runScrape: jest.fn() }));
|
||||
jest.doMock('uuid', () => ({ v4: jest.fn(() => 'mock-uuid') }));
|
||||
|
||||
const { getNextScheduledRun: freshGetNext } = require('../../jobs/scraperJob');
|
||||
|
||||
expect(freshGetNext()).toBeNull();
|
||||
});
|
||||
|
||||
test('should return null when no scheduler is active', () => {
|
||||
jest.resetModules();
|
||||
|
||||
jest.doMock('../../config/scraper', () => ({
|
||||
SCRAPER_ENABLED: true,
|
||||
SCRAPER_SCHEDULE: '0 6 * * *',
|
||||
SCRAPER_TIMEZONE: 'UTC'
|
||||
}));
|
||||
jest.doMock('node-cron', () => ({ validate: jest.fn(), schedule: jest.fn() }));
|
||||
jest.doMock('../../services/scraperLogger', () => ({
|
||||
createLogger: jest.fn(() => ({ info: jest.fn(), warn: jest.fn(), error: jest.fn() }))
|
||||
}));
|
||||
jest.doMock('../../services/scraperService', () => ({ runScrape: jest.fn() }));
|
||||
jest.doMock('uuid', () => ({ v4: jest.fn(() => 'mock-uuid') }));
|
||||
|
||||
const { getNextScheduledRun: freshGetNext } = require('../../jobs/scraperJob');
|
||||
|
||||
// No initializeScheduler called, so no scheduledJob
|
||||
expect(freshGetNext()).toBeNull();
|
||||
});
|
||||
|
||||
test('should return an ISO date string when scheduler is active', () => {
|
||||
jest.resetModules();
|
||||
|
||||
jest.doMock('../../config/scraper', () => ({
|
||||
SCRAPER_ENABLED: true,
|
||||
SCRAPER_SCHEDULE: '0 6 * * *',
|
||||
SCRAPER_TIMEZONE: 'UTC'
|
||||
}));
|
||||
jest.doMock('node-cron', () => ({
|
||||
validate: jest.fn(() => true),
|
||||
schedule: jest.fn(() => ({ stop: jest.fn() }))
|
||||
}));
|
||||
jest.doMock('../../services/scraperLogger', () => ({
|
||||
createLogger: jest.fn(() => ({ info: jest.fn(), warn: jest.fn(), error: jest.fn() }))
|
||||
}));
|
||||
jest.doMock('../../services/scraperService', () => ({ runScrape: jest.fn() }));
|
||||
jest.doMock('uuid', () => ({ v4: jest.fn(() => 'mock-uuid') }));
|
||||
|
||||
const { initializeScheduler: freshInit, getNextScheduledRun: freshGetNext } = require('../../jobs/scraperJob');
|
||||
|
||||
freshInit(mockDb);
|
||||
|
||||
const nextRun = freshGetNext();
|
||||
// Should be a valid ISO date string
|
||||
expect(nextRun).not.toBeNull();
|
||||
expect(new Date(nextRun).toISOString()).toBe(nextRun);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user