diff --git a/src/common/request.ts b/src/common/request.ts index 8a830c2..d317645 100644 --- a/src/common/request.ts +++ b/src/common/request.ts @@ -7,19 +7,20 @@ import { DateRange, Opponent } from './reservation' export interface ReservationRequest { username: string password: string - dateRanges: { + dateRange: { 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, + UNDEFINED_REQUEST_BODY, + INVALID_JSON, + INVALID_REQUEST_BODY, + INVALID_DATE_RANGE, + INVALID_START_OR_END_DATE, + INVALID_OPPONENT, } export class ValidationError extends Error { @@ -31,11 +32,16 @@ export class ValidationError extends Error { } } +/** + * Validates an incoming request body and converts to ReservationRequest + * @param body String of request body + * @returns ReservationRequest + */ export const validateRequest = ( body: string ): ReservationRequest => { const request = validateRequestBody(body) - validateRequestDateRanges(request.dateRanges) + validateRequestDateRange(request.dateRange) validateRequestOpponent(request.opponent) return request } @@ -46,15 +52,16 @@ const validateRequestBody = (body?: string): ReservationRequest => { } const jsonBody = transformRequestBody(body) - const { username, password, opponent, dateRanges } = jsonBody + const { username, password, opponent, dateRange } = jsonBody if ( !username || username.length < 1 || !password || password.length < 1 || - !dateRanges || - dateRanges.length < 1 || + !dateRange || + !dateRange.start || + !dateRange.end || (opponent && opponent.id && opponent.id.length < 1) || (opponent && opponent.name && opponent.name.length < 1) ) { @@ -65,39 +72,40 @@ const validateRequestBody = (body?: string): ReservationRequest => { } 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) } - } - ) + let json + try { + json = JSON.parse(body) + } catch (err) { + throw new ValidationError('Invalid request', ValidationErrorCode.INVALID_JSON) + } + const startTime = json.dateRange?.start ?? 'invalid' + const endTime = json.dateRange?.end ?? 'invalid' + const dateRange: DateRange = { start: dayjs(startTime), end: dayjs(endTime) } return { username: json.username, password: json.password, opponent: json.opponent, - dateRanges, + dateRange, } } -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) - } +const validateRequestDateRange = (dateRange: DateRange): void => { + // checking that both dates are valid + const { start, end } = dateRange + 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) - } + // 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) } } diff --git a/src/lambda/reservationHandler.ts b/src/lambda/reservationHandler.ts deleted file mode 100644 index 802f47d..0000000 --- a/src/lambda/reservationHandler.ts +++ /dev/null @@ -1,18 +0,0 @@ -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 => { - 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 }) -} diff --git a/src/lambda/reservationScheduler.ts b/src/lambda/reservationScheduler.ts deleted file mode 100644 index f74b268..0000000 --- a/src/lambda/reservationScheduler.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { SQSEvent, SQSHandler } from 'aws-lambda' -import { validateRequestEvent } from '../common/request' - -import { Reservation } from '../common/reservation' -import { Runner } from '../common/runner' - -export const run: SQSHandler = async (event: SQSEvent): Promise => { - const { request, error } = validateRequestEvent(event) - if (error || !request) { - throw new Error(error?.message) - } - - const { username, password, dateTimes, opponent } = request - const reservations = dateTimes.map((dt) => new Reservation(dt, opponent)) - - const runner = new Runner(username, password, reservations) - await runner.run({ headless: false }) -} diff --git a/src/lambdas/reservationHandler.ts b/src/lambdas/reservationHandler.ts new file mode 100644 index 0000000..bcf46d4 --- /dev/null +++ b/src/lambdas/reservationHandler.ts @@ -0,0 +1,7 @@ +import { Handler } from 'aws-lambda' + +import { Reservation } from '../common/reservation' +import { Runner } from '../common/runner' + +export const run: Handler = async (payload: string): Promise => { +} diff --git a/src/lambdas/reservationScheduler.ts b/src/lambdas/reservationScheduler.ts new file mode 100644 index 0000000..7cd3a68 --- /dev/null +++ b/src/lambdas/reservationScheduler.ts @@ -0,0 +1,22 @@ +import { Handler } from 'aws-lambda' +import dayjs from 'dayjs' + +import { InputEvent } from '../stepFunctions/event' +import { Reservation } from '../common/reservation' +import { validateRequest } from '../common/request' +import { Runner } from '../common/runner' + +export const run: Handler = async (input: InputEvent): Promise => { + console.log(`Handling event: ${input}`) + const { username, password, dateRange, opponent } = validateRequest(JSON.stringify(input.reservationRequest)) + console.log('Successfully validated request') + + console.log('Creating reservation') + const reservation = new Reservation(dateRange, opponent) + console.log('Created reservation') + + console.log('Runner starting') + const runner = new Runner(username, password, [reservation]) + await runner.run() + console.log('Runner finished') +} diff --git a/src/stepFunctions/event.ts b/src/stepFunctions/event.ts new file mode 100644 index 0000000..518367e --- /dev/null +++ b/src/stepFunctions/event.ts @@ -0,0 +1,9 @@ +import { ReservationRequest } from "../common/request"; + +export interface RawReservationRequest extends Omit { + dateRanges: { start: string, end: string }[] +} + +export interface InputEvent { + reservationRequest: RawReservationRequest +} \ No newline at end of file diff --git a/tests/common/request.test.ts b/tests/common/request.test.ts index b427a71..8359a71 100644 --- a/tests/common/request.test.ts +++ b/tests/common/request.test.ts @@ -12,9 +12,10 @@ describe('request', () => { const body = JSON.stringify({ username: 'collin', password: '123abc', - dateRanges: [ - { start: '2021-12-25T12:34:56Z', end: '2021-12-25T12:45:56Z' } - ], + dateRange: { + start: '2021-12-25T12:34:56Z', + end: '2021-12-25T12:45:56Z' + }, opponent: { id: '123', name: 'collin', @@ -28,17 +29,36 @@ describe('request', () => { expect(() => validateRequest(undefined)).toThrowError(new ValidationError('Invalid request', ValidationErrorCode.UNDEFINED_REQUEST_BODY)) }) + test('should fail for invalid json', () => { + const body = `A{ + username: 'collin', + password: '123abc', + dateRange: { + start: '2021-12-25T12:34:56Z', + end: '2021-12-25T12:45:56Z' + }, + opponent: { + id: '123', + name: 'collin', + } + }` + + expect(() => validateRequest(body)).toThrowError(new ValidationError('Invalid request', ValidationErrorCode.INVALID_JSON)) + }) + 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: '', password: '1qaz2wsx', dateRange: { start: '1', end: '1' }, opponent: { id: '123', name: 'abc' } }, + { password: '1qaz2wsx', dateRange: { start: '1', end: '1' }, opponent: { id: '123', name: 'abc' } }, + { username: 'collin', password: '', dateRange: { start: '1', end: '1' }, opponent: { id: '123', name: 'abc' } }, + { username: 'collin', dateRange: { start: '1', end: '1' }, opponent: { id: '123', name: 'abc' } }, + { username: 'collin', password: '1qaz2wsx', dateRange: {}, opponent: { id: '123', name: 'abc' } }, + { username: 'collin', password: '1qaz2wsx', dateRange: { start: '1' }, opponent: { id: '123', name: 'abc' } }, + { username: 'collin', password: '1qaz2wsx', dateRange: { end: '1' }, 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' } }, + { username: 'collin', password: '1qaz2wsx', dateRange: { start: '1', end: '1' }, opponent: { id: '', name: 'abc' } }, + { username: 'collin', password: '1qaz2wsx', dateRange: { start: '1', end: '1' }, opponent: { name: 'abc' } }, + { username: 'collin', password: '1qaz2wsx', dateRange: { start: '1', end: '1' }, opponent: { id: '123', name: '' } }, + { username: 'collin', password: '1qaz2wsx', dateRange: { 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)) }) @@ -47,9 +67,10 @@ describe('request', () => { const body = JSON.stringify({ username: 'collin', password: '123abc', - dateRanges: [ - { start: 'monkey', end: '2021-12-25T12:45:56Z' } - ], + dateRange: { + start: 'monkey', + end: '2021-12-25T12:45:56Z' + }, opponent: { id: '123', name: 'collin', @@ -67,7 +88,7 @@ describe('request', () => { const body = JSON.stringify({ username: 'collin', password: '123abc', - dateRanges: [ + dateRange: [ dateRange ], opponent: { @@ -83,9 +104,10 @@ describe('request', () => { const body = JSON.stringify({ username: 'collin', password: '123abc', - dateRanges: [ - { start: '2021-12-25T12:34:56Z', end: '2021-12-25T12:45:56Z' } - ], + dateRange: { + start: '2021-12-25T12:34:56Z', + end: '2021-12-25T12:45:56Z' + }, }) expect(() => validateRequest(body)).not.toThrow() @@ -100,9 +122,10 @@ describe('request', () => { const body = JSON.stringify({ username: 'collin', password: '123abc', - dateRanges: [ - { start: '2021-12-25T12:34:56Z', end: '2021-12-25T12:45:56Z' } - ], + dateRange: { + start: '2021-12-25T12:34:56Z', + end: '2021-12-25T12:45:56Z' + }, opponent, })