Adding cron job for running reservations; changing how the runner worked to only instantiate one runner and reuse it; lots of tests!
This commit is contained in:
parent
a765df3530
commit
69249a11dc
14 changed files with 202 additions and 56 deletions
48
package-lock.json
generated
48
package-lock.json
generated
|
|
@ -14,6 +14,7 @@
|
||||||
"axios": "^1.2.0",
|
"axios": "^1.2.0",
|
||||||
"dayjs": "^1.11.6",
|
"dayjs": "^1.11.6",
|
||||||
"mysql": "^2.18.1",
|
"mysql": "^2.18.1",
|
||||||
|
"node-cron": "^3.0.2",
|
||||||
"puppeteer": "^19.1.0",
|
"puppeteer": "^19.1.0",
|
||||||
"uuid": "^9.0.0"
|
"uuid": "^9.0.0"
|
||||||
},
|
},
|
||||||
|
|
@ -24,6 +25,7 @@
|
||||||
"@types/jest": "^29.2.3",
|
"@types/jest": "^29.2.3",
|
||||||
"@types/mysql": "^2.15.21",
|
"@types/mysql": "^2.15.21",
|
||||||
"@types/node": "^18.11.9",
|
"@types/node": "^18.11.9",
|
||||||
|
"@types/node-cron": "^3.0.6",
|
||||||
"@types/puppeteer": "^5.4.7",
|
"@types/puppeteer": "^5.4.7",
|
||||||
"@types/uuid": "^8.3.4",
|
"@types/uuid": "^8.3.4",
|
||||||
"@typescript-eslint/eslint-plugin": "^5.42.0",
|
"@typescript-eslint/eslint-plugin": "^5.42.0",
|
||||||
|
|
@ -3339,6 +3341,12 @@
|
||||||
"integrity": "sha512-CRpX21/kGdzjOpFsZSkcrXMGIBWMGNIHXXBVFSH+ggkftxg+XYP20TESbh+zFvFj3EQOl5byk0HTRn1IL6hbqg==",
|
"integrity": "sha512-CRpX21/kGdzjOpFsZSkcrXMGIBWMGNIHXXBVFSH+ggkftxg+XYP20TESbh+zFvFj3EQOl5byk0HTRn1IL6hbqg==",
|
||||||
"devOptional": true
|
"devOptional": true
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/node-cron": {
|
||||||
|
"version": "3.0.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/node-cron/-/node-cron-3.0.6.tgz",
|
||||||
|
"integrity": "sha512-Qu9dpjkgj2JmzRmDMVzpt2dFKuJ7wma0mxEvbbgomwkhAdHKT2LpSLYHawzd9OeeP4HsyhmcV9o/xLgJyPNcgw==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"node_modules/@types/parse-json": {
|
"node_modules/@types/parse-json": {
|
||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz",
|
||||||
|
|
@ -9517,6 +9525,25 @@
|
||||||
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-5.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-5.0.0.tgz",
|
||||||
"integrity": "sha512-CvkDw2OEnme7ybCykJpVcKH+uAOLV2qLqiyla128dN9TkEWfrYmxG6C2boDe5KcNQqZF3orkqzGgOMvZ/JNekA=="
|
"integrity": "sha512-CvkDw2OEnme7ybCykJpVcKH+uAOLV2qLqiyla128dN9TkEWfrYmxG6C2boDe5KcNQqZF3orkqzGgOMvZ/JNekA=="
|
||||||
},
|
},
|
||||||
|
"node_modules/node-cron": {
|
||||||
|
"version": "3.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/node-cron/-/node-cron-3.0.2.tgz",
|
||||||
|
"integrity": "sha512-iP8l0yGlNpE0e6q1o185yOApANRe47UPbLf4YxfbiNHt/RU5eBcGB/e0oudruheSf+LQeDMezqC5BVAb5wwRcQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"uuid": "8.3.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/node-cron/node_modules/uuid": {
|
||||||
|
"version": "8.3.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
|
||||||
|
"integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
|
||||||
|
"bin": {
|
||||||
|
"uuid": "dist/bin/uuid"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/node-fetch": {
|
"node_modules/node-fetch": {
|
||||||
"version": "2.6.7",
|
"version": "2.6.7",
|
||||||
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz",
|
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz",
|
||||||
|
|
@ -13540,6 +13567,12 @@
|
||||||
"integrity": "sha512-CRpX21/kGdzjOpFsZSkcrXMGIBWMGNIHXXBVFSH+ggkftxg+XYP20TESbh+zFvFj3EQOl5byk0HTRn1IL6hbqg==",
|
"integrity": "sha512-CRpX21/kGdzjOpFsZSkcrXMGIBWMGNIHXXBVFSH+ggkftxg+XYP20TESbh+zFvFj3EQOl5byk0HTRn1IL6hbqg==",
|
||||||
"devOptional": true
|
"devOptional": true
|
||||||
},
|
},
|
||||||
|
"@types/node-cron": {
|
||||||
|
"version": "3.0.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/node-cron/-/node-cron-3.0.6.tgz",
|
||||||
|
"integrity": "sha512-Qu9dpjkgj2JmzRmDMVzpt2dFKuJ7wma0mxEvbbgomwkhAdHKT2LpSLYHawzd9OeeP4HsyhmcV9o/xLgJyPNcgw==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"@types/parse-json": {
|
"@types/parse-json": {
|
||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz",
|
||||||
|
|
@ -18223,6 +18256,21 @@
|
||||||
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-5.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-5.0.0.tgz",
|
||||||
"integrity": "sha512-CvkDw2OEnme7ybCykJpVcKH+uAOLV2qLqiyla128dN9TkEWfrYmxG6C2boDe5KcNQqZF3orkqzGgOMvZ/JNekA=="
|
"integrity": "sha512-CvkDw2OEnme7ybCykJpVcKH+uAOLV2qLqiyla128dN9TkEWfrYmxG6C2boDe5KcNQqZF3orkqzGgOMvZ/JNekA=="
|
||||||
},
|
},
|
||||||
|
"node-cron": {
|
||||||
|
"version": "3.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/node-cron/-/node-cron-3.0.2.tgz",
|
||||||
|
"integrity": "sha512-iP8l0yGlNpE0e6q1o185yOApANRe47UPbLf4YxfbiNHt/RU5eBcGB/e0oudruheSf+LQeDMezqC5BVAb5wwRcQ==",
|
||||||
|
"requires": {
|
||||||
|
"uuid": "8.3.2"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"uuid": {
|
||||||
|
"version": "8.3.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
|
||||||
|
"integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg=="
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node-fetch": {
|
"node-fetch": {
|
||||||
"version": "2.6.7",
|
"version": "2.6.7",
|
||||||
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz",
|
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz",
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@
|
||||||
"preinstall": "export CXX=g++-12",
|
"preinstall": "export CXX=g++-12",
|
||||||
"clean": "rm -r ./dist || true",
|
"clean": "rm -r ./dist || true",
|
||||||
"build": "rollup -c src/server/rollup.config.js",
|
"build": "rollup -c src/server/rollup.config.js",
|
||||||
|
"test": "jest",
|
||||||
"test:unit": "jest tests/unit/*",
|
"test:unit": "jest tests/unit/*",
|
||||||
"test:unit:clean": "npm run test:clear-cache && npm run test:unit",
|
"test:unit:clean": "npm run test:clear-cache && npm run test:unit",
|
||||||
"test:integration": "jest tests/integration/*",
|
"test:integration": "jest tests/integration/*",
|
||||||
|
|
@ -29,6 +30,7 @@
|
||||||
"axios": "^1.2.0",
|
"axios": "^1.2.0",
|
||||||
"dayjs": "^1.11.6",
|
"dayjs": "^1.11.6",
|
||||||
"mysql": "^2.18.1",
|
"mysql": "^2.18.1",
|
||||||
|
"node-cron": "^3.0.2",
|
||||||
"puppeteer": "^19.1.0",
|
"puppeteer": "^19.1.0",
|
||||||
"uuid": "^9.0.0"
|
"uuid": "^9.0.0"
|
||||||
},
|
},
|
||||||
|
|
@ -39,6 +41,7 @@
|
||||||
"@types/jest": "^29.2.3",
|
"@types/jest": "^29.2.3",
|
||||||
"@types/mysql": "^2.15.21",
|
"@types/mysql": "^2.15.21",
|
||||||
"@types/node": "^18.11.9",
|
"@types/node": "^18.11.9",
|
||||||
|
"@types/node-cron": "^3.0.6",
|
||||||
"@types/puppeteer": "^5.4.7",
|
"@types/puppeteer": "^5.4.7",
|
||||||
"@types/uuid": "^8.3.4",
|
"@types/uuid": "^8.3.4",
|
||||||
"@typescript-eslint/eslint-plugin": "^5.42.0",
|
"@typescript-eslint/eslint-plugin": "^5.42.0",
|
||||||
|
|
|
||||||
|
|
@ -156,12 +156,12 @@ export class Reservation {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
public static async fetch(id: number): Promise<Reservation | null> {
|
public static async fetchById(id: number): Promise<Reservation | null> {
|
||||||
const response = await query<SqlReservation>(
|
const response = await query<SqlReservation>(
|
||||||
`
|
`
|
||||||
SELECT *
|
SELECT *
|
||||||
FROM reservations
|
FROM reservations
|
||||||
WHERE id = ?
|
WHERE id = ?;
|
||||||
`,
|
`,
|
||||||
[id]
|
[id]
|
||||||
)
|
)
|
||||||
|
|
@ -184,6 +184,35 @@ export class Reservation {
|
||||||
|
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static async fetchFirst(): Promise<Reservation | null> {
|
||||||
|
const response = await query<SqlReservation>(
|
||||||
|
`
|
||||||
|
SELECT *
|
||||||
|
FROM reservations
|
||||||
|
ORDER BY date_range_start DESC
|
||||||
|
LIMIT 1;
|
||||||
|
`
|
||||||
|
)
|
||||||
|
|
||||||
|
if (response.results.length === 1) {
|
||||||
|
const sqlReservation = response.results[0]
|
||||||
|
const res = new Reservation(
|
||||||
|
{
|
||||||
|
username: sqlReservation.username,
|
||||||
|
password: sqlReservation.password,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
start: dayjs(sqlReservation.date_range_start),
|
||||||
|
end: dayjs(sqlReservation.date_range_end),
|
||||||
|
},
|
||||||
|
{ id: sqlReservation.opponent_id, name: sqlReservation.opponent_name }
|
||||||
|
)
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SerializedDateRange {
|
export interface SerializedDateRange {
|
||||||
|
|
|
||||||
24
src/common/reserver.ts
Normal file
24
src/common/reserver.ts
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
import { Reservation } from './reservation'
|
||||||
|
import { Runner } from './runner'
|
||||||
|
|
||||||
|
let runner: Runner | undefined
|
||||||
|
const getRunner = () => {
|
||||||
|
if (!runner) {
|
||||||
|
runner = new Runner({ headless: true })
|
||||||
|
}
|
||||||
|
return runner
|
||||||
|
}
|
||||||
|
|
||||||
|
export const reserve = async (reservation?: Reservation) => {
|
||||||
|
let reservationToPerform = reservation
|
||||||
|
if (!reservationToPerform) {
|
||||||
|
reservationToPerform = (await Reservation.fetchFirst()) || undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!reservationToPerform) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const runner = getRunner()
|
||||||
|
await runner.run(reservationToPerform)
|
||||||
|
}
|
||||||
|
|
@ -11,65 +11,51 @@ import { Logger } from './logger'
|
||||||
import { Opponent, Reservation } from './reservation'
|
import { Opponent, Reservation } from './reservation'
|
||||||
|
|
||||||
export class Runner {
|
export class Runner {
|
||||||
private readonly username: string
|
|
||||||
private readonly password: string
|
|
||||||
private readonly reservations: Reservation[]
|
|
||||||
|
|
||||||
private browser: Browser | undefined
|
private browser: Browser | undefined
|
||||||
private page: Page | undefined
|
private page: Page | undefined
|
||||||
|
private options?: LaunchOptions &
|
||||||
|
BrowserLaunchArgumentOptions &
|
||||||
|
BrowserConnectOptions
|
||||||
|
|
||||||
public constructor(
|
constructor(
|
||||||
username: string,
|
|
||||||
password: string,
|
|
||||||
reservations: Reservation[]
|
|
||||||
) {
|
|
||||||
this.username = username
|
|
||||||
this.password = password
|
|
||||||
this.reservations = reservations
|
|
||||||
}
|
|
||||||
|
|
||||||
public async run(
|
|
||||||
options?: LaunchOptions &
|
options?: LaunchOptions &
|
||||||
BrowserLaunchArgumentOptions &
|
BrowserLaunchArgumentOptions &
|
||||||
BrowserConnectOptions
|
BrowserConnectOptions
|
||||||
): Promise<Reservation[]> {
|
) {
|
||||||
Logger.debug('Launching browser')
|
this.options = options
|
||||||
this.browser = await puppeteer.launch(options)
|
|
||||||
this.page = await this.browser?.newPage()
|
|
||||||
await this.login()
|
|
||||||
return await this.makeReservations()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async login() {
|
public async run(reservation: Reservation): Promise<boolean> {
|
||||||
|
Logger.debug('Launching browser')
|
||||||
|
if (!this.browser) {
|
||||||
|
this.browser = await puppeteer.launch(this.options)
|
||||||
|
}
|
||||||
|
this.page = await this.browser?.newPage()
|
||||||
|
await this.login(reservation.user.username, reservation.user.password)
|
||||||
|
return this.makeReservation(reservation)
|
||||||
|
}
|
||||||
|
|
||||||
|
private async login(username: string, password: string) {
|
||||||
Logger.debug('Logging in')
|
Logger.debug('Logging in')
|
||||||
await this.page?.goto('https://squashcity.baanreserveren.nl/')
|
await this.page?.goto('https://squashcity.baanreserveren.nl/')
|
||||||
await this.page
|
await this.page
|
||||||
?.waitForSelector('input[name=username]')
|
?.waitForSelector('input[name=username]')
|
||||||
.then((i) => i?.type(this.username))
|
.then((i) => i?.type(username))
|
||||||
await this.page
|
await this.page?.$('input[name=password]').then((i) => i?.type(password))
|
||||||
?.$('input[name=password]')
|
|
||||||
.then((i) => i?.type(this.password))
|
|
||||||
await this.page?.$('button').then((b) => b?.click())
|
await this.page?.$('button').then((b) => b?.click())
|
||||||
}
|
}
|
||||||
|
|
||||||
private async makeReservations(): Promise<Reservation[]> {
|
private async makeReservation(reservation: Reservation): Promise<boolean> {
|
||||||
for (let i = 0; i < this.reservations.length; i++) {
|
|
||||||
Logger.debug('Making reservation', this.reservations[i].format())
|
|
||||||
await this.makeReservation(this.reservations[i])
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.reservations
|
|
||||||
}
|
|
||||||
|
|
||||||
private async makeReservation(reservation: Reservation): Promise<void> {
|
|
||||||
try {
|
try {
|
||||||
await this.navigateToDay(reservation.dateRange.start)
|
await this.navigateToDay(reservation.dateRange.start)
|
||||||
await this.selectAvailableTime(reservation)
|
await this.selectAvailableTime(reservation)
|
||||||
await this.selectOpponent(reservation.opponent)
|
await this.selectOpponent(reservation.opponent)
|
||||||
await this.confirmReservation()
|
await this.confirmReservation()
|
||||||
reservation.booked = true
|
reservation.booked = true
|
||||||
|
return true
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
Logger.error('Error making reservation', reservation.format())
|
Logger.error('Error making reservation', reservation.format())
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import { v4 } from 'uuid'
|
||||||
import { Logger, LogLevel } from './logger'
|
import { Logger, LogLevel } from './logger'
|
||||||
import { Reservation } from './reservation'
|
import { Reservation } from './reservation'
|
||||||
import { validateJSONRequest } from './request'
|
import { validateJSONRequest } from './request'
|
||||||
|
import { reserve } from './reserver'
|
||||||
|
|
||||||
export interface ScheduledReservation {
|
export interface ScheduledReservation {
|
||||||
reservation: Reservation
|
reservation: Reservation
|
||||||
|
|
@ -16,7 +17,7 @@ export interface SchedulerResult {
|
||||||
|
|
||||||
export type SchedulerInput = Record<string, unknown>
|
export type SchedulerInput = Record<string, unknown>
|
||||||
|
|
||||||
export const work = async (
|
export const schedule = async (
|
||||||
payload: SchedulerInput
|
payload: SchedulerInput
|
||||||
): Promise<SchedulerResult> => {
|
): Promise<SchedulerResult> => {
|
||||||
Logger.instantiate('scheduler', v4(), LogLevel.DEBUG)
|
Logger.instantiate('scheduler', v4(), LogLevel.DEBUG)
|
||||||
|
|
@ -38,6 +39,9 @@ export const work = async (
|
||||||
Logger.debug(
|
Logger.debug(
|
||||||
'Reservation date is more than 7 days away; saving for later reservation'
|
'Reservation date is more than 7 days away; saving for later reservation'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
await Reservation.save(reservation)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
scheduledReservation: {
|
scheduledReservation: {
|
||||||
reservation,
|
reservation,
|
||||||
|
|
@ -47,6 +51,7 @@ export const work = async (
|
||||||
}
|
}
|
||||||
|
|
||||||
Logger.info('Reservation request can be performed now')
|
Logger.info('Reservation request can be performed now')
|
||||||
|
await reserve(reservation)
|
||||||
return {
|
return {
|
||||||
scheduledReservation: { reservation },
|
scheduledReservation: { reservation },
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import { connect, disconnect } from './common/database'
|
import { connect, disconnect } from './common/database'
|
||||||
import { Logger } from './common/logger'
|
import { Logger } from './common/logger'
|
||||||
import server from './server'
|
import server from './server/http'
|
||||||
|
import { startTasks, stopTasks } from './server/cron'
|
||||||
|
|
||||||
process.on('unhandledRejection', (reason) => {
|
process.on('unhandledRejection', (reason) => {
|
||||||
Logger.error('unhandled rejection', { reason })
|
Logger.error('unhandled rejection', { reason })
|
||||||
|
|
@ -12,10 +13,13 @@ process.on('uncaughtException', (error, origin) => {
|
||||||
|
|
||||||
process.on('beforeExit', async () => {
|
process.on('beforeExit', async () => {
|
||||||
await disconnect()
|
await disconnect()
|
||||||
|
stopTasks()
|
||||||
})
|
})
|
||||||
|
|
||||||
const port = process.env.SERVER_PORT || 3000
|
const port = process.env.SERVER_PORT || 3000
|
||||||
|
|
||||||
|
startTasks()
|
||||||
|
|
||||||
server.listen(port, async () => {
|
server.listen(port, async () => {
|
||||||
Logger.info('server ready and listening', { port })
|
Logger.info('server ready and listening', { port })
|
||||||
await connect()
|
await connect()
|
||||||
|
|
|
||||||
|
|
@ -9,8 +9,8 @@ const run = async (request: Record<string, any>) => {
|
||||||
const { user, dateRange, opponent } = request
|
const { user, dateRange, opponent } = request
|
||||||
const reservation = new Reservation(user, dateRange, opponent)
|
const reservation = new Reservation(user, dateRange, opponent)
|
||||||
|
|
||||||
const runner = new Runner(username, password, [reservation])
|
const runner = new Runner({ headless: false })
|
||||||
await runner.run({ headless: false })
|
await runner.run(reservation)
|
||||||
}
|
}
|
||||||
|
|
||||||
// get supplied args
|
// get supplied args
|
||||||
|
|
|
||||||
42
src/server/cron.ts
Normal file
42
src/server/cron.ts
Normal file
|
|
@ -0,0 +1,42 @@
|
||||||
|
import { schedule, ScheduledTask, ScheduleOptions } from 'node-cron'
|
||||||
|
import { Logger } from '../common/logger'
|
||||||
|
import { reserve } from '../common/reserver'
|
||||||
|
|
||||||
|
const tasks: ScheduledTask[] = []
|
||||||
|
|
||||||
|
const getTaskConfig = (name: string): ScheduleOptions => ({
|
||||||
|
name,
|
||||||
|
recoverMissedExecutions: false,
|
||||||
|
timezone: 'Europe/Amsterdam',
|
||||||
|
})
|
||||||
|
|
||||||
|
export const startTasks = () => {
|
||||||
|
try {
|
||||||
|
const task = schedule(
|
||||||
|
'0 * * * * *',
|
||||||
|
async (timestamp) => {
|
||||||
|
Logger.info('Running cron job', { timestamp })
|
||||||
|
try {
|
||||||
|
await reserve()
|
||||||
|
Logger.info('Completed running cron job')
|
||||||
|
} catch (error: any) {
|
||||||
|
Logger.error('Error running cron job', { error: error.message })
|
||||||
|
}
|
||||||
|
},
|
||||||
|
getTaskConfig('reserver cron')
|
||||||
|
)
|
||||||
|
Logger.debug('Started cron task')
|
||||||
|
tasks.push(task)
|
||||||
|
} catch (error: any) {
|
||||||
|
Logger.error('Failed to start tasks', { error: error.message })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const stopTasks = () => {
|
||||||
|
try {
|
||||||
|
tasks.map((task) => task.stop())
|
||||||
|
Logger.debug('Stopped cron tasks')
|
||||||
|
} catch (error: any) {
|
||||||
|
Logger.error('Failed to stop tasks', { error: error.message })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import http from 'http'
|
import http from 'http'
|
||||||
import { v4 } from 'uuid'
|
import { v4 } from 'uuid'
|
||||||
import { Logger, LogLevel } from '../common/logger'
|
import { Logger, LogLevel } from '../common/logger'
|
||||||
import { work as schedule } from '../common/scheduler'
|
import { schedule } from '../common/scheduler'
|
||||||
import { parseJson } from './utils'
|
import { parseJson } from './utils'
|
||||||
|
|
||||||
// Handles POST requests to /reservations
|
// Handles POST requests to /reservations
|
||||||
|
|
@ -2,4 +2,4 @@ describe('failure', () => {
|
||||||
test('should fail', () => {
|
test('should fail', () => {
|
||||||
expect(true).toBeFalsy()
|
expect(true).toBeFalsy()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -78,7 +78,7 @@ describe('Logger', () => {
|
||||||
'abc',
|
'abc',
|
||||||
'INFO',
|
'INFO',
|
||||||
'first',
|
'first',
|
||||||
{ password: '***'},
|
{ password: '***' }
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,18 @@
|
||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
import { ValidationError, ValidationErrorCode } from '../../../src/common/request'
|
import {
|
||||||
|
ValidationError,
|
||||||
|
ValidationErrorCode,
|
||||||
|
} from '../../../src/common/request'
|
||||||
import { Reservation } from '../../../src/common/reservation'
|
import { Reservation } from '../../../src/common/reservation'
|
||||||
import { work, SchedulerInput } from '../../../src/common/scheduler'
|
import { schedule, SchedulerInput } from '../../../src/common/scheduler'
|
||||||
|
|
||||||
jest.mock('../../../src/common/logger')
|
jest.mock('../../../src/common/logger')
|
||||||
|
jest.mock('../../../src/common/reserver')
|
||||||
jest.useFakeTimers().setSystemTime(new Date('2022-01-01'))
|
jest.useFakeTimers().setSystemTime(new Date('2022-01-01'))
|
||||||
|
|
||||||
describe('scheduler', () => {
|
describe('scheduler', () => {
|
||||||
test('should handle valid requests within reservation window', async () => {
|
test('should handle valid requests within reservation window', async () => {
|
||||||
|
jest.spyOn(Reservation, 'save').mockResolvedValueOnce()
|
||||||
const start = dayjs().add(15, 'minutes')
|
const start = dayjs().add(15, 'minutes')
|
||||||
const end = start.add(15, 'minutes')
|
const end = start.add(15, 'minutes')
|
||||||
|
|
||||||
|
|
@ -18,7 +23,7 @@ describe('scheduler', () => {
|
||||||
opponent: { id: '123', name: 'collin' },
|
opponent: { id: '123', name: 'collin' },
|
||||||
}
|
}
|
||||||
|
|
||||||
expect(await work(payload)).toMatchSnapshot({
|
expect(await schedule(payload)).toMatchSnapshot({
|
||||||
scheduledReservation: {
|
scheduledReservation: {
|
||||||
reservation: {
|
reservation: {
|
||||||
user: {
|
user: {
|
||||||
|
|
@ -42,7 +47,7 @@ describe('scheduler', () => {
|
||||||
opponent: { id: '123', name: 'collin' },
|
opponent: { id: '123', name: 'collin' },
|
||||||
}
|
}
|
||||||
|
|
||||||
await expect(await work(payload)).toMatchSnapshot({
|
await expect(await schedule(payload)).toMatchSnapshot({
|
||||||
scheduledReservation: {
|
scheduledReservation: {
|
||||||
reservation: new Reservation(
|
reservation: new Reservation(
|
||||||
{ username: 'collin', password: expect.any(String) },
|
{ username: 'collin', password: expect.any(String) },
|
||||||
|
|
@ -69,7 +74,7 @@ describe('scheduler', () => {
|
||||||
opponent: { id: '123', name: 'collin' },
|
opponent: { id: '123', name: 'collin' },
|
||||||
}
|
}
|
||||||
|
|
||||||
await expect(work(payload)).rejects.toThrowError(
|
await expect(schedule(payload)).rejects.toThrowError(
|
||||||
new ValidationError(
|
new ValidationError(
|
||||||
'Invalid request',
|
'Invalid request',
|
||||||
ValidationErrorCode.INVALID_REQUEST_BODY
|
ValidationErrorCode.INVALID_REQUEST_BODY
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
import server from '../../../src/server/index'
|
import server from '../../../src/server/http'
|
||||||
import * as scheduler from '../../../src/common/scheduler'
|
import * as scheduler from '../../../src/common/scheduler'
|
||||||
import * as utils from '../../../src/server/utils'
|
import * as utils from '../../../src/server/utils'
|
||||||
|
|
||||||
|
|
@ -26,7 +26,7 @@ describe('server', () => {
|
||||||
|
|
||||||
test('should accept POST to /reservations', async () => {
|
test('should accept POST to /reservations', async () => {
|
||||||
jest
|
jest
|
||||||
.spyOn(scheduler, 'work')
|
.spyOn(scheduler, 'schedule')
|
||||||
.mockImplementationOnce(() => Promise.resolve({}))
|
.mockImplementationOnce(() => Promise.resolve({}))
|
||||||
const response = await axios.post(
|
const response = await axios.post(
|
||||||
`${baseUrl}/reservations`,
|
`${baseUrl}/reservations`,
|
||||||
|
|
@ -38,7 +38,7 @@ describe('server', () => {
|
||||||
|
|
||||||
test('should reject non-POST request', async () => {
|
test('should reject non-POST request', async () => {
|
||||||
jest
|
jest
|
||||||
.spyOn(scheduler, 'work')
|
.spyOn(scheduler, 'schedule')
|
||||||
.mockImplementationOnce(() => Promise.resolve({}))
|
.mockImplementationOnce(() => Promise.resolve({}))
|
||||||
await expect(() => axios.get(`${baseUrl}/reservations`)).rejects.toThrow(
|
await expect(() => axios.get(`${baseUrl}/reservations`)).rejects.toThrow(
|
||||||
axios.AxiosError
|
axios.AxiosError
|
||||||
|
|
@ -47,7 +47,7 @@ describe('server', () => {
|
||||||
|
|
||||||
test('should reject request to other route', async () => {
|
test('should reject request to other route', async () => {
|
||||||
jest
|
jest
|
||||||
.spyOn(scheduler, 'work')
|
.spyOn(scheduler, 'schedule')
|
||||||
.mockImplementationOnce(() => Promise.resolve({}))
|
.mockImplementationOnce(() => Promise.resolve({}))
|
||||||
await expect(() => axios.post(`${baseUrl}/something-else`)).rejects.toThrow(
|
await expect(() => axios.post(`${baseUrl}/something-else`)).rejects.toThrow(
|
||||||
axios.AxiosError
|
axios.AxiosError
|
||||||
|
|
@ -56,7 +56,7 @@ describe('server', () => {
|
||||||
|
|
||||||
test('should reject request without content-type of json', async () => {
|
test('should reject request without content-type of json', async () => {
|
||||||
jest
|
jest
|
||||||
.spyOn(scheduler, 'work')
|
.spyOn(scheduler, 'schedule')
|
||||||
.mockImplementationOnce(() => Promise.resolve({}))
|
.mockImplementationOnce(() => Promise.resolve({}))
|
||||||
await expect(() =>
|
await expect(() =>
|
||||||
axios.post(`${baseUrl}/reservations`, 'test,123', {
|
axios.post(`${baseUrl}/reservations`, 'test,123', {
|
||||||
|
|
@ -79,7 +79,7 @@ describe('server', () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
test('should reject request if schedule cannot be performed', async () => {
|
test('should reject request if schedule cannot be performed', async () => {
|
||||||
jest.spyOn(scheduler, 'work').mockImplementationOnce(Promise.reject)
|
jest.spyOn(scheduler, 'schedule').mockImplementationOnce(Promise.reject)
|
||||||
await expect(() =>
|
await expect(() =>
|
||||||
axios.post(
|
axios.post(
|
||||||
`${baseUrl}/reservations`,
|
`${baseUrl}/reservations`,
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue