Skip to content

Commit

Permalink
Merge branch 'feat/availabilities' into rewrite-application
Browse files Browse the repository at this point in the history
  • Loading branch information
AlbertoBaroso committed Nov 10, 2023
2 parents 43ad011 + 6ff25a1 commit 060284f
Show file tree
Hide file tree
Showing 21 changed files with 623 additions and 8 deletions.
2 changes: 2 additions & 0 deletions api/src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { ConfigModule, ConfigService } from '@nestjs/config';
import { TypeOrmModule, TypeOrmModuleOptions } from '@nestjs/typeorm';
import { UsersModule } from './users/users.module';
import { ApplicationsModule } from './application/applications.module';
import { TimeSlotsModule } from './timeslots/timeslots.module';
import { AuthenticationModule } from './authentication/authentication.module';
import { APP_GUARD } from '@nestjs/core';
import { JwtGuard } from './authentication/jwt-guard.guard';
Expand Down Expand Up @@ -31,6 +32,7 @@ import { AuthorizationGuard } from './authorization/authorization.guard';
}),
UsersModule,
ApplicationsModule,
TimeSlotsModule,
AuthenticationModule,
AuthorizationModule,
],
Expand Down
1 change: 0 additions & 1 deletion api/src/application/applications.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,6 @@ export class ApplicationsService {

// Add state condition when "state" is specified
if (state) conditions['state'] = state;

// Retrieve applications
return await this.applicationRepository.findBy(conditions);
}
Expand Down
2 changes: 1 addition & 1 deletion api/src/application/create-application.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ export class CreateApplicationDto implements Partial<Application> {
export function flattenApplication(
application: CreateApplicationDto,
): Application {
let newApplication: Application = {
const newApplication: Application = {
...application,
...application.bscApplication,
...application.mscApplication,
Expand Down
10 changes: 10 additions & 0 deletions api/src/mocks/data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,16 @@ import {
import { UpdateApplicationDto } from 'src/application/update-application.dto';

export const testDate = new Date(2023, 0, 1);
export const testDateTimeStart = new Date(2023, 0, 1, 10, 30, 0);
export const testDateTime10Minutes = new Date(2023, 0, 1, 10, 40, 0);
export const testDateTime3Hours = new Date(2023, 0, 1, 13, 30, 0);
export const testDateTimeEnd = new Date(2023, 0, 1, 11, 30, 0);

export const mockTimeSlot = {
start: testDateTimeStart,
end: testDateTimeEnd,
id: 1,
};

export const baseFile = {
encoding: '7bit',
Expand Down
10 changes: 10 additions & 0 deletions api/src/timeslots/create-timeslot.dto.ts
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;
}
14 changes: 14 additions & 0 deletions api/src/timeslots/timeslot.entity.ts
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;
}
104 changes: 104 additions & 0 deletions api/src/timeslots/timeslots.controller.spec.ts
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);
});
});
});
91 changes: 91 additions & 0 deletions api/src/timeslots/timeslots.controller.ts
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);
}
}
14 changes: 14 additions & 0 deletions api/src/timeslots/timeslots.module.ts
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 {}
76 changes: 76 additions & 0 deletions api/src/timeslots/timeslots.service.spec.ts
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);
});
});
});
Loading

0 comments on commit 060284f

Please sign in to comment.