diff --git a/src/runner/baanreserveren/service.ts b/src/runner/baanreserveren/service.ts index 77f7fa1..bae48bb 100644 --- a/src/runner/baanreserveren/service.ts +++ b/src/runner/baanreserveren/service.ts @@ -12,7 +12,7 @@ import { MonitorType } from '../../monitoring/entity' import { Opponent, Reservation } from '../../reservations/entity' import { EmptyPage } from '../pages/empty' -const BAAN_RESERVEREN_ROOT_URL = 'https://squashcity.baanreserveren.nl' +export const BAAN_RESERVEREN_ROOT_URL = 'https://squashcity.baanreserveren.nl' export enum BaanReserverenUrls { Reservations = '/reservations', @@ -33,7 +33,7 @@ interface BaanReserverenSession { } // TODO: Add to DB to make configurable -enum CourtSlot { +export enum CourtSlot { One = '51', Two = '52', Three = '53', @@ -67,8 +67,8 @@ const CourtSlotToNumber: Record = { // Lower is better const CourtRank: Record = { - [CourtSlot.One]: 0, - [CourtSlot.Two]: 0, + [CourtSlot.One]: 2, + [CourtSlot.Two]: 1, [CourtSlot.Three]: 0, [CourtSlot.Four]: 0, [CourtSlot.Five]: 99, // shitty @@ -77,9 +77,9 @@ const CourtRank: Record = { [CourtSlot.Eight]: 0, [CourtSlot.Nine]: 0, [CourtSlot.Ten]: 0, - [CourtSlot.Eleven]: 1, // no one likes upstairs - [CourtSlot.Twelve]: 1, // no one likes upstairs - [CourtSlot.Thirteen]: 1, // no one likes upstairs + [CourtSlot.Eleven]: 10, // no one likes upstairs + [CourtSlot.Twelve]: 9, // no one likes upstairs + [CourtSlot.Thirteen]: 9, // no one likes upstairs } as const enum StartTimeClass { @@ -680,7 +680,11 @@ export class BaanReserverenService { const time = date.format('HH:mm') for (const [timeClass, times] of Object.entries(StartTimeClassStartTimes)) { if (times.includes(time)) { - return StartTimeClassCourtSlots[timeClass as StartTimeClass] + const courtSlots = [ + ...StartTimeClassCourtSlots[timeClass as StartTimeClass], + ] + // sort by ranking + return courtSlots.sort((a, b) => CourtRank[a] - CourtRank[b]) } } } diff --git a/test/unit/baanreserveren/service.spec.ts b/test/unit/baanreserveren/service.spec.ts new file mode 100644 index 0000000..7d90da6 --- /dev/null +++ b/test/unit/baanreserveren/service.spec.ts @@ -0,0 +1,135 @@ +import { getQueueToken } from '@nestjs/bull' +import { ConfigService } from '@nestjs/config' +import { Test, TestingModule } from '@nestjs/testing' + +import dayjs from '../../../src/common/dayjs' +import { LoggerService } from '../../../src/logger/service.logger' +import { MONITORING_QUEUE_NAME } from '../../../src/monitoring/config' +import { Reservation } from '../../../src/reservations/entity' +import { + BAAN_RESERVEREN_ROOT_URL, + BaanReserverenService, + CourtSlot, +} from '../../../src/runner/baanreserveren/service' +import { EmptyPage } from '../../../src/runner/pages/empty' + +describe('baanreserveren.service', () => { + let module: TestingModule + let pageGotoSpy: jest.SpyInstance + let brService: BaanReserverenService + + beforeAll(async () => { + pageGotoSpy = jest + .fn() + .mockImplementation(() => Promise.resolve({ status: () => 200 })) + module = await Test.createTestingModule({ + providers: [ + BaanReserverenService, + { + provide: ConfigService, + useValue: { getOrThrow: jest.fn().mockReturnValue('test') }, + }, + { + provide: LoggerService, + useValue: { debug: jest.fn(), warn: jest.fn() }, + }, + { + provide: EmptyPage, + useValue: { + waitForNetworkIdle: jest.fn().mockResolvedValue(null), + waitForSelector: jest.fn().mockResolvedValue(undefined), + goto: pageGotoSpy, + url: jest + .fn() + .mockReturnValue({ includes: jest.fn().mockReturnValue(true) }), + $: jest.fn().mockResolvedValue(undefined), + }, + }, + { + provide: getQueueToken(MONITORING_QUEUE_NAME), + useValue: { add: jest.fn() }, + }, + ], + }).compile() + brService = module.get(BaanReserverenService) + }) + + describe('performSpeedyReservation', () => { + it.each([ + [18, 15, CourtSlot.Seven, CourtSlot.Six], + [18, 30, CourtSlot.Three, CourtSlot.One], + [18, 45, CourtSlot.Twelve, CourtSlot.Thirteen], + ])( + 'should try highest ranked court first', + async (startHour, startMinute, preferredCourt, backupCourt) => { + const start = dayjs() + .set('hour', startHour) + .set('minute', startMinute) + .set('second', 0) + .set('millisecond', 0) + const reservation = new Reservation({ + id: '1', + ownerId: '1', + dateRangeStart: start, + dateRangeEnd: start.add(45, 'minute'), + opponents: [], + }) + await brService.performSpeedyReservation(reservation) + expect(pageGotoSpy).toHaveBeenCalledWith( + `${BAAN_RESERVEREN_ROOT_URL}/reservations/make/${preferredCourt}/${ + start.valueOf() / 1000 + }`, + ) + expect(pageGotoSpy).not.toHaveBeenCalledWith( + `${BAAN_RESERVEREN_ROOT_URL}/reservations/make/${backupCourt}/${ + start.valueOf() / 1000 + }`, + ) + }, + ) + + it.each([ + [18, 15, CourtSlot.Seven, CourtSlot.Eight], + [18, 30, CourtSlot.Three, CourtSlot.Four], + [18, 45, CourtSlot.Twelve, CourtSlot.Thirteen], + ])( + 'should try backup if first rank is taken', + async (startHour, startMinute, preferredCourt, backupCourt) => { + pageGotoSpy.mockImplementation((url: string) => { + if ( + url === + `${BAAN_RESERVEREN_ROOT_URL}/reservations/make/${preferredCourt}/${ + start.valueOf() / 1000 + }` + ) { + return Promise.resolve({ status: () => 400 }) // fail on the preferred court + } + return Promise.resolve({ status: () => 200 }) + }) + const start = dayjs() + .set('hour', startHour) + .set('minute', startMinute) + .set('second', 0) + .set('millisecond', 0) + const reservation = new Reservation({ + id: '1', + ownerId: '1', + dateRangeStart: start, + dateRangeEnd: start.add(45, 'minute'), + opponents: [], + }) + await brService.performSpeedyReservation(reservation) + expect(pageGotoSpy).toHaveBeenCalledWith( + `${BAAN_RESERVEREN_ROOT_URL}/reservations/make/${preferredCourt}/${ + start.valueOf() / 1000 + }`, + ) + expect(pageGotoSpy).toHaveBeenCalledWith( + `${BAAN_RESERVEREN_ROOT_URL}/reservations/make/${backupCourt}/${ + start.valueOf() / 1000 + }`, + ) + }, + ) + }) +})