-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge branch 'feat/availabilities' into rewrite-application
- Loading branch information
Showing
21 changed files
with
623 additions
and
8 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
import { TimeSlot } from '@hkrecruitment/shared'; | ||
import { ApiProperty } from '@nestjs/swagger'; | ||
|
||
export class CreateTimeSlotDto implements TimeSlot { | ||
@ApiProperty() | ||
start: Date; | ||
|
||
@ApiProperty() | ||
end: Date; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm'; | ||
import { TimeSlot as TimeSlotInterface } from '@hkrecruitment/shared'; | ||
|
||
@Entity() | ||
export class TimeSlot implements TimeSlotInterface { | ||
@PrimaryGeneratedColumn('increment') | ||
id: number; | ||
|
||
@Column() | ||
start: Date; | ||
|
||
@Column() | ||
end: Date; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,104 @@ | ||
import { TestBed } from '@automock/jest'; | ||
import { | ||
mockTimeSlot, | ||
testDate, | ||
testDateTimeEnd, | ||
testDateTimeStart, | ||
} from '@mocks/data'; | ||
import { TimeSlotsController } from './timeslots.controller'; | ||
import { TimeSlotsService } from './timeslots.service'; | ||
import { testDateTime10Minutes } from '@mocks/data'; | ||
import { testDateTime3Hours } from '@mocks/data'; | ||
|
||
describe('TimeSlotController', () => { | ||
let controller: TimeSlotsController; | ||
let service: TimeSlotsService; | ||
|
||
/************* Test setup ************/ | ||
|
||
beforeEach(async () => { | ||
const { unit, unitRef } = TestBed.create(TimeSlotsController).compile(); | ||
controller = unit; | ||
service = unitRef.get(TimeSlotsService); | ||
}); | ||
|
||
it('should be defined', () => { | ||
expect(controller).toBeDefined(); | ||
expect(service).toBeDefined(); | ||
}); | ||
|
||
// Create a time slot | ||
describe('createTimeSlot', () => { | ||
it('should allow creating a valid time slot', async () => { | ||
const timeSlot = { | ||
start: mockTimeSlot.start, | ||
end: mockTimeSlot.end, | ||
}; | ||
|
||
jest.spyOn(service, 'countOverlappingTimeSlots').mockResolvedValue(0); | ||
jest.spyOn(service, 'createTimeSlot').mockResolvedValue(mockTimeSlot); | ||
|
||
const result = await controller.createTimeSlot(timeSlot); | ||
|
||
expect(result).toEqual(mockTimeSlot); | ||
}); | ||
|
||
it('should throw an error if the duration is less than 30 minutes', async () => { | ||
const timeSlot = { | ||
start: testDateTimeStart, | ||
end: testDateTime10Minutes, | ||
}; | ||
|
||
await expect(controller.createTimeSlot(timeSlot)).rejects.toThrow( | ||
'The duration of the time slot must be at least 30 minutes', | ||
); | ||
}); | ||
|
||
it('should throw an error if the duration is more than 60 minutes', async () => { | ||
const timeSlot = { | ||
start: testDateTimeStart, | ||
end: testDateTime3Hours, | ||
}; | ||
|
||
await expect(controller.createTimeSlot(timeSlot)).rejects.toThrow( | ||
'The duration of the time slot must be at most 60 minutes', | ||
); | ||
}); | ||
|
||
it('should throw an error if the time slot overlaps with an existing time slot', async () => { | ||
const timeSlot = { | ||
start: testDateTimeStart, | ||
end: testDateTimeEnd, | ||
}; | ||
|
||
jest.spyOn(service, 'countOverlappingTimeSlots').mockResolvedValue(1); | ||
|
||
await expect(controller.createTimeSlot(timeSlot)).rejects.toThrow( | ||
'The time slot overlaps with existing time slots', | ||
); | ||
}); | ||
}); | ||
|
||
describe('deleteTimeSlot', () => { | ||
it('should allow deleting an existing time slot', async () => { | ||
jest.spyOn(service, 'findById').mockResolvedValue(mockTimeSlot); | ||
jest.spyOn(service, 'deleteTimeSlot').mockResolvedValue(mockTimeSlot); | ||
|
||
await expect(controller.deleteTimeSlot(mockTimeSlot.id)).resolves.toEqual( | ||
mockTimeSlot, | ||
); | ||
expect(service.deleteTimeSlot).toHaveBeenCalledWith(mockTimeSlot); | ||
expect(service.deleteTimeSlot).toHaveBeenCalledTimes(1); | ||
}); | ||
|
||
it('should throw an error if the time slot does not exist', async () => { | ||
jest.spyOn(service, 'findById').mockResolvedValue(null); | ||
jest.spyOn(service, 'deleteTimeSlot').mockResolvedValue(mockTimeSlot); | ||
|
||
await expect( | ||
controller.deleteTimeSlot(mockTimeSlot.id), | ||
).rejects.toThrowError('Time slot not found'); | ||
expect(service.deleteTimeSlot).toHaveBeenCalledTimes(0); | ||
}); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,91 @@ | ||
import { | ||
Body, | ||
Controller, | ||
BadRequestException, | ||
NotFoundException, | ||
ConflictException, | ||
Param, | ||
Post, | ||
Delete, | ||
} from '@nestjs/common'; | ||
import { TimeSlotsService } from './timeslots.service'; | ||
import { Action, createTimeSlotSchema, TimeSlot } from '@hkrecruitment/shared'; | ||
import { JoiValidate } from '../joi-validation/joi-validate.decorator'; | ||
import { | ||
ApiBadRequestResponse, | ||
ApiBearerAuth, | ||
ApiForbiddenResponse, | ||
ApiNotFoundResponse, | ||
ApiCreatedResponse, | ||
ApiOkResponse, | ||
ApiTags, | ||
ApiConflictResponse, | ||
ApiNoContentResponse, | ||
} from '@nestjs/swagger'; | ||
import { CheckPolicies } from 'src/authorization/check-policies.decorator'; | ||
import { CreateTimeSlotDto } from './create-timeslot.dto'; | ||
import * as Joi from 'joi'; | ||
|
||
@ApiBearerAuth() | ||
@ApiTags('timeslots') | ||
@Controller('timeslots') | ||
export class TimeSlotsController { | ||
constructor(private readonly timeSlotsService: TimeSlotsService) {} | ||
|
||
@ApiBadRequestResponse() | ||
@ApiForbiddenResponse() | ||
@ApiConflictResponse({ | ||
description: 'The time slot overlaps with existing time slots', | ||
}) | ||
@ApiCreatedResponse() | ||
@JoiValidate({ | ||
body: createTimeSlotSchema, | ||
}) | ||
@CheckPolicies((ability) => ability.can(Action.Create, 'TimeSlot')) | ||
@Post() | ||
async createTimeSlot(@Body() timeSlot: CreateTimeSlotDto): Promise<TimeSlot> { | ||
const startDate = new Date(timeSlot.start); | ||
const endDate = new Date(timeSlot.end); | ||
|
||
// Check duration | ||
const durationInMinutes = | ||
(endDate.getTime() - startDate.getTime()) / (1000 * 60); | ||
if (durationInMinutes < 30) { | ||
throw new BadRequestException( | ||
'The duration of the time slot must be at least 30 minutes', | ||
); | ||
} else if (durationInMinutes > 60) { | ||
throw new BadRequestException( | ||
'The duration of the time slot must be at most 60 minutes', | ||
); | ||
} | ||
|
||
// Check overlapping timeslots | ||
const overlappingTimeSlots = | ||
await this.timeSlotsService.countOverlappingTimeSlots(startDate, endDate); | ||
if (overlappingTimeSlots > 0) | ||
throw new ConflictException( | ||
'The time slot overlaps with existing time slots', | ||
); | ||
|
||
return await this.timeSlotsService.createTimeSlot(timeSlot); | ||
} | ||
|
||
@ApiBadRequestResponse() | ||
@ApiForbiddenResponse() | ||
@ApiNotFoundResponse() | ||
@ApiOkResponse() | ||
@ApiNoContentResponse() | ||
@CheckPolicies((ability) => ability.can(Action.Delete, 'TimeSlot')) | ||
@Delete('/:time_slot_id') | ||
@JoiValidate({ | ||
param: Joi.number().positive().integer().required().label('time_slot_id'), | ||
}) | ||
async deleteTimeSlot( | ||
@Param('time_slot_id') timeSlotId: number, | ||
): Promise<TimeSlot> { | ||
const timeSlot = await this.timeSlotsService.findById(timeSlotId); | ||
if (!timeSlot) throw new NotFoundException('Time slot not found'); | ||
return await this.timeSlotsService.deleteTimeSlot(timeSlot); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
import { Module } from '@nestjs/common'; | ||
import { TimeSlotsService } from './timeslots.service'; | ||
import { TimeSlotsController } from './timeslots.controller'; | ||
import { TypeOrmModule } from '@nestjs/typeorm'; | ||
import { TimeSlot } from './timeslot.entity'; | ||
import { UsersModule } from 'src/users/users.module'; | ||
|
||
@Module({ | ||
imports: [TypeOrmModule.forFeature([TimeSlot]), UsersModule], | ||
providers: [TimeSlotsService], | ||
controllers: [TimeSlotsController], | ||
exports: [TimeSlotsService], | ||
}) | ||
export class TimeSlotsModule {} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,76 @@ | ||
import { mockTimeSlot, testDate } from '@mocks/data'; | ||
import { mockedRepository } from '@mocks/repositories'; | ||
import { TestingModule, Test } from '@nestjs/testing'; | ||
import { getRepositoryToken } from '@nestjs/typeorm'; | ||
import { TimeSlot } from './timeslot.entity'; | ||
import { TimeSlotsService } from './timeslots.service'; | ||
|
||
describe('TimeSlotsService', () => { | ||
let timeSlotService: TimeSlotsService; | ||
|
||
/************* Test setup ************/ | ||
|
||
beforeAll(() => { | ||
jest | ||
.spyOn(global, 'Date') | ||
.mockImplementation(() => testDate as unknown as string); | ||
}); | ||
|
||
beforeEach(async () => { | ||
const module: TestingModule = await Test.createTestingModule({ | ||
providers: [ | ||
TimeSlotsService, | ||
{ | ||
provide: getRepositoryToken(TimeSlot), | ||
useValue: mockedRepository, | ||
}, | ||
], | ||
}).compile(); | ||
|
||
timeSlotService = module.get<TimeSlotsService>(TimeSlotsService); | ||
}); | ||
|
||
afterEach(() => jest.clearAllMocks()); | ||
|
||
/*************** Tests ***************/ | ||
|
||
it('should be defined', () => { | ||
expect(timeSlotService).toBeDefined(); | ||
}); | ||
|
||
describe('deleteTimeSlot', () => { | ||
it('should remove the specified timeslot from the database', async () => { | ||
jest.spyOn(mockedRepository, 'remove').mockResolvedValue(mockTimeSlot); | ||
const result = await timeSlotService.deleteTimeSlot(mockTimeSlot); | ||
expect(result).toEqual(mockTimeSlot); | ||
expect(mockedRepository.remove).toHaveBeenCalledTimes(1); | ||
}); | ||
}); | ||
|
||
describe('listTimeSlots', () => { | ||
it('should return all timeslots', async () => { | ||
jest.spyOn(mockedRepository, 'find').mockResolvedValue([mockTimeSlot]); | ||
const result = await timeSlotService.listTimeSlots(); | ||
expect(result).toEqual([mockTimeSlot]); | ||
expect(mockedRepository.find).toHaveBeenCalledTimes(1); | ||
}); | ||
}); | ||
|
||
describe('findById', () => { | ||
it('should return the timeslot with the specified id', async () => { | ||
jest.spyOn(mockedRepository, 'findBy').mockResolvedValue([mockTimeSlot]); | ||
const result = await timeSlotService.findById(mockTimeSlot.id); | ||
expect(result).toEqual(mockTimeSlot); | ||
expect(mockedRepository.findBy).toHaveBeenCalledTimes(1); | ||
}); | ||
}); | ||
|
||
describe('createTimeSlot', () => { | ||
it('should create a new timeslot', async () => { | ||
jest.spyOn(mockedRepository, 'save').mockResolvedValue(mockTimeSlot); | ||
const result = await timeSlotService.createTimeSlot(mockTimeSlot); | ||
expect(result).toEqual(mockTimeSlot); | ||
expect(mockedRepository.save).toHaveBeenCalledTimes(1); | ||
}); | ||
}); | ||
}); |
Oops, something went wrong.