Adding some ✨ pretty ✨ stuff
Reorganized code and added some unit tests for reservations and requests
This commit is contained in:
parent
eec0edb76b
commit
743cc08887
16 changed files with 7977 additions and 262 deletions
|
|
@ -7,5 +7,9 @@
|
||||||
"extends": [
|
"extends": [
|
||||||
"eslint:recommended",
|
"eslint:recommended",
|
||||||
"plugin:@typescript-eslint/recommended"
|
"plugin:@typescript-eslint/recommended"
|
||||||
|
],
|
||||||
|
"ignorePatterns": [
|
||||||
|
"**/*.js",
|
||||||
|
"!src/**/*.ts"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
@ -1,3 +1,6 @@
|
||||||
{
|
{
|
||||||
"presets": ["@babel/preset-typescript"]
|
"presets": [
|
||||||
|
["@babel/preset-env", { "targets": { "node": "current" } }],
|
||||||
|
"@babel/preset-typescript"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
29
jest.config.js
Normal file
29
jest.config.js
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
/*
|
||||||
|
* For a detailed explanation regarding each configuration property, visit:
|
||||||
|
* https://jestjs.io/docs/configuration
|
||||||
|
*/
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
clearMocks: true,
|
||||||
|
collectCoverage: true,
|
||||||
|
coverageReporters: [
|
||||||
|
"text"
|
||||||
|
],
|
||||||
|
coveragePathIgnorePatterns: [
|
||||||
|
"/node_modules/"
|
||||||
|
],
|
||||||
|
coverageProvider: "v8",
|
||||||
|
moduleDirectories: [
|
||||||
|
"node_modules"
|
||||||
|
],
|
||||||
|
moduleFileExtensions: [
|
||||||
|
"js",
|
||||||
|
"ts"
|
||||||
|
],
|
||||||
|
roots: [
|
||||||
|
"<rootDir>/tests"
|
||||||
|
],
|
||||||
|
testMatch: [
|
||||||
|
"**/*.test.ts"
|
||||||
|
]
|
||||||
|
};
|
||||||
7683
package-lock.json
generated
7683
package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -7,6 +7,7 @@
|
||||||
"node": "14.x"
|
"node": "14.x"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
"test": "jest",
|
||||||
"lint": "eslint src/ --ext ts",
|
"lint": "eslint src/ --ext ts",
|
||||||
"prettier": "prettier src -w",
|
"prettier": "prettier src -w",
|
||||||
"build": "tsc",
|
"build": "tsc",
|
||||||
|
|
@ -16,18 +17,22 @@
|
||||||
"author": "Collin Duncan <cgduncan7@gmail.com>",
|
"author": "Collin Duncan <cgduncan7@gmail.com>",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"dayjs": "^1.10.7",
|
||||||
"puppeteer": "^11.0.0"
|
"puppeteer": "^11.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "^7.16.0",
|
"@babel/core": "^7.16.0",
|
||||||
"@babel/preset-env": "^7.16.0",
|
"@babel/preset-env": "^7.16.4",
|
||||||
"@babel/preset-typescript": "^7.16.0",
|
"@babel/preset-typescript": "^7.16.0",
|
||||||
"@rollup/plugin-babel": "^5.3.0",
|
"@rollup/plugin-babel": "^5.3.0",
|
||||||
"@types/aws-lambda": "^8.10.85",
|
"@types/aws-lambda": "^8.10.85",
|
||||||
|
"@types/jest": "^27.0.2",
|
||||||
"@types/puppeteer": "^5.4.4",
|
"@types/puppeteer": "^5.4.4",
|
||||||
"@typescript-eslint/eslint-plugin": "^5.4.0",
|
"@typescript-eslint/eslint-plugin": "^5.4.0",
|
||||||
"@typescript-eslint/parser": "^5.4.0",
|
"@typescript-eslint/parser": "^5.4.0",
|
||||||
|
"babel-jest": "^27.3.1",
|
||||||
"eslint": "^8.2.0",
|
"eslint": "^8.2.0",
|
||||||
|
"jest": "^27.3.1",
|
||||||
"prettier": "^2.4.1",
|
"prettier": "^2.4.1",
|
||||||
"typescript": "^4.4.4"
|
"typescript": "^4.4.4"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
116
src/common/request.ts
Normal file
116
src/common/request.ts
Normal file
|
|
@ -0,0 +1,116 @@
|
||||||
|
import dayjs, { Dayjs } from 'dayjs'
|
||||||
|
import isSameOrBefore from 'dayjs/plugin/isSameOrBefore'
|
||||||
|
dayjs.extend(isSameOrBefore)
|
||||||
|
|
||||||
|
import { DateRange, Opponent } from './reservation'
|
||||||
|
|
||||||
|
export interface ReservationRequest {
|
||||||
|
username: string
|
||||||
|
password: string
|
||||||
|
dateRanges: {
|
||||||
|
start: Dayjs
|
||||||
|
end: Dayjs
|
||||||
|
}[]
|
||||||
|
opponent: Opponent
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum ValidationErrorCode {
|
||||||
|
UNDEFINED_REQUEST_BODY = 1,
|
||||||
|
INVALID_REQUEST_BODY = 2,
|
||||||
|
INVALID_DATE_RANGE = 3,
|
||||||
|
INVALID_START_OR_END_DATE = 4,
|
||||||
|
INVALID_OPPONENT = 5,
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ValidationError extends Error {
|
||||||
|
public readonly code: ValidationErrorCode
|
||||||
|
|
||||||
|
constructor(message: string, code: ValidationErrorCode) {
|
||||||
|
super(message)
|
||||||
|
this.code = code
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const validateRequest = (
|
||||||
|
body: string
|
||||||
|
): ReservationRequest => {
|
||||||
|
const request = validateRequestBody(body)
|
||||||
|
validateRequestDateRanges(request.dateRanges)
|
||||||
|
validateRequestOpponent(request.opponent)
|
||||||
|
return request
|
||||||
|
}
|
||||||
|
|
||||||
|
const validateRequestBody = (body?: string): ReservationRequest => {
|
||||||
|
if (body === undefined) {
|
||||||
|
throw new ValidationError('Invalid request', ValidationErrorCode.UNDEFINED_REQUEST_BODY)
|
||||||
|
}
|
||||||
|
|
||||||
|
const jsonBody = transformRequestBody(body)
|
||||||
|
const { username, password, opponent, dateRanges } = jsonBody
|
||||||
|
|
||||||
|
if (
|
||||||
|
!username ||
|
||||||
|
username.length < 1 ||
|
||||||
|
!password ||
|
||||||
|
password.length < 1 ||
|
||||||
|
!dateRanges ||
|
||||||
|
dateRanges.length < 1 ||
|
||||||
|
(opponent && opponent.id && opponent.id.length < 1) ||
|
||||||
|
(opponent && opponent.name && opponent.name.length < 1)
|
||||||
|
) {
|
||||||
|
throw new ValidationError('Invalid request', ValidationErrorCode.INVALID_REQUEST_BODY)
|
||||||
|
}
|
||||||
|
|
||||||
|
return jsonBody
|
||||||
|
}
|
||||||
|
|
||||||
|
const transformRequestBody = (body: string): ReservationRequest => {
|
||||||
|
const json = JSON.parse(body)
|
||||||
|
const dateRanges: DateRange[] = json.dateRanges?.map(
|
||||||
|
({ start, end }: { start: string; end: string }): DateRange => {
|
||||||
|
return { start: dayjs(start), end: dayjs(end) }
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return {
|
||||||
|
username: json.username,
|
||||||
|
password: json.password,
|
||||||
|
opponent: json.opponent,
|
||||||
|
dateRanges,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const validateRequestDateRanges = (dateRanges: DateRange[]): void => {
|
||||||
|
for (let i = 0; i < dateRanges.length; i++) {
|
||||||
|
// checking that both dates are valid
|
||||||
|
const { start, end } = dateRanges[i]
|
||||||
|
if (!start.isValid() || !end.isValid()) {
|
||||||
|
throw new ValidationError('Invalid request', ValidationErrorCode.INVALID_DATE_RANGE)
|
||||||
|
}
|
||||||
|
|
||||||
|
// checking that:
|
||||||
|
// 1. start occurs after now
|
||||||
|
// 2. start occurs before or same as end
|
||||||
|
// 3. start and end fall on same YYYY/MM/DD
|
||||||
|
if (
|
||||||
|
!start.isAfter(dayjs()) ||
|
||||||
|
!start.isSameOrBefore(end) ||
|
||||||
|
start.format('YYYY MM DD') !== end.format('YYYY MM DD')
|
||||||
|
) {
|
||||||
|
throw new ValidationError('Invalid request', ValidationErrorCode.INVALID_START_OR_END_DATE)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const validateRequestOpponent = (opponent?: Opponent): void => {
|
||||||
|
if (!opponent) return
|
||||||
|
|
||||||
|
const { id, name } = opponent
|
||||||
|
if (
|
||||||
|
typeof id !== 'string' ||
|
||||||
|
typeof name !== 'string' ||
|
||||||
|
id.length < 1 ||
|
||||||
|
name.length < 1
|
||||||
|
) {
|
||||||
|
throw new ValidationError('Invalid request', ValidationErrorCode.INVALID_OPPONENT)
|
||||||
|
}
|
||||||
|
}
|
||||||
40
src/common/reservation.ts
Normal file
40
src/common/reservation.ts
Normal file
|
|
@ -0,0 +1,40 @@
|
||||||
|
import dayjs, { Dayjs } from 'dayjs'
|
||||||
|
import isSameOrBefore from 'dayjs/plugin/isSameOrBefore'
|
||||||
|
dayjs.extend(isSameOrBefore)
|
||||||
|
|
||||||
|
export interface Opponent {
|
||||||
|
id: string
|
||||||
|
name: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DateRange {
|
||||||
|
start: dayjs.Dayjs
|
||||||
|
end: dayjs.Dayjs
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Reservation {
|
||||||
|
public readonly dateRange: DateRange
|
||||||
|
public readonly opponent: Opponent
|
||||||
|
public readonly possibleDates: Dayjs[]
|
||||||
|
public booked = false
|
||||||
|
|
||||||
|
constructor(dateRange: DateRange, opponent: Opponent) {
|
||||||
|
this.dateRange = dateRange
|
||||||
|
this.opponent = opponent
|
||||||
|
this.possibleDates = this.createPossibleDates()
|
||||||
|
}
|
||||||
|
|
||||||
|
private createPossibleDates(): Dayjs[] {
|
||||||
|
const possibleDates: Dayjs[] = []
|
||||||
|
|
||||||
|
const { start, end } = this.dateRange
|
||||||
|
|
||||||
|
let possibleDate = dayjs(start)
|
||||||
|
while (possibleDate.isSameOrBefore(end)) {
|
||||||
|
possibleDates.push(possibleDate)
|
||||||
|
possibleDate = possibleDate.add(15, 'minute')
|
||||||
|
}
|
||||||
|
|
||||||
|
return possibleDates
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { Dayjs } from 'dayjs'
|
||||||
import puppeteer, {
|
import puppeteer, {
|
||||||
Browser,
|
Browser,
|
||||||
BrowserConnectOptions,
|
BrowserConnectOptions,
|
||||||
|
|
@ -6,7 +7,8 @@ import puppeteer, {
|
||||||
LaunchOptions,
|
LaunchOptions,
|
||||||
Page,
|
Page,
|
||||||
} from 'puppeteer'
|
} from 'puppeteer'
|
||||||
import { DateTime, Opponent, Reservation, timeToString } from './reservation'
|
|
||||||
|
import { Opponent, Reservation } from './reservation'
|
||||||
|
|
||||||
export class Runner {
|
export class Runner {
|
||||||
private readonly username: string
|
private readonly username: string
|
||||||
|
|
@ -58,7 +60,7 @@ export class Runner {
|
||||||
|
|
||||||
private async makeReservation(reservation: Reservation): Promise<void> {
|
private async makeReservation(reservation: Reservation): Promise<void> {
|
||||||
try {
|
try {
|
||||||
await this.navigateToDay(reservation.dateTime)
|
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()
|
||||||
|
|
@ -68,21 +70,25 @@ export class Runner {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async navigateToDay(dt: DateTime): Promise<void> {
|
private async navigateToDay(date: Dayjs): Promise<void> {
|
||||||
await this.page
|
await this.page
|
||||||
?.waitForSelector(`td#cal_${dt.year}_${dt.month}_${dt.day}`)
|
?.waitForSelector(
|
||||||
|
`td#cal_${date.get('year')}_${date.get('month') + 1}_${date.get('day')}`
|
||||||
|
)
|
||||||
.then((d) => d?.click())
|
.then((d) => d?.click())
|
||||||
await this.page?.waitForSelector(
|
await this.page?.waitForSelector(
|
||||||
`td#cal_${dt.year}_${dt.month}_${dt.day}.selected`
|
`td#cal_${date.get('year')}_${date.get('month') + 1}_${date.get(
|
||||||
|
'day'
|
||||||
|
)}.selected`
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private async selectAvailableTime(res: Reservation): Promise<void> {
|
private async selectAvailableTime(res: Reservation): Promise<void> {
|
||||||
let freeCourt: ElementHandle | null | undefined
|
let freeCourt: ElementHandle | null | undefined
|
||||||
let i = 0
|
let i = 0
|
||||||
while (i < res.possibleTimes.length && !freeCourt) {
|
while (i < res.possibleDates.length && !freeCourt) {
|
||||||
const possibleTime = res.possibleTimes[i]
|
const possibleDate = res.possibleDates[i]
|
||||||
const timeString = timeToString(possibleTime)
|
const timeString = possibleDate.format('HH:mm')
|
||||||
const selector =
|
const selector =
|
||||||
`tr[data-time='${timeString}']` + `> td.free[rowspan='3'][type='free']`
|
`tr[data-time='${timeString}']` + `> td.free[rowspan='3'][type='free']`
|
||||||
freeCourt = await this.page?.$(selector)
|
freeCourt = await this.page?.$(selector)
|
||||||
18
src/lambda/reservationHandler.ts
Normal file
18
src/lambda/reservationHandler.ts
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
import { SQSEvent, SQSHandler } from 'aws-lambda'
|
||||||
|
import { validateRequest } from '../common/request'
|
||||||
|
|
||||||
|
import { Reservation } from '../common/reservation'
|
||||||
|
import { Runner } from '../common/runner'
|
||||||
|
|
||||||
|
export const run: SQSHandler = async (event: SQSEvent): Promise<void> => {
|
||||||
|
const { request, error } = validateRequest(event.Records[0].body)
|
||||||
|
if (error || !request) {
|
||||||
|
throw new Error(error?.message)
|
||||||
|
}
|
||||||
|
|
||||||
|
const { username, password, dateRanges, opponent } = request
|
||||||
|
const reservations = dateRanges.map((dr) => new Reservation(dr, opponent))
|
||||||
|
|
||||||
|
const runner = new Runner(username, password, reservations)
|
||||||
|
await runner.run({ headless: false })
|
||||||
|
}
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
import { SQSEvent, SQSHandler } from 'aws-lambda'
|
import { SQSEvent, SQSHandler } from 'aws-lambda'
|
||||||
import { validateRequestEvent } from './request'
|
import { validateRequestEvent } from '../common/request'
|
||||||
|
|
||||||
import { Reservation } from './reservation'
|
import { Reservation } from '../common/reservation'
|
||||||
import { Runner } from './runner'
|
import { Runner } from '../common/runner'
|
||||||
|
|
||||||
export const run: SQSHandler = async (event: SQSEvent): Promise<void> => {
|
export const run: SQSHandler = async (event: SQSEvent): Promise<void> => {
|
||||||
const { request, error } = validateRequestEvent(event)
|
const { request, error } = validateRequestEvent(event)
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { IncomingRequest } from './request'
|
import { IncomingRequest } from './common/request'
|
||||||
import { Reservation } from './reservation'
|
import { Reservation } from './common/reservation'
|
||||||
import { Runner } from './runner'
|
import { Runner } from './common/runner'
|
||||||
|
|
||||||
const run = async (request: IncomingRequest) => {
|
const run = async (request: IncomingRequest) => {
|
||||||
const { username, password, dateTimes, opponent } = request
|
const { username, password, dateTimes, opponent } = request
|
||||||
|
|
|
||||||
114
src/request.ts
114
src/request.ts
|
|
@ -1,114 +0,0 @@
|
||||||
import { SQSEvent } from 'aws-lambda'
|
|
||||||
import { DateTime, Opponent } from './reservation'
|
|
||||||
|
|
||||||
export interface IncomingRequest {
|
|
||||||
username: string
|
|
||||||
password: string
|
|
||||||
dateTimes: DateTime[]
|
|
||||||
opponent: Opponent
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ValidationError {
|
|
||||||
message: string
|
|
||||||
code: number
|
|
||||||
}
|
|
||||||
|
|
||||||
export const validateRequestEvent = (
|
|
||||||
event: SQSEvent
|
|
||||||
): { request?: IncomingRequest; error?: ValidationError } => {
|
|
||||||
try {
|
|
||||||
const request = validateRequestBody(event.Records[0].body)
|
|
||||||
validateRequestDateTimes(request.dateTimes)
|
|
||||||
validateRequestOpponent(request.opponent)
|
|
||||||
return { request }
|
|
||||||
} catch (err: unknown) {
|
|
||||||
return { error: { message: 'Invalid request', code: (err as ValidationError).code ?? 0 } }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const validateRequestBody = (body?: string): IncomingRequest => {
|
|
||||||
if (body === undefined) {
|
|
||||||
throw {
|
|
||||||
message: 'Invalid request',
|
|
||||||
code: 1,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let jsonBody: IncomingRequest
|
|
||||||
try {
|
|
||||||
jsonBody = JSON.parse(body)
|
|
||||||
} catch (err) {
|
|
||||||
throw {
|
|
||||||
message: 'Invalid request',
|
|
||||||
code: 2,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const { username, password, dateTimes } = jsonBody
|
|
||||||
if (
|
|
||||||
!username ||
|
|
||||||
username.length < 1 ||
|
|
||||||
!password ||
|
|
||||||
password.length < 1 ||
|
|
||||||
!dateTimes
|
|
||||||
) {
|
|
||||||
throw {
|
|
||||||
message: 'Invalid request',
|
|
||||||
code: 3,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return jsonBody
|
|
||||||
}
|
|
||||||
|
|
||||||
const validateRequestDateTimes = (dateTimes: DateTime[]): void => {
|
|
||||||
const now = new Date()
|
|
||||||
for (let i = 0; i < dateTimes.length; i++) {
|
|
||||||
const dateTime = dateTimes[i]
|
|
||||||
const { year, month, day, timeRange } = dateTime
|
|
||||||
const { start, end } = timeRange
|
|
||||||
|
|
||||||
if (
|
|
||||||
typeof year !== 'number' ||
|
|
||||||
typeof month !== 'number' ||
|
|
||||||
typeof day !== 'number' ||
|
|
||||||
typeof start.hour !== 'number' ||
|
|
||||||
typeof start.minute !== 'number' ||
|
|
||||||
typeof end.hour !== 'number' ||
|
|
||||||
typeof end.minute !== 'number'
|
|
||||||
) {
|
|
||||||
throw {
|
|
||||||
message: 'Invalid request',
|
|
||||||
code: 4,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const date = new Date()
|
|
||||||
date.setFullYear(year, month - 1, day)
|
|
||||||
date.setHours(start.hour, start.minute)
|
|
||||||
|
|
||||||
if (now.getTime() >= date.getTime()) {
|
|
||||||
throw {
|
|
||||||
message: 'Invalid request',
|
|
||||||
code: 5,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const validateRequestOpponent = (opponent?: Opponent): void => {
|
|
||||||
if (!opponent) return
|
|
||||||
|
|
||||||
const { id, name } = opponent
|
|
||||||
if (
|
|
||||||
typeof id !== 'string' ||
|
|
||||||
typeof name !== 'string' ||
|
|
||||||
id.length < 1 ||
|
|
||||||
name.length < 1
|
|
||||||
) {
|
|
||||||
throw {
|
|
||||||
message: 'Invalid request',
|
|
||||||
code: 6,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,54 +0,0 @@
|
||||||
export interface Time {
|
|
||||||
hour: number
|
|
||||||
minute: number
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Opponent {
|
|
||||||
id: string
|
|
||||||
name: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface DateTime {
|
|
||||||
year: number
|
|
||||||
month: number
|
|
||||||
day: number
|
|
||||||
timeRange: {
|
|
||||||
start: Time
|
|
||||||
end: Time
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const timeToString = ({ hour, minute }: Time): string =>
|
|
||||||
`${`${hour}`.padStart(2, '0')}:${`${minute}`.padStart(2, '0')}`
|
|
||||||
|
|
||||||
export class Reservation {
|
|
||||||
public readonly dateTime: DateTime
|
|
||||||
public readonly opponent: Opponent
|
|
||||||
public readonly possibleTimes: Time[]
|
|
||||||
public booked = false
|
|
||||||
|
|
||||||
constructor(dateTime: DateTime, opponent: Opponent) {
|
|
||||||
this.dateTime = dateTime
|
|
||||||
this.opponent = opponent
|
|
||||||
this.possibleTimes = this.createPossibleTimes()
|
|
||||||
}
|
|
||||||
|
|
||||||
private createPossibleTimes() {
|
|
||||||
const possibleTimes: Time[] = []
|
|
||||||
|
|
||||||
const { start, end } = this.dateTime.timeRange
|
|
||||||
|
|
||||||
let { hour, minute } = start
|
|
||||||
const { hour: endHour, minute: endMinute } = end
|
|
||||||
|
|
||||||
while (hour <= endHour && minute <= endMinute) {
|
|
||||||
possibleTimes.push({ hour, minute })
|
|
||||||
minute = (minute + 15) % 60
|
|
||||||
if (minute === 0) {
|
|
||||||
hour++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return possibleTimes
|
|
||||||
}
|
|
||||||
}
|
|
||||||
112
tests/common/request.test.ts
Normal file
112
tests/common/request.test.ts
Normal file
|
|
@ -0,0 +1,112 @@
|
||||||
|
import dayjs from 'dayjs'
|
||||||
|
|
||||||
|
import {
|
||||||
|
validateRequest,
|
||||||
|
ValidationError,
|
||||||
|
ValidationErrorCode,
|
||||||
|
} from '../../src/common/request'
|
||||||
|
|
||||||
|
describe('request', () => {
|
||||||
|
describe('validateRequest', () => {
|
||||||
|
test('should return ReservationRequest', () => {
|
||||||
|
const body = JSON.stringify({
|
||||||
|
username: 'collin',
|
||||||
|
password: '123abc',
|
||||||
|
dateRanges: [
|
||||||
|
{ start: '2021-12-25T12:34:56Z', end: '2021-12-25T12:45:56Z' }
|
||||||
|
],
|
||||||
|
opponent: {
|
||||||
|
id: '123',
|
||||||
|
name: 'collin',
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(() => validateRequest(body)).not.toThrow()
|
||||||
|
})
|
||||||
|
|
||||||
|
test('should fail for undefined body', () => {
|
||||||
|
expect(() => validateRequest(undefined)).toThrowError(new ValidationError('Invalid request', ValidationErrorCode.UNDEFINED_REQUEST_BODY))
|
||||||
|
})
|
||||||
|
|
||||||
|
test.each([
|
||||||
|
{ username: '', password: '1qaz2wsx', dateRanges: [{ start: '1', end: '1' }], opponent: { id: '123', name: 'abc' } },
|
||||||
|
{ password: '1qaz2wsx', dateRanges: [{ start: '1', end: '1' }], opponent: { id: '123', name: 'abc' } },
|
||||||
|
{ username: 'collin', password: '', dateRanges: [{ start: '1', end: '1' }], opponent: { id: '123', name: 'abc' } },
|
||||||
|
{ username: 'collin', dateRanges: [{ start: '1', end: '1' }], opponent: { id: '123', name: 'abc' } },
|
||||||
|
{ username: 'collin', password: '1qaz2wsx', dateRanges: [], opponent: { id: '123', name: 'abc' } },
|
||||||
|
{ username: 'collin', password: '1qaz2wsx', opponent: { id: '123', name: 'abc' } },
|
||||||
|
{ username: 'collin', password: '1qaz2wsx', dateRanges: [{ start: '1', end: '1' }], opponent: { id: '', name: 'abc' } },
|
||||||
|
{ username: 'collin', password: '1qaz2wsx', dateRanges: [{ start: '1', end: '1' }], opponent: { name: 'abc' } },
|
||||||
|
{ username: 'collin', password: '1qaz2wsx', dateRanges: [{ start: '1', end: '1' }], opponent: { id: '123', name: '' } },
|
||||||
|
{ username: 'collin', password: '1qaz2wsx', dateRanges: [{ start: '1', end: '1' }], opponent: { id: '123' } },
|
||||||
|
])('should fail for body missing required values', (body) => {
|
||||||
|
expect(() => validateRequest(JSON.stringify(body))).toThrowError(new ValidationError('Invalid request', ValidationErrorCode.INVALID_REQUEST_BODY))
|
||||||
|
})
|
||||||
|
|
||||||
|
test('should fail for invalid date range', () => {
|
||||||
|
const body = JSON.stringify({
|
||||||
|
username: 'collin',
|
||||||
|
password: '123abc',
|
||||||
|
dateRanges: [
|
||||||
|
{ start: 'monkey', end: '2021-12-25T12:45:56Z' }
|
||||||
|
],
|
||||||
|
opponent: {
|
||||||
|
id: '123',
|
||||||
|
name: 'collin',
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(() => validateRequest(body)).toThrowError(new ValidationError('Invalid request', ValidationErrorCode.INVALID_DATE_RANGE))
|
||||||
|
})
|
||||||
|
|
||||||
|
test.each([
|
||||||
|
{ start: dayjs().subtract(1, 'hour').toString(), end: dayjs().add(1, 'hour').toString() },
|
||||||
|
{ start: dayjs().add(2, 'hour').toString(), end: dayjs().add(1, 'hour').toString() },
|
||||||
|
{ start: dayjs().toString(), end: dayjs().add(1, 'day').toString() }
|
||||||
|
])('should fail for improper start or end dates', (dateRange) => {
|
||||||
|
const body = JSON.stringify({
|
||||||
|
username: 'collin',
|
||||||
|
password: '123abc',
|
||||||
|
dateRanges: [
|
||||||
|
dateRange
|
||||||
|
],
|
||||||
|
opponent: {
|
||||||
|
id: '123',
|
||||||
|
name: 'collin',
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(() => validateRequest(body)).toThrowError(new ValidationError('Invalid request', ValidationErrorCode.INVALID_START_OR_END_DATE))
|
||||||
|
})
|
||||||
|
|
||||||
|
test('should not fail if no opponent is provided', () => {
|
||||||
|
const body = JSON.stringify({
|
||||||
|
username: 'collin',
|
||||||
|
password: '123abc',
|
||||||
|
dateRanges: [
|
||||||
|
{ start: '2021-12-25T12:34:56Z', end: '2021-12-25T12:45:56Z' }
|
||||||
|
],
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(() => validateRequest(body)).not.toThrow()
|
||||||
|
})
|
||||||
|
|
||||||
|
test.each([
|
||||||
|
{ id: 123, name: 'collin' },
|
||||||
|
{ id: '', name: 'collin' },
|
||||||
|
{ id: '123', name: true },
|
||||||
|
{ id: '123', name: '' },
|
||||||
|
])('should fail for invalid opponent id', (opponent) => {
|
||||||
|
const body = JSON.stringify({
|
||||||
|
username: 'collin',
|
||||||
|
password: '123abc',
|
||||||
|
dateRanges: [
|
||||||
|
{ start: '2021-12-25T12:34:56Z', end: '2021-12-25T12:45:56Z' }
|
||||||
|
],
|
||||||
|
opponent,
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(() => validateRequest(body)).toThrowError(new ValidationError('Invalid request', ValidationErrorCode.INVALID_OPPONENT))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
22
tests/common/reservation.test.ts
Normal file
22
tests/common/reservation.test.ts
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
import dayjs from 'dayjs'
|
||||||
|
import { DateRange, Reservation } from '../../src/common/reservation'
|
||||||
|
|
||||||
|
describe("Reservation", () => {
|
||||||
|
it("will create correct possible dates", () => {
|
||||||
|
const startDate = dayjs().set("hour", 12).set("minute", 0)
|
||||||
|
const endDate = dayjs().set("hour", 13).set("minute", 0)
|
||||||
|
const dateRange: DateRange = {
|
||||||
|
start: startDate,
|
||||||
|
end: endDate,
|
||||||
|
}
|
||||||
|
const res = new Reservation(dateRange, { id: 'collin', name: 'collin' })
|
||||||
|
|
||||||
|
expect(res.possibleDates).toHaveLength(5)
|
||||||
|
|
||||||
|
expect(res.possibleDates[0]).toEqual(startDate)
|
||||||
|
expect(res.possibleDates[1]).toEqual(startDate.add(15, "minute"))
|
||||||
|
expect(res.possibleDates[2]).toEqual(startDate.add(30, "minute"))
|
||||||
|
expect(res.possibleDates[3]).toEqual(startDate.add(45, "minute"))
|
||||||
|
expect(res.possibleDates[4]).toEqual(startDate.add(60, "minute"))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
@ -8,6 +8,7 @@
|
||||||
|
|
||||||
"strict": true,
|
"strict": true,
|
||||||
"esModuleInterop": true,
|
"esModuleInterop": true,
|
||||||
|
"allowSyntheticDefaultImports": true,
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
"forceConsistentCasingInFileNames": true,
|
"forceConsistentCasingInFileNames": true,
|
||||||
"outDir": "dist/"
|
"outDir": "dist/"
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue