Skip to content

Commit

Permalink
feature-interview
Browse files Browse the repository at this point in the history
  • Loading branch information
Mugna0990 committed May 22, 2024
1 parent 22c3b49 commit d979247
Show file tree
Hide file tree
Showing 13 changed files with 419 additions and 116 deletions.
6 changes: 0 additions & 6 deletions api/src/application/application.entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,12 +40,6 @@ export class Application implements ApplicationInterface {
@Column({ length: 255 })
cv: string;

// @Column()
// availability: TimeSlot[];

// @Column({ "name": "interview_id" })
// interviewId: number;

@Column({ name: 'ita_level' })
itaLevel: LangLevel;
}
Expand Down
9 changes: 9 additions & 0 deletions api/src/application/applications.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,15 @@ export class ApplicationsService {
return match.length > 0;
}

async findLastApplicationByActiveUserId(applicantId: string): Promise<Application> {
return await this.applicationRepository.findOne({
where: { applicantId },
order: {
lastModified: 'DESC'
},
});
}

async listApplications(
submittedFrom: string,
submittedUntil: string,
Expand Down
22 changes: 2 additions & 20 deletions api/src/interview/create-interview.dto.ts
Original file line number Diff line number Diff line change
@@ -1,28 +1,10 @@
import { Interview } from '@hkrecruitment/shared';
import { User } from '../users/user.entity'
import { ApiProperty } from '@nestjs/swagger';


export class CreateInterviewDto implements Partial<Interview> {

@ApiProperty()
notes: string;

@ApiProperty()
created_at: Date;

@ApiProperty()
id_timeslot: number;
createdAt: Date;

@ApiProperty()
id_application: number;

@ApiProperty()
interviewer_1: User;

@ApiProperty()
interviewer_2: User;

@ApiProperty({ required: false })
optional_interviewer?: User;
timeslot_id: number;
}
162 changes: 162 additions & 0 deletions api/src/interview/interview.controller.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
import { Action, Person, Role } from '@hkrecruitment/shared';
import { InterviewController } from './interview.controller';
import { InterviewService } from './interview.service';
import { TestBed } from '@automock/jest';
import { createMockAbility } from '@hkrecruitment/shared/abilities.spec';
import { ForbiddenException, NotFoundException } from '@nestjs/common';
import { CreateInterviewDto } from './create-interview.dto';
import { AuthenticatedRequest } from 'src/authorization/authenticated-request.types';
import { createMock } from '@golevelup/ts-jest';
import { UpdateInterviewDto } from './update-interview.dto';
import { Interview } from './interview.entity';
import {
mockInterview,
MockCreateInterviewDTO,
MockUpdateInterviewDTO,
mockTimeSlot,
} from '@mocks/data';

describe('InterviewController', () => {
let controller: InterviewController;
let service: InterviewService;

beforeEach(async () => {
const { unit, unitRef } = TestBed.create(InterviewController).compile();

controller = unit;
service = unitRef.get(InterviewService);
});

it('should be defined', () => {
expect(controller).toBeDefined();
expect(service).toBeDefined();
});

describe('findById', () => {
it('should return an Interview if it exists', async () => {
const mockAbility = createMockAbility(({ can }) => {
can(Action.Read, 'Interview');
});
jest.spyOn(service, 'findById').mockResolvedValue(mockInterview);
expect(await controller.findById(123, mockAbility)).toStrictEqual(
mockInterview,
);
expect(service.findById).toHaveBeenCalled();
expect(mockAbility.can).toHaveBeenCalled();
});

it("should throw a NotFoundException if the Interview doesn't exist", async () => {
const mockAbility = createMockAbility(({ can }) => {
can(Action.Read, 'Interview');
});
jest.spyOn(service, 'findById').mockResolvedValue(null);
await expect(controller.findById(321, mockAbility)).rejects.toThrow(
NotFoundException,
);
expect(service.findById).toHaveBeenCalled();
});
});

describe('update', () => {
const mockReq = createMock<AuthenticatedRequest>();
const mockInterviewDto: UpdateInterviewDto = {
...mockInterview,
};
it('should update an Interview if it exists and the user is allowed to update it', async () => {
const mockAbility = createMockAbility(({ can }) => {
can(Action.Update, 'Interview');
});
jest.spyOn(service, 'findById').mockResolvedValue(mockInterview);
jest
.spyOn(service, 'update')
.mockImplementation((mockInterview) => Promise.resolve(mockInterview));
expect(
await controller.update(
123,
{ ...mockInterviewDto, notes: 'Notes' },
mockAbility,
mockReq,
),
).toStrictEqual({ ...mockInterview, notes: 'Notes' });
expect(service.findById).toHaveBeenCalledTimes(1);
expect(service.update).toHaveBeenCalledTimes(1);
expect(mockAbility.can).toHaveBeenCalled();
});

it("should throw a NotFoundException if the Interview doesn't exist", async () => {
const mockAbility = createMockAbility(({ can }) => {
can(Action.Update, 'Interview');
});
jest.spyOn(service, 'findById').mockResolvedValue(null);
await expect(
controller.update(321, mockInterviewDto, mockAbility, mockReq),
).rejects.toThrow(NotFoundException);
expect(service.findById).toHaveBeenCalledTimes(1);
});

it('should throw a ForbiddenException if the user is not allowed to update the Interview', async () => {
const mockAbility = createMockAbility(({ cannot }) => {
cannot(Action.Update, 'Interview', { oauthId: 123 });
});
jest.spyOn(service, 'findById').mockResolvedValue(mockInterview);
await expect(
controller.update(123, mockInterviewDto, mockAbility, mockReq),
).rejects.toThrow(ForbiddenException);
expect(service.findById).toHaveBeenCalledTimes(1);
expect(mockAbility.can).toHaveBeenCalled();
});
});

describe('create', () => {
const mockReq = createMock<AuthenticatedRequest>();
const mockInterviewDto: CreateInterviewDto = {
createdAt: new Date(2023, 0, 1),
timeslot_id: mockTimeSlot.id,
};

beforeEach(() => {
jest
.spyOn(service, 'create')
.mockImplementation((mockInterviewDto) =>
Promise.resolve(mockInterview),
);
});

it("should create an Interview if it doesn't exist and it's allowed", async () => {
const mockAbility = createMockAbility(({ can }) => {
can(Action.Create, 'Person');
});
jest.spyOn(service, 'findById').mockResolvedValue(null);
expect(
await controller.create(mockInterviewDto, mockAbility, mockReq),
).toStrictEqual(mockInterview);
expect(service.findById).toHaveBeenCalledTimes(1);
expect(service.create).toHaveBeenCalledTimes(1);
expect(mockAbility.can).toHaveBeenCalled();
});

it('should throw if interview with that Id already exists', async () => {
const mockAbility = createMockAbility(({ can }) => {
can(Action.Create, 'Interview');
});
jest.spyOn(service, 'findById').mockResolvedValue(mockInterview);
await expect(
controller.create(mockInterviewDto, mockAbility, mockReq),
).rejects.toThrow(ForbiddenException);
expect(service.findById).toHaveBeenCalledTimes(1);
expect(service.create).not.toHaveBeenCalled();
});

it('should throw if the user is not allowed to create an Interview', async () => {
const mockAbility = createMockAbility(({ cannot }) => {
cannot(Action.Create, 'Interview', { oauthId: '312' });
});
jest.spyOn(service, 'findById').mockResolvedValue(null);
await expect(
controller.create(mockInterviewDto, mockAbility, mockReq),
).rejects.toThrow(ForbiddenException);
expect(service.create).not.toHaveBeenCalled();
expect(mockAbility.can).toHaveBeenCalled();
});
});
});
43 changes: 22 additions & 21 deletions api/src/interview/interview.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,10 @@ import {
Body,
Post,
Req,
Delete
Delete,
ConflictException,
} from '@nestjs/common';
import { Interview } from './interview.entity';
import { Interview } from './interview.entity';
import { InterviewService } from './interview.Service';
import { TimeSlotsService } from '../timeslots/timeslots.service';
import { ApplicationsService } from '../application/applications.service';
Expand All @@ -24,7 +25,7 @@ import {
updateInterviewSchema,
} from '@hkrecruitment/shared';
import { JoiValidate } from 'src/joi-validation/joi-validate.decorator';
import {
import {
ApiBadRequestResponse,
ApiBearerAuth,
ApiNotFoundResponse,
Expand All @@ -38,13 +39,12 @@ import { Ability } from 'src/authorization/ability.decorator';
@ApiBearerAuth()
@ApiTags('interview')
@Controller('interview')

export class InterviewController {
constructor(
private readonly interviewService: InterviewService,
private readonly timeSlotService: TimeSlotsService,
private readonly applicationService: ApplicationsService
) {}
private readonly applicationService: ApplicationsService,
) {}

@ApiNotFoundResponse()
@ApiBadRequestResponse()
Expand Down Expand Up @@ -108,7 +108,7 @@ export class InterviewController {
...updateInterview,
});
}

@ApiNotFoundResponse()
@ApiBadRequestResponse()
@Post()
Expand All @@ -121,19 +121,20 @@ export class InterviewController {
@Ability() ability: AppAbility,
@Req() req: AuthenticatedRequest,
): Promise<Interview> {
const timeslot = await this.timeSlotService.findById(interview.id_timeslot)
if (timeslot === null) {
throw new NotFoundException();
}
const application = await this.applicationService.findByApplicationId(interview.id_application)
if (application === null) {
throw new NotFoundException();
}
return this.interviewService.create(
interview,
application,
timeslot
);
const Id = req.user.sub;
const timeslot = await this.timeSlotService.findById(interview.timeslot_id);
if (timeslot === null) {
throw new NotFoundException();
}
const application =
await this.applicationService.findLastApplicationByActiveUserId(Id);
if (application === null) {
throw new NotFoundException();
}
try {
return this.interviewService.create(interview, application, timeslot);
} catch {
throw new ConflictException();
}
}
}

20 changes: 10 additions & 10 deletions api/src/interview/interview.entity.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';
import {Interview as InterviewSlot} from '@hkrecruitment/shared';
import {User} from '../users/user.entity';
import { Interview as InterviewSlot } from '@hkrecruitment/shared';
import { User } from '../users/user.entity';
import { TimeSlot } from '../timeslots/timeslot.entity';
import { Application } from '../application/application.entity';

Expand All @@ -12,21 +12,21 @@ export class Interview implements InterviewSlot {
@Column()
notes: string;

@Column()
created_at: Date;
@Column({ name: 'created_at' })
createdAt: Date;

@Column()
timeslot: TimeSlot;

@Column()
application: Application;

@Column()
interviewer_1: User;
@Column({ name: 'interviewer_1' })
interviewer1: User;

@Column()
interviewer_2: User;
@Column({ name: 'interviewer_2' })
interviewer2: User;

@Column({nullable: true})
optional_interviewer?: User;
@Column({ name: 'optional_interviewer', nullable: true })
optionalInterviewer?: User;
}
3 changes: 2 additions & 1 deletion api/src/interview/interview.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,10 @@ import { InterviewService } from './interview.service';
import { InterviewController } from './interview.controller';
import { TypeOrmModule } from '@nestjs/typeorm';
import { Interview } from './interview.entity';
import { RecruitmentSessionModule } from 'src/recruitment-session/recruitment-session.module';

@Module({
imports: [TypeOrmModule.forFeature([Interview])],
imports: [TypeOrmModule.forFeature([Interview]), RecruitmentSessionModule],
providers: [InterviewService],
controllers: [InterviewController],
exports: [InterviewService],
Expand Down
Loading

0 comments on commit d979247

Please sign in to comment.