From 95019a86cf95fa2b2484a33ff8486ee9c91d92e2 Mon Sep 17 00:00:00 2001 From: Collin Duncan <3679940+cgduncan7@users.noreply.github.com> Date: Wed, 10 Apr 2024 11:30:51 +0200 Subject: [PATCH] Fixing some issues with the cron job waiting until 7 and adding a unit test for it --- src/reservations/cron.ts | 34 +++++--- test/unit/reservations/cron.spec.ts | 121 ++++++++++++++++++++++++++++ 2 files changed, 142 insertions(+), 13 deletions(-) create mode 100644 test/unit/reservations/cron.spec.ts diff --git a/src/reservations/cron.ts b/src/reservations/cron.ts index 8e64dce..31e7a9c 100644 --- a/src/reservations/cron.ts +++ b/src/reservations/cron.ts @@ -1,6 +1,7 @@ import { InjectQueue } from '@nestjs/bull' import { Inject, Injectable } from '@nestjs/common' import { Cron, CronExpression } from '@nestjs/schedule' +import { Dayjs } from 'dayjs' import dayjs from '../common/dayjs' import { LoggerService } from '../logger/service.logger' @@ -30,6 +31,19 @@ export class ReservationsCronService { private readonly loggerService: LoggerService, ) {} + private async sleepUntil(time: Dayjs) { + let keepSleeping = true + + while (keepSleeping) { + const now = dayjs() + keepSleeping = !time.isBefore(now) + if (!keepSleeping) break + await new Promise((res) => { + setTimeout(() => res(null), 50) + }) + } + } + @Cron('55 06 * * *', { name: 'handleDailyReservations', timeZone: 'Europe/Amsterdam', @@ -48,19 +62,13 @@ export class ReservationsCronService { this.loggerService.log(`Warmed up! Waiting for go-time`) - let not7AM = true - const waitTime = 10 - const time7AM = dayjs() - .set('hour', 7) - .set('minute', 0) - .set('second', 0) - .set('millisecond', 0) - - while (not7AM) { - not7AM = !time7AM.isBefore(dayjs()) && time7AM.diff(dayjs()) >= waitTime // current time is more than 100ms from 7am - if (!not7AM) break - await new Promise((res) => setTimeout(res, waitTime)) // wait for waitTime and then try again - } + await this.sleepUntil( + dayjs() + .set('hour', 7) + .set('minute', 0) + .set('second', 0) + .set('millisecond', 0), + ) this.loggerService.log(`It's go-time`) diff --git a/test/unit/reservations/cron.spec.ts b/test/unit/reservations/cron.spec.ts new file mode 100644 index 0000000..5c4c7ea --- /dev/null +++ b/test/unit/reservations/cron.spec.ts @@ -0,0 +1,121 @@ +import { ConfigModule } from '@nestjs/config' +import { Test, TestingModule } from '@nestjs/testing' +import { TypeOrmModule } from '@nestjs/typeorm' + +import { LoggerModule } from '../../../src/logger/module' +import { LoggerService } from '../../../src/logger/service.logger' +import { NtfyModule } from '../../../src/ntfy/module' +import { NtfyProvider } from '../../../src/ntfy/provider' +import { ReservationsCronService } from '../../../src/reservations/cron' +import { Reservation } from '../../../src/reservations/entity' +import { ReservationsModule } from '../../../src/reservations/module' +import { ReservationsService } from '../../../src/reservations/service' +import { BaanReserverenService } from '../../../src/runner/baanreserveren/service' +import { RunnerModule } from '../../../src/runner/module' + +describe('reservations.cron', () => { + let module: TestingModule + let cronService: ReservationsCronService + + beforeAll(async () => { + module = await Test.createTestingModule({ + imports: [ + ReservationsModule, + RunnerModule, + NtfyModule, + LoggerModule, + ConfigModule.forRoot({ isGlobal: true }), + TypeOrmModule.forRoot({ + type: 'sqlite', + database: ':memory:', + entities: [Reservation], + }), + ], + }).compile() + + cronService = module.get(ReservationsCronService) + }) + + afterAll(async () => { + await module.close() + }) + + describe('handleDailyReservations', () => { + beforeEach(() => { + jest.spyOn( + module.get(NtfyProvider), + 'sendCronStartNotification', + ) + jest.spyOn( + module.get(NtfyProvider), + 'sendCronStopNotification', + ) + }) + + describe('has scheduleable reservations', () => { + const stubbedReservation = { id: 'abc-123' } + let warmupSpy: jest.SpyInstance + let performReservationSpy: jest.SpyInstance + let loggerSpy: jest.SpyInstance + const loggerInvocationDates: { msg: string; timestamp: Date }[] = [] + + beforeAll(async () => { + jest + .spyOn( + module.get(ReservationsService), + 'getSchedulable', + ) + // @ts-expect-error stubbed reservation + .mockResolvedValue([stubbedReservation]) + + warmupSpy = jest + .spyOn( + module.get(BaanReserverenService), + 'warmup', + ) + .mockReturnValue(new Promise((res) => setTimeout(res, 1000))) + + performReservationSpy = jest + .spyOn( + module.get(BaanReserverenService), + 'performReservation', + ) + .mockResolvedValue() + + loggerSpy = jest + .spyOn(module.get(LoggerService), 'log') + .mockImplementation((msg: string) => { + loggerInvocationDates.push({ msg, timestamp: new Date() }) + }) + + jest + .useFakeTimers({ advanceTimers: true }) + .setSystemTime(new Date('2024-01-01T06:59:58.050+01:00')) + + await cronService.handleDailyReservations() + }) + + afterAll(() => { + jest.useRealTimers() + }) + + it('should perform warmup', () => { + expect(warmupSpy).toHaveBeenCalledTimes(1) + }) + + it('should wait until 7am', () => { + expect(loggerSpy).toHaveBeenCalledTimes(5) + const { timestamp: goTimeInvocation } = + loggerInvocationDates.find(({ msg }) => msg === `It's go-time`) ?? {} + expect(goTimeInvocation).toBeDefined() + expect(goTimeInvocation?.getHours()).toEqual(7) + expect(goTimeInvocation?.getSeconds()).toEqual(0) + expect(goTimeInvocation?.getMilliseconds()).toBeLessThanOrEqual(100) + }) + + it('should perform reservations', () => { + expect(performReservationSpy).toHaveBeenCalledWith(stubbedReservation) + }) + }) + }) +})