From 3953454f909ccf31e3c1d9a5431875b56ac14987 Mon Sep 17 00:00:00 2001 From: luanavfg Date: Thu, 7 Mar 2024 08:14:36 -0300 Subject: [PATCH 1/8] feat(CC-132): create generic use case to update appointment --- .../update-appointment-dto.ts | 47 ++++++++++ .../update-appointment.service.ts | 31 +++++++ .../update-appointment.spec.ts | 93 +++++++++++++++++++ 3 files changed, 171 insertions(+) create mode 100644 apps/core-rest-api/src/app/core/domains/appointment/use-cases/update-single-appointment/update-appointment-dto.ts create mode 100644 apps/core-rest-api/src/app/core/domains/appointment/use-cases/update-single-appointment/update-appointment.service.ts create mode 100644 apps/core-rest-api/src/app/core/domains/appointment/use-cases/update-single-appointment/update-appointment.spec.ts diff --git a/apps/core-rest-api/src/app/core/domains/appointment/use-cases/update-single-appointment/update-appointment-dto.ts b/apps/core-rest-api/src/app/core/domains/appointment/use-cases/update-single-appointment/update-appointment-dto.ts new file mode 100644 index 0000000..4645858 --- /dev/null +++ b/apps/core-rest-api/src/app/core/domains/appointment/use-cases/update-single-appointment/update-appointment-dto.ts @@ -0,0 +1,47 @@ +import { IsBoolean, IsDate, IsEnum, IsOptional, IsString } from "class-validator"; +import { PaymentMethod } from '../../../../shared/interfaces/payments'; + +export class UpdateAppointmentDto { + @IsString() + id!: string; + + @IsOptional() + @IsDate() + date?: Date; + + @IsOptional() + @IsBoolean() + online?: boolean; + + @IsOptional() + @IsBoolean() + confirmed?: boolean; + + @IsOptional() + @IsDate() + confirmationDate?: Date | null; + + @IsOptional() + @IsBoolean() + cancelled?: boolean; + + @IsOptional() + @IsDate() + cancellationDate?: Date | null; + + @IsOptional() + @IsBoolean() + done?: boolean | null; + + @IsOptional() + @IsBoolean() + missed?: boolean | null; + + @IsOptional() + @IsBoolean() + paid?: boolean; + + @IsOptional() + @IsEnum(PaymentMethod) + paymentMethod?: PaymentMethod; +} diff --git a/apps/core-rest-api/src/app/core/domains/appointment/use-cases/update-single-appointment/update-appointment.service.ts b/apps/core-rest-api/src/app/core/domains/appointment/use-cases/update-single-appointment/update-appointment.service.ts new file mode 100644 index 0000000..5237dab --- /dev/null +++ b/apps/core-rest-api/src/app/core/domains/appointment/use-cases/update-single-appointment/update-appointment.service.ts @@ -0,0 +1,31 @@ +import { NotFoundException } from '@nestjs/common'; +import { plainToInstance } from 'class-transformer'; +import { APPOINTMENT_ERROR_MESSAGES } from '../../../../../shared/errors/error-messages'; +import { applicationValidateOrReject } from '../../../../../shared/validators/validate-or-reject'; +import { AppointmentDatabaseRepository } from '../../repositories/database-repository'; +import { UpdateAppointmentDto } from './update-appointment-dto'; + +export class UpdateAppointmentService { + constructor(private appointmentDatabaseRepository: AppointmentDatabaseRepository) {} + + async execute(newAppointmentInfo: UpdateAppointmentDto): Promise { + // Validate props + + const updateAppointmentDateDtoInstance = plainToInstance( + UpdateAppointmentDto, + newAppointmentInfo, + ); + + await applicationValidateOrReject(updateAppointmentDateDtoInstance); + + const oldAppointmentInfo = + await this.appointmentDatabaseRepository.findSingleAppointmentById( + newAppointmentInfo.id, + ); + if (!oldAppointmentInfo) { + throw new NotFoundException(APPOINTMENT_ERROR_MESSAGES['APPOINTMENT_NOT_FOUND']); + } + + await this.appointmentDatabaseRepository.updateAppointment(newAppointmentInfo); + } +} diff --git a/apps/core-rest-api/src/app/core/domains/appointment/use-cases/update-single-appointment/update-appointment.spec.ts b/apps/core-rest-api/src/app/core/domains/appointment/use-cases/update-single-appointment/update-appointment.spec.ts new file mode 100644 index 0000000..a64842c --- /dev/null +++ b/apps/core-rest-api/src/app/core/domains/appointment/use-cases/update-single-appointment/update-appointment.spec.ts @@ -0,0 +1,93 @@ +import { fakerPT_BR as faker } from '@faker-js/faker'; +import { NotFoundException } from '@nestjs/common'; +import { randomUUID } from 'crypto'; +import { APPOINTMENT_ERROR_MESSAGES } from '../../../../../shared/errors/error-messages'; +import { PaymentMethod } from '../../../../shared/interfaces/payments'; +import { InMemoryAppointmentDatabaseRepository } from '../../repositories/database-in-memory-repository'; +import { AppointmentDatabaseRepository } from '../../repositories/database-repository'; +import { CreateSingleAppointmentDto } from '../create-single-appointment/create-single-appointment-dto'; +import { UpdateAppointmentDto } from './update-appointment-dto'; +import { UpdateAppointmentService } from './update-appointment.service'; + +describe('[appointment] Update Appointment Service', () => { + const fakeAppointment: CreateSingleAppointmentDto = { + psychologistId: randomUUID(), + patientId: randomUUID(), + date: faker.date.recent({ days: 10 }), + online: false, + clinicId: randomUUID(), + confirmed: true, + confirmationDate: faker.date.recent({ days: 5 }), + cancelled: false, + paymentMethod: PaymentMethod.CREDIT_CARD, + }; + + let service: UpdateAppointmentService; + let databaseRepository: AppointmentDatabaseRepository; + + beforeEach(async () => { + databaseRepository = new InMemoryAppointmentDatabaseRepository(); + service = new UpdateAppointmentService(databaseRepository); + }); + + it('should update an appointment changing infos', async () => { + const createAppointment = + await databaseRepository.createSingleAppointment(fakeAppointment); + + const newAppointmentInfo: UpdateAppointmentDto = { + id: createAppointment.id, + paid: true, + paymentMethod: PaymentMethod.PIX, + }; + + await service.execute(newAppointmentInfo); + const findAppointment = await databaseRepository.findSingleAppointmentById( + newAppointmentInfo.id, + ); + + expect(findAppointment).toEqual({ + ...createAppointment, + ...newAppointmentInfo, + }); + + expect(findAppointment?.paid).toBe(newAppointmentInfo.paid); + + expect(findAppointment?.paymentMethod).toBe(newAppointmentInfo.paymentMethod); + expect(findAppointment?.paymentMethod).not.toBe(fakeAppointment.paymentMethod); + }); + + it('should update an appointment changing date', async () => { + const createAppointment = + await databaseRepository.createSingleAppointment(fakeAppointment); + + const newAppointmentInfo: UpdateAppointmentDto = { + id: createAppointment.id, + date: faker.date.future() + }; + + await service.execute(newAppointmentInfo); + const findAppointment = await databaseRepository.findSingleAppointmentById( + newAppointmentInfo.id, + ); + + expect(findAppointment).toEqual({ + ...createAppointment, + ...newAppointmentInfo, + }); + + expect(findAppointment?.date).toBe(newAppointmentInfo.date); + expect(findAppointment?.date).not.toBe(fakeAppointment.date); + }); + + it('should throw error if appointment does not exist', async () => { + const newAppointmentInfo: UpdateAppointmentDto = { + id: randomUUID(), + paid: true, + paymentMethod: PaymentMethod.PIX, + }; + + await expect(service.execute(newAppointmentInfo)).rejects.toThrow( + new NotFoundException(APPOINTMENT_ERROR_MESSAGES['APPOINTMENT_NOT_FOUND']), + ); + }); +}); From 63ad046b0a167cebf313e40d47e39f5951f29d53 Mon Sep 17 00:00:00 2001 From: luanavfg Date: Thu, 7 Mar 2024 08:16:26 -0300 Subject: [PATCH 2/8] adjust(CC-132): create generic methods to update appointment in repositories --- ...tgres-prisma-orm-appointment-repository.ts | 14 +++++++++++ .../database-in-memory-repository.ts | 24 +++++++++++++++++++ .../repositories/database-repository.ts | 4 ++++ 3 files changed, 42 insertions(+) diff --git a/apps/core-rest-api/src/app/adapters/database/repositories/appointment/postgres-prisma-orm-appointment-repository.ts b/apps/core-rest-api/src/app/adapters/database/repositories/appointment/postgres-prisma-orm-appointment-repository.ts index 201770f..2d1d8cf 100644 --- a/apps/core-rest-api/src/app/adapters/database/repositories/appointment/postgres-prisma-orm-appointment-repository.ts +++ b/apps/core-rest-api/src/app/adapters/database/repositories/appointment/postgres-prisma-orm-appointment-repository.ts @@ -88,6 +88,20 @@ export class PostgresqlPrismaOrmAppointmentRepository implements AppointmentData await this.postgresqlPrismaOrmService['appointment'].update(toPrismaEntity); } + async updateAppointment(newAppointmentInfo: UpdateAppointmentInfoDto): Promise { + const oldAppointmentInfo = await this.findSingleAppointmentById(newAppointmentInfo.id); + + if (!oldAppointmentInfo) { + throw new ConflictException(APPOINTMENT_ERROR_MESSAGES['APPOINTMENT_NOT_FOUND']); + } + + const toPrismaEntity = PostgresqlPrismaAppointmentMapper.toPrismaUpdate({ + ...newAppointmentInfo, + }); + + await this.postgresqlPrismaOrmService['appointment'].update(toPrismaEntity); + } + async deleteSingleAppointment(appointmentId: string): Promise { const isAppointmentExists = await this.findSingleAppointmentById(appointmentId); diff --git a/apps/core-rest-api/src/app/core/domains/appointment/repositories/database-in-memory-repository.ts b/apps/core-rest-api/src/app/core/domains/appointment/repositories/database-in-memory-repository.ts index a80eed9..e2ed3eb 100644 --- a/apps/core-rest-api/src/app/core/domains/appointment/repositories/database-in-memory-repository.ts +++ b/apps/core-rest-api/src/app/core/domains/appointment/repositories/database-in-memory-repository.ts @@ -4,6 +4,7 @@ import { AppointmentEntity } from '../entities/appointment/entity'; import { CreateSingleAppointmentDto } from '../use-cases/create-single-appointment/create-single-appointment-dto'; import { UpdatedAppointmentDateDto } from '../use-cases/update-appointment-date/update-appointment-date-dto'; import { UpdateAppointmentInfoDto } from '../use-cases/update-appointment-info/update-appointment-info-dto'; +import { UpdateAppointmentDto } from '../use-cases/update-single-appointment/update-appointment-dto'; import { AppointmentDatabaseRepository } from './database-repository'; export class InMemoryAppointmentDatabaseRepository @@ -94,6 +95,29 @@ export class InMemoryAppointmentDatabaseRepository this.appointments[appointmentIndex] = updateAppointmentDate; } + async updateAppointment( + newAppointmentInfo: UpdateAppointmentDto, + ): Promise { + const oldAppointmentInfo = await this.findSingleAppointmentById( + newAppointmentInfo.id, + ); + + if (!oldAppointmentInfo) { + throw new ConflictException(APPOINTMENT_ERROR_MESSAGES['APPOINTMENT_NOT_FOUND']); + } + + const appointmentIndex = this.appointments.findIndex((appointment) => { + return appointment.id === newAppointmentInfo.id; + }); + + const updatedAppointment = Object.assign(oldAppointmentInfo, { + ...newAppointmentInfo, + updatedAt: new Date(), + }); + + this.appointments[appointmentIndex] = updatedAppointment; + } + async deleteSingleAppointment(appointmentId: string): Promise { const isAppointmentExist = await this.findSingleAppointmentById(appointmentId); diff --git a/apps/core-rest-api/src/app/core/domains/appointment/repositories/database-repository.ts b/apps/core-rest-api/src/app/core/domains/appointment/repositories/database-repository.ts index b894895..8488d5c 100644 --- a/apps/core-rest-api/src/app/core/domains/appointment/repositories/database-repository.ts +++ b/apps/core-rest-api/src/app/core/domains/appointment/repositories/database-repository.ts @@ -2,6 +2,7 @@ import { AppointmentEntity } from '../entities/appointment/entity'; import { CreateSingleAppointmentDto } from '../use-cases/create-single-appointment/create-single-appointment-dto'; import { UpdatedAppointmentDateDto } from '../use-cases/update-appointment-date/update-appointment-date-dto'; import { UpdateAppointmentInfoDto } from '../use-cases/update-appointment-info/update-appointment-info-dto'; +import { UpdateAppointmentDto } from '../use-cases/update-single-appointment/update-appointment-dto'; export abstract class AppointmentDatabaseRepository { abstract createSingleAppointment( @@ -20,5 +21,8 @@ export abstract class AppointmentDatabaseRepository { abstract updateAppointmentDate( newAppointmentInfo: UpdatedAppointmentDateDto ): Promise; + abstract updateAppointment( + newAppointmentInfo: UpdateAppointmentDto + ): Promise; abstract deleteSingleAppointment(appointmentId: string): Promise; } From 1908da48170f854c7351ca1d7a33bc4d25ef0b83 Mon Sep 17 00:00:00 2001 From: luanavfg Date: Thu, 7 Mar 2024 08:20:43 -0300 Subject: [PATCH 3/8] feat(CC-132): add endpoint to update appointment --- .../update-appointment/input.dto.ts | 49 +++++++++++++++++++ .../nestjs-update-appointment.service.ts | 10 ++++ .../update-appointment/output.dto.ts | 6 +++ .../update-appointment.controller.ts | 37 ++++++++++++++ 4 files changed, 102 insertions(+) create mode 100644 apps/core-rest-api/src/app/adapters/controllers/api/use-cases/appointment/update-appointment/input.dto.ts create mode 100644 apps/core-rest-api/src/app/adapters/controllers/api/use-cases/appointment/update-appointment/nestjs-update-appointment.service.ts create mode 100644 apps/core-rest-api/src/app/adapters/controllers/api/use-cases/appointment/update-appointment/output.dto.ts create mode 100644 apps/core-rest-api/src/app/adapters/controllers/api/use-cases/appointment/update-appointment/update-appointment.controller.ts diff --git a/apps/core-rest-api/src/app/adapters/controllers/api/use-cases/appointment/update-appointment/input.dto.ts b/apps/core-rest-api/src/app/adapters/controllers/api/use-cases/appointment/update-appointment/input.dto.ts new file mode 100644 index 0000000..4a1238e --- /dev/null +++ b/apps/core-rest-api/src/app/adapters/controllers/api/use-cases/appointment/update-appointment/input.dto.ts @@ -0,0 +1,49 @@ +import { IsBoolean, IsDate, IsEnum, IsOptional, IsString } from 'class-validator'; +import { PaymentMethod } from '../../../../../../core/shared/interfaces/payments'; + +export class UpdateAppointmentControllerReqParamsInputDto { + @IsString() + id!: string; +} + +export class UpdateAppointmentControllerBodyParamsInputDto { + @IsOptional() + @IsDate() + date?: Date; + + @IsOptional() + @IsBoolean() + online?: boolean; + + @IsOptional() + @IsBoolean() + confirmed?: boolean; + + @IsOptional() + @IsDate() + confirmationDate?: Date | null; + + @IsOptional() + @IsBoolean() + cancelled?: boolean; + + @IsOptional() + @IsDate() + cancellationDate?: Date | null; + + @IsOptional() + @IsBoolean() + done?: boolean | null; + + @IsOptional() + @IsBoolean() + missed?: boolean | null; + + @IsOptional() + @IsBoolean() + paid?: boolean; + + @IsOptional() + @IsEnum(PaymentMethod) + paymentMethod?: PaymentMethod; +} diff --git a/apps/core-rest-api/src/app/adapters/controllers/api/use-cases/appointment/update-appointment/nestjs-update-appointment.service.ts b/apps/core-rest-api/src/app/adapters/controllers/api/use-cases/appointment/update-appointment/nestjs-update-appointment.service.ts new file mode 100644 index 0000000..33a81a4 --- /dev/null +++ b/apps/core-rest-api/src/app/adapters/controllers/api/use-cases/appointment/update-appointment/nestjs-update-appointment.service.ts @@ -0,0 +1,10 @@ +import { Injectable } from '@nestjs/common'; +import { AppointmentDatabaseRepository } from '../../../../../../core/domains/appointment/repositories/database-repository'; +import { UpdateAppointmentService } from '../../../../../../core/domains/appointment/use-cases/update-single-appointment/update-appointment.service'; + +@Injectable() +export class NestjsUpdateAppointmentService extends UpdateAppointmentService { + constructor(appointmentDatabaseRepository: AppointmentDatabaseRepository) { + super(appointmentDatabaseRepository); + } +} diff --git a/apps/core-rest-api/src/app/adapters/controllers/api/use-cases/appointment/update-appointment/output.dto.ts b/apps/core-rest-api/src/app/adapters/controllers/api/use-cases/appointment/update-appointment/output.dto.ts new file mode 100644 index 0000000..07d4fdf --- /dev/null +++ b/apps/core-rest-api/src/app/adapters/controllers/api/use-cases/appointment/update-appointment/output.dto.ts @@ -0,0 +1,6 @@ +import { IsString } from 'class-validator'; + +export class UpdateAppointmentControllerOutputDto { + @IsString() + message!: string; +} diff --git a/apps/core-rest-api/src/app/adapters/controllers/api/use-cases/appointment/update-appointment/update-appointment.controller.ts b/apps/core-rest-api/src/app/adapters/controllers/api/use-cases/appointment/update-appointment/update-appointment.controller.ts new file mode 100644 index 0000000..c5fbadf --- /dev/null +++ b/apps/core-rest-api/src/app/adapters/controllers/api/use-cases/appointment/update-appointment/update-appointment.controller.ts @@ -0,0 +1,37 @@ +import { BadRequestException, Body, Controller, Param, Patch } from "@nestjs/common"; +import { ApiBearerAuth, ApiTags } from "@nestjs/swagger"; +import { GlobalAppHttpException } from '../../../../../../shared/errors/globalAppHttpException'; +import { UpdateAppointmentControllerBodyParamsInputDto, UpdateAppointmentControllerReqParamsInputDto } from "./input.dto"; +import { NestjsUpdateAppointmentService } from "./nestjs-update-appointment.service"; +import { UpdateAppointmentControllerOutputDto } from "./output.dto"; + + +@ApiTags('Appointment') +@ApiBearerAuth() +@Controller({ + path: 'appointment', +}) +export class UpdateAppointmentController { + constructor(private updateAppointmentService: NestjsUpdateAppointmentService) {} + + @Patch(':id/update') + async execute( + @Param() { id }: UpdateAppointmentControllerReqParamsInputDto, + @Body() updateAppointmentDto: UpdateAppointmentControllerBodyParamsInputDto) + : Promise{ + + try { + const isReqBodyEmpty = Object.keys(updateAppointmentDto).length === 0; + + if (isReqBodyEmpty) { + throw new BadRequestException('Must provide at least one field to update'); + } + + await this.updateAppointmentService.execute({...updateAppointmentDto, id}); + + return { message: 'Appointment updated successfully' }; + } catch (error) { + throw new GlobalAppHttpException(error); + } + } +} From b74420d94d7cb954d33af6d925e3338a2995006c Mon Sep 17 00:00:00 2001 From: luanavfg Date: Thu, 7 Mar 2024 08:21:22 -0300 Subject: [PATCH 4/8] test(CC-132): test update appointment endpoint --- .../http/appointment-client.http | 14 +- .../update-appointment.e2e-spec.ts | 125 ++++++++++++++++++ .../tests/utils/e2e-tests-initial-setup.ts | 17 +++ 3 files changed, 154 insertions(+), 2 deletions(-) create mode 100644 apps/core-rest-api/src/app/adapters/controllers/api/use-cases/appointment/update-appointment/update-appointment.e2e-spec.ts diff --git a/apps/core-rest-api/http/appointment-client.http b/apps/core-rest-api/http/appointment-client.http index 428d4a8..b2d9bd0 100644 --- a/apps/core-rest-api/http/appointment-client.http +++ b/apps/core-rest-api/http/appointment-client.http @@ -1,8 +1,7 @@ @authToken = {{$dotenv AUTH_TOKEN}} @apiKey = {{$dotenv API_KEY}} -# @appointmentDate = {{require "./appointmentDate.js"}} -#### +#### Create Appointment # @name create_appointment POST http://localhost:3333/core/appointment/create HTTP/1.1 @@ -19,3 +18,14 @@ Authorization: Bearer {{authToken}} "cancelled": false, "date": "2024-01-19T08:30:54" } + +### Update Appointment + +# @name update_appointment +PATCH http://localhost:3333/core/appointment/{{$dotenv APPOINTMENT_ID}}/update HTTP/1.1 +Content-Type: application/json +Authorization: Bearer {{authToken}} + +{ + "confirmed": true +} diff --git a/apps/core-rest-api/src/app/adapters/controllers/api/use-cases/appointment/update-appointment/update-appointment.e2e-spec.ts b/apps/core-rest-api/src/app/adapters/controllers/api/use-cases/appointment/update-appointment/update-appointment.e2e-spec.ts new file mode 100644 index 0000000..9a8444c --- /dev/null +++ b/apps/core-rest-api/src/app/adapters/controllers/api/use-cases/appointment/update-appointment/update-appointment.e2e-spec.ts @@ -0,0 +1,125 @@ +import request from 'supertest'; + +import { INestApplication } from '@nestjs/common'; + +import { faker } from '@faker-js/faker'; +import { setupE2ETest } from '../../../../../../../../tests/utils/e2e-tests-initial-setup'; +import { AppointmentEntity } from '../../../../../../core/domains/appointment/entities/appointment/entity'; + +describe('[E2E] - Update Appointment', () => { + let app: INestApplication; + let appointment: AppointmentEntity; + let access_token: string; + let invalid_access_token: string; + + beforeAll(async () => { + const setup = await setupE2ETest(); + app = setup.app; + appointment = setup.appointment + access_token = setup.access_token; + invalid_access_token = setup.invalid_access_token; + }); + + it('[PATCH] - Should successfully update an appointment', async () => { + const updatedAppointmentInfos = { + confirmed: false, + cancelled: true, + }; + + const response = await request(app.getHttpServer()) + .patch(`/appointment/${appointment.id}/update`) + .set('Authorization', `Bearer ${access_token}`) + .send(updatedAppointmentInfos); + + expect(response.statusCode).toBe(200); + expect(response.body.message).toBe('Appointment updated successfully'); + }); + + it('[PATCH] - Should return an error when trying to update an appointment without access_token', async () => { + const updatedAppointmentInfos = { + confirmed: false, + cancelled: true, + }; + + const response = await request(app.getHttpServer()) + .patch(`/appointment/${appointment.id}/update`) + .send(updatedAppointmentInfos); + + expect(response.statusCode).toBe(401); + expect(response.body.message).toBe('Invalid JWT token'); + }); + + it('[PATCH] - Should return an error when trying to update an appointment with invalid access_token', async () => { + const updatedAppointmentInfos = { + confirmed: false, + }; + + const response = await request(app.getHttpServer()) + .patch(`/appointment/${appointment.id}/update`) + .set('Authorization', `Bearer ${invalid_access_token}`) + .send(updatedAppointmentInfos); + + expect(response.statusCode).toBe(401); + expect(response.body.message).toBe('Invalid JWT token'); + }); + + it('[PATCH] - Should return an error when trying to update an appointment with invalid id', async () => { + const updatedAppointmentInfos = { + confirmed: false, + }; + + const response = await request(app.getHttpServer()) + .patch(`/appointment/invalid_id/update`) + .set('Authorization', `Bearer ${access_token}`) + .send(updatedAppointmentInfos); + + expect(response.statusCode).toBe(400); + expect(response.body.message).toEqual(['id must be a UUID']); + }); + + it('[PATCH] - Should return an error when trying to update an appointment with non existent id', async () => { + const nonExistentId = faker.string.uuid(); + + const updatedAppointmentInfos = { + confirmed: false, + }; + + const response = await request(app.getHttpServer()) + .patch(`/appointment/${nonExistentId}/update`) + .set('Authorization', `Bearer ${access_token}`) + .send(updatedAppointmentInfos); + + expect(response.statusCode).toBe(404); + expect(response.body.message).toBe('appointment not found'); + }); + + it('[PATCH] - Should return an error when trying to update an appointment with empty request body', async () => { + const updatedAppointmentInfos = {}; + + const response = await request(app.getHttpServer()) + .patch(`/psychologist/${appointment.id}/update`) + .set('Authorization', `Bearer ${access_token}`) + .send(updatedAppointmentInfos); + + expect(response.statusCode).toBe(400); + expect(response.body.message).toBe('Must provide at least one field to update'); + }); + + it('[PATCH] - Should return an error when trying to update an appointment with an invalid body type params', async () => { + const updatedAppointmentInfos = { + confirmed: 'false', + cancelled: 'true' + }; + + const response = await request(app.getHttpServer()) + .patch(`/psychologist/${appointment.id}/update`) + .set('Authorization', `Bearer ${access_token}`) + .send(updatedAppointmentInfos); + + expect(response.statusCode).toBe(400); + expect(response.body.message).toEqual([ + 'confirmed must be a boolean', + 'cancelled must be a boolean', + ]); + }); +}); diff --git a/apps/core-rest-api/tests/utils/e2e-tests-initial-setup.ts b/apps/core-rest-api/tests/utils/e2e-tests-initial-setup.ts index 5f64468..559447e 100644 --- a/apps/core-rest-api/tests/utils/e2e-tests-initial-setup.ts +++ b/apps/core-rest-api/tests/utils/e2e-tests-initial-setup.ts @@ -8,6 +8,7 @@ import { ApiModule } from '../../src/app/adapters/controllers/api/api.module'; import { PostgreSqlPrismaOrmService } from '../../src/app/adapters/database/infra/prisma/prisma.service'; import { DatabaseRepositoriesModule } from '../../src/app/adapters/database/repositories/repositories.module'; +import { AppointmentFactory } from '../factories/make-appointment'; import { ClinicFactory } from '../factories/make-clinic'; import { PatientFactory } from '../factories/make-patient'; import { PatientAppointmentRegistryFactory } from '../factories/make-patient-appointment-registry'; @@ -31,6 +32,7 @@ export async function setupE2ETest() { const clinicFactory: ClinicFactory = moduleRef.get(ClinicFactory); const psychologistFactory: PsychologistFactory = moduleRef.get(PsychologistFactory); const patientFactory: PatientFactory = moduleRef.get(PatientFactory); + const appointmentFactory: AppointmentFactory = moduleRef.get(AppointmentFactory); const patientAppointmentRegistryFactory: PatientAppointmentRegistryFactory = moduleRef.get(PatientAppointmentRegistryFactory); @@ -75,6 +77,19 @@ export async function setupE2ETest() { clinicId: clinic.id, }); + // Creating an appointment to use in tests + const appointment = await appointmentFactory.makePrismaAppointment({ + psychologistId: psychologist.id, + clinicId: clinic.id, + patientId: patient.id, + date: faker.date.recent({ days: 10 }), + cancelled: false, + confirmationDate: null, + confirmed: true, + online: false, + paymentMethod: undefined + }); + // Creating a patient appointment registry to use in tests const patientAppointmentRegistry = await patientAppointmentRegistryFactory.makePrismaPatientAppointmentRegistry({ @@ -98,6 +113,8 @@ export async function setupE2ETest() { clinic, patientFactory, patient, + appointment, + appointmentFactory, patientAppointmentRegistry, }; } From 7d679ba61ae1971ebda233951524db2673fa1541 Mon Sep 17 00:00:00 2001 From: luanavfg Date: Thu, 7 Mar 2024 08:21:56 -0300 Subject: [PATCH 5/8] adjust(CC-132): add controller in api module --- .../src/app/adapters/controllers/api/api.module.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/apps/core-rest-api/src/app/adapters/controllers/api/api.module.ts b/apps/core-rest-api/src/app/adapters/controllers/api/api.module.ts index e4a1114..9638543 100644 --- a/apps/core-rest-api/src/app/adapters/controllers/api/api.module.ts +++ b/apps/core-rest-api/src/app/adapters/controllers/api/api.module.ts @@ -24,6 +24,8 @@ import { NestjsDeletePsychologistService } from './use-cases/psychologist/delete import { NestjsUpdatePsychologistService } from './use-cases/psychologist/update-psychologist/nestjs-update-psychologist.service'; import { CreateAppointmentController } from './use-cases/appointment/create-appointment/create-appointment.controller'; +import { NestjsUpdateAppointmentService } from './use-cases/appointment/update-appointment/nestjs-update-appointment.service'; +import { UpdateAppointmentController } from './use-cases/appointment/update-appointment/update-appointment.controller'; import { CreateClinicController } from './use-cases/clinic/create-clinic/create-clinic.controller'; import { DeleteClinicController } from './use-cases/clinic/delete-clinic/delete-clinic.controller'; import { UpdateClinicController } from './use-cases/clinic/update-clinic/update-clinic.controller'; @@ -66,6 +68,7 @@ import { DeletePsychologistController } from './use-cases/psychologist/delete-ps DeletePatientAppointmentRegistryController, UpdatePatientAppointmentRegistryController, UpdatePatientController, + UpdateAppointmentController, ], providers: [ BcryptHasherService, @@ -84,6 +87,7 @@ import { DeletePsychologistController } from './use-cases/psychologist/delete-ps NestjsDeletePatientAppointmentRegistryService, NestjsUpdatePatientAppointmentRegistryService, NestjsUpdatePatientService, + NestjsUpdateAppointmentService ], }) export class ApiModule {} From adb2c2a64138e96067181fc91a6a8aa2a7f7e9a8 Mon Sep 17 00:00:00 2001 From: luanavfg Date: Thu, 7 Mar 2024 09:23:25 -0300 Subject: [PATCH 6/8] refactor(CC-132): use function to call endpoint in tests --- .../update-appointment/input.dto.ts | 4 +- .../update-appointment.e2e-spec.ts | 87 +++++++++++++------ 2 files changed, 62 insertions(+), 29 deletions(-) diff --git a/apps/core-rest-api/src/app/adapters/controllers/api/use-cases/appointment/update-appointment/input.dto.ts b/apps/core-rest-api/src/app/adapters/controllers/api/use-cases/appointment/update-appointment/input.dto.ts index 4a1238e..1329e61 100644 --- a/apps/core-rest-api/src/app/adapters/controllers/api/use-cases/appointment/update-appointment/input.dto.ts +++ b/apps/core-rest-api/src/app/adapters/controllers/api/use-cases/appointment/update-appointment/input.dto.ts @@ -1,12 +1,12 @@ import { IsBoolean, IsDate, IsEnum, IsOptional, IsString } from 'class-validator'; import { PaymentMethod } from '../../../../../../core/shared/interfaces/payments'; -export class UpdateAppointmentControllerReqParamsInputDto { +export class UpdateAppointmentControllerParamsInputDto { @IsString() id!: string; } -export class UpdateAppointmentControllerBodyParamsInputDto { +export class UpdateAppointmentControllerBodyInputDto { @IsOptional() @IsDate() date?: Date; diff --git a/apps/core-rest-api/src/app/adapters/controllers/api/use-cases/appointment/update-appointment/update-appointment.e2e-spec.ts b/apps/core-rest-api/src/app/adapters/controllers/api/use-cases/appointment/update-appointment/update-appointment.e2e-spec.ts index 9a8444c..e91e1f0 100644 --- a/apps/core-rest-api/src/app/adapters/controllers/api/use-cases/appointment/update-appointment/update-appointment.e2e-spec.ts +++ b/apps/core-rest-api/src/app/adapters/controllers/api/use-cases/appointment/update-appointment/update-appointment.e2e-spec.ts @@ -4,7 +4,9 @@ import { INestApplication } from '@nestjs/common'; import { faker } from '@faker-js/faker'; import { setupE2ETest } from '../../../../../../../../tests/utils/e2e-tests-initial-setup'; +import { Replace } from '../../../../../../../app/shared/utils'; import { AppointmentEntity } from '../../../../../../core/domains/appointment/entities/appointment/entity'; +import { UpdateAppointmentControllerBodyInputDto } from './input.dto'; describe('[E2E] - Update Appointment', () => { let app: INestApplication; @@ -20,16 +22,35 @@ describe('[E2E] - Update Appointment', () => { invalid_access_token = setup.invalid_access_token; }); + type IUpdateAppointmentProps = Replace< + UpdateAppointmentControllerBodyInputDto, + { confirmed?: string | boolean; cancelled?: string | boolean } + >; + + async function updateAppointmentWithoutAcessToken (appointmentId: string, updatedAppointmentInfos: IUpdateAppointmentProps) { + const response = await request(app.getHttpServer()) + .patch(`/appointment/${appointmentId}/update`) + .send(updatedAppointmentInfos) + + return response + } + + async function updateAppointmentWithAcessToken (appointmentId: string, updatedAppointmentInfos: IUpdateAppointmentProps, accessToken?: string) { + const response = await request(app.getHttpServer()) + .patch(`/appointment/${appointmentId}/update`) + .set('Authorization', `Bearer ${accessToken}`) + .send(updatedAppointmentInfos); + + return response + } + it('[PATCH] - Should successfully update an appointment', async () => { const updatedAppointmentInfos = { confirmed: false, cancelled: true, }; - const response = await request(app.getHttpServer()) - .patch(`/appointment/${appointment.id}/update`) - .set('Authorization', `Bearer ${access_token}`) - .send(updatedAppointmentInfos); + const response = await updateAppointmentWithAcessToken(appointment.id, updatedAppointmentInfos, access_token) expect(response.statusCode).toBe(200); expect(response.body.message).toBe('Appointment updated successfully'); @@ -41,9 +62,11 @@ describe('[E2E] - Update Appointment', () => { cancelled: true, }; - const response = await request(app.getHttpServer()) - .patch(`/appointment/${appointment.id}/update`) - .send(updatedAppointmentInfos); + // const response = await request(app.getHttpServer()) + // .patch(`/appointment/${appointment.id}/update`) + // .send(updatedAppointmentInfos); + + const response = await updateAppointmentWithoutAcessToken(appointment.id, updatedAppointmentInfos) expect(response.statusCode).toBe(401); expect(response.body.message).toBe('Invalid JWT token'); @@ -54,10 +77,12 @@ describe('[E2E] - Update Appointment', () => { confirmed: false, }; - const response = await request(app.getHttpServer()) - .patch(`/appointment/${appointment.id}/update`) - .set('Authorization', `Bearer ${invalid_access_token}`) - .send(updatedAppointmentInfos); + // const response = await request(app.getHttpServer()) + // .patch(`/appointment/${appointment.id}/update`) + // .set('Authorization', `Bearer ${invalid_access_token}`) + // .send(updatedAppointmentInfos); + + const response = await updateAppointmentWithAcessToken(appointment.id, updatedAppointmentInfos, invalid_access_token) expect(response.statusCode).toBe(401); expect(response.body.message).toBe('Invalid JWT token'); @@ -68,10 +93,12 @@ describe('[E2E] - Update Appointment', () => { confirmed: false, }; - const response = await request(app.getHttpServer()) - .patch(`/appointment/invalid_id/update`) - .set('Authorization', `Bearer ${access_token}`) - .send(updatedAppointmentInfos); + // const response = await request(app.getHttpServer()) + // .patch(`/appointment/invalid_id/update`) + // .set('Authorization', `Bearer ${access_token}`) + // .send(updatedAppointmentInfos); + + const response = await updateAppointmentWithAcessToken(appointment.id, updatedAppointmentInfos, access_token) expect(response.statusCode).toBe(400); expect(response.body.message).toEqual(['id must be a UUID']); @@ -84,10 +111,12 @@ describe('[E2E] - Update Appointment', () => { confirmed: false, }; - const response = await request(app.getHttpServer()) - .patch(`/appointment/${nonExistentId}/update`) - .set('Authorization', `Bearer ${access_token}`) - .send(updatedAppointmentInfos); + // const response = await request(app.getHttpServer()) + // .patch(`/appointment/${nonExistentId}/update`) + // .set('Authorization', `Bearer ${access_token}`) + // .send(updatedAppointmentInfos); + + const response = await updateAppointmentWithAcessToken(nonExistentId, updatedAppointmentInfos, access_token) expect(response.statusCode).toBe(404); expect(response.body.message).toBe('appointment not found'); @@ -96,10 +125,12 @@ describe('[E2E] - Update Appointment', () => { it('[PATCH] - Should return an error when trying to update an appointment with empty request body', async () => { const updatedAppointmentInfos = {}; - const response = await request(app.getHttpServer()) - .patch(`/psychologist/${appointment.id}/update`) - .set('Authorization', `Bearer ${access_token}`) - .send(updatedAppointmentInfos); + // const response = await request(app.getHttpServer()) + // .patch(`/psychologist/${appointment.id}/update`) + // .set('Authorization', `Bearer ${access_token}`) + // .send(updatedAppointmentInfos); + + const response = await updateAppointmentWithAcessToken(appointment.id, updatedAppointmentInfos, access_token) expect(response.statusCode).toBe(400); expect(response.body.message).toBe('Must provide at least one field to update'); @@ -111,10 +142,12 @@ describe('[E2E] - Update Appointment', () => { cancelled: 'true' }; - const response = await request(app.getHttpServer()) - .patch(`/psychologist/${appointment.id}/update`) - .set('Authorization', `Bearer ${access_token}`) - .send(updatedAppointmentInfos); + // const response = await request(app.getHttpServer()) + // .patch(`/psychologist/${appointment.id}/update`) + // .set('Authorization', `Bearer ${access_token}`) + // .send(updatedAppointmentInfos); + + const response = await updateAppointmentWithAcessToken(appointment.id, updatedAppointmentInfos, access_token) expect(response.statusCode).toBe(400); expect(response.body.message).toEqual([ From 4f660e6250ab185b59ae36201423b41832cb6438 Mon Sep 17 00:00:00 2001 From: luanavfg Date: Mon, 11 Mar 2024 09:26:19 -0300 Subject: [PATCH 7/8] adjust(CC-132): rename input and output dtos --- .../update-appointment/update-appointment.controller.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/apps/core-rest-api/src/app/adapters/controllers/api/use-cases/appointment/update-appointment/update-appointment.controller.ts b/apps/core-rest-api/src/app/adapters/controllers/api/use-cases/appointment/update-appointment/update-appointment.controller.ts index c5fbadf..7ce46e3 100644 --- a/apps/core-rest-api/src/app/adapters/controllers/api/use-cases/appointment/update-appointment/update-appointment.controller.ts +++ b/apps/core-rest-api/src/app/adapters/controllers/api/use-cases/appointment/update-appointment/update-appointment.controller.ts @@ -1,7 +1,7 @@ import { BadRequestException, Body, Controller, Param, Patch } from "@nestjs/common"; import { ApiBearerAuth, ApiTags } from "@nestjs/swagger"; import { GlobalAppHttpException } from '../../../../../../shared/errors/globalAppHttpException'; -import { UpdateAppointmentControllerBodyParamsInputDto, UpdateAppointmentControllerReqParamsInputDto } from "./input.dto"; +import { UpdateAppointmentControllerBodyInputDto, UpdateAppointmentControllerParamsInputDto } from "./input.dto"; import { NestjsUpdateAppointmentService } from "./nestjs-update-appointment.service"; import { UpdateAppointmentControllerOutputDto } from "./output.dto"; @@ -16,10 +16,9 @@ export class UpdateAppointmentController { @Patch(':id/update') async execute( - @Param() { id }: UpdateAppointmentControllerReqParamsInputDto, - @Body() updateAppointmentDto: UpdateAppointmentControllerBodyParamsInputDto) + @Param() { id }: UpdateAppointmentControllerParamsInputDto, + @Body() updateAppointmentDto: UpdateAppointmentControllerBodyInputDto) : Promise{ - try { const isReqBodyEmpty = Object.keys(updateAppointmentDto).length === 0; From 913384ee03d2ac2e2d94ec0e3013d031b31abece Mon Sep 17 00:00:00 2001 From: luanavfg Date: Mon, 11 Mar 2024 09:27:55 -0300 Subject: [PATCH 8/8] test(CC-132): adjust update appointment e2e test and remove comments --- .../update-appointment.e2e-spec.ts | 46 +++---------------- 1 file changed, 7 insertions(+), 39 deletions(-) diff --git a/apps/core-rest-api/src/app/adapters/controllers/api/use-cases/appointment/update-appointment/update-appointment.e2e-spec.ts b/apps/core-rest-api/src/app/adapters/controllers/api/use-cases/appointment/update-appointment/update-appointment.e2e-spec.ts index e91e1f0..71eecf3 100644 --- a/apps/core-rest-api/src/app/adapters/controllers/api/use-cases/appointment/update-appointment/update-appointment.e2e-spec.ts +++ b/apps/core-rest-api/src/app/adapters/controllers/api/use-cases/appointment/update-appointment/update-appointment.e2e-spec.ts @@ -1,8 +1,6 @@ -import request from 'supertest'; - -import { INestApplication } from '@nestjs/common'; - import { faker } from '@faker-js/faker'; +import { INestApplication } from '@nestjs/common'; +import request from 'supertest'; import { setupE2ETest } from '../../../../../../../../tests/utils/e2e-tests-initial-setup'; import { Replace } from '../../../../../../../app/shared/utils'; import { AppointmentEntity } from '../../../../../../core/domains/appointment/entities/appointment/entity'; @@ -62,10 +60,6 @@ describe('[E2E] - Update Appointment', () => { cancelled: true, }; - // const response = await request(app.getHttpServer()) - // .patch(`/appointment/${appointment.id}/update`) - // .send(updatedAppointmentInfos); - const response = await updateAppointmentWithoutAcessToken(appointment.id, updatedAppointmentInfos) expect(response.statusCode).toBe(401); @@ -77,11 +71,6 @@ describe('[E2E] - Update Appointment', () => { confirmed: false, }; - // const response = await request(app.getHttpServer()) - // .patch(`/appointment/${appointment.id}/update`) - // .set('Authorization', `Bearer ${invalid_access_token}`) - // .send(updatedAppointmentInfos); - const response = await updateAppointmentWithAcessToken(appointment.id, updatedAppointmentInfos, invalid_access_token) expect(response.statusCode).toBe(401); @@ -92,16 +81,10 @@ describe('[E2E] - Update Appointment', () => { const updatedAppointmentInfos = { confirmed: false, }; + const response = await updateAppointmentWithAcessToken('invalid_id', updatedAppointmentInfos, access_token) - // const response = await request(app.getHttpServer()) - // .patch(`/appointment/invalid_id/update`) - // .set('Authorization', `Bearer ${access_token}`) - // .send(updatedAppointmentInfos); - - const response = await updateAppointmentWithAcessToken(appointment.id, updatedAppointmentInfos, access_token) - - expect(response.statusCode).toBe(400); - expect(response.body.message).toEqual(['id must be a UUID']); + expect(response.statusCode).toBe(404); + expect(response.body.message).toEqual('appointment not found'); }); it('[PATCH] - Should return an error when trying to update an appointment with non existent id', async () => { @@ -111,11 +94,6 @@ describe('[E2E] - Update Appointment', () => { confirmed: false, }; - // const response = await request(app.getHttpServer()) - // .patch(`/appointment/${nonExistentId}/update`) - // .set('Authorization', `Bearer ${access_token}`) - // .send(updatedAppointmentInfos); - const response = await updateAppointmentWithAcessToken(nonExistentId, updatedAppointmentInfos, access_token) expect(response.statusCode).toBe(404); @@ -125,11 +103,6 @@ describe('[E2E] - Update Appointment', () => { it('[PATCH] - Should return an error when trying to update an appointment with empty request body', async () => { const updatedAppointmentInfos = {}; - // const response = await request(app.getHttpServer()) - // .patch(`/psychologist/${appointment.id}/update`) - // .set('Authorization', `Bearer ${access_token}`) - // .send(updatedAppointmentInfos); - const response = await updateAppointmentWithAcessToken(appointment.id, updatedAppointmentInfos, access_token) expect(response.statusCode).toBe(400); @@ -142,17 +115,12 @@ describe('[E2E] - Update Appointment', () => { cancelled: 'true' }; - // const response = await request(app.getHttpServer()) - // .patch(`/psychologist/${appointment.id}/update`) - // .set('Authorization', `Bearer ${access_token}`) - // .send(updatedAppointmentInfos); - const response = await updateAppointmentWithAcessToken(appointment.id, updatedAppointmentInfos, access_token) expect(response.statusCode).toBe(400); expect(response.body.message).toEqual([ - 'confirmed must be a boolean', - 'cancelled must be a boolean', + 'confirmed must be a boolean value', + 'cancelled must be a boolean value', ]); }); });