diff --git a/src/application/controllers/guardian/index.ts b/src/application/controllers/guardian/index.ts new file mode 100644 index 00000000..36269a0e --- /dev/null +++ b/src/application/controllers/guardian/index.ts @@ -0,0 +1 @@ +export * from './update-guardian' diff --git a/src/application/controllers/guardian/update-guardian.ts b/src/application/controllers/guardian/update-guardian.ts new file mode 100644 index 00000000..17dde1b1 --- /dev/null +++ b/src/application/controllers/guardian/update-guardian.ts @@ -0,0 +1,45 @@ +import { badRequest, notAcceptable, serverError, success, type HttpRequest, type HttpResponse } from '@/application/helpers' +import { type Controller, type Validation } from '@/application/protocols' +import { type UpdateGuardian } from '@/domain/use-cases' + +export class UpdateGuardianController implements Controller { + private readonly validation: Validation + private readonly UpdateGuardian: UpdateGuardian + + constructor ({ validation, updateGuardian }: UpdateGuardianController.Dependencies) { + this.validation = validation + this.UpdateGuardian = updateGuardian + } + + async handle (httpRequest: HttpRequest): Promise { + try { + const error = this.validation.validate({ ...httpRequest.body, ...httpRequest.params }) + if (error) { + return badRequest(error) + } + const { guardianId } = httpRequest.params + const image = httpRequest.file ?? null + const updateData = { ...httpRequest.body, guardianId, image } + const result = await this.UpdateGuardian.update(updateData) + if (!result.isSuccess) { + return notAcceptable(result.error as Error) + } + return success({ + id: result.data?.id, + firstName: result.data?.firstName, + lastName: result.data?.lastName, + phone: result.data?.phone, + image: result.data?.image + }) + } catch (error) { + return serverError(error as Error) + } + } +} + +export namespace UpdateGuardianController { + export interface Dependencies { + validation: Validation + updateGuardian: UpdateGuardian + } +} diff --git a/src/application/controllers/index.ts b/src/application/controllers/index.ts index 23f30a88..de3b9278 100644 --- a/src/application/controllers/index.ts +++ b/src/application/controllers/index.ts @@ -9,3 +9,4 @@ export * from './email-confirmation' export * from './scheduler' export * from './tasks' export * from './settings' +export * from './guardian' diff --git a/src/data/protocols/db/guardian/index.ts b/src/data/protocols/db/guardian/index.ts index a7b35cd9..c8024d28 100644 --- a/src/data/protocols/db/guardian/index.ts +++ b/src/data/protocols/db/guardian/index.ts @@ -8,3 +8,4 @@ export * from './update-guardian-password-repository' export * from './save-token-repository' export * from './update-email-confirmation-repository' export * from './update-guardian-image-repository' +export * from './update-guardian-repository' diff --git a/src/data/protocols/db/guardian/load-guardian-by-id-repository.ts b/src/data/protocols/db/guardian/load-guardian-by-id-repository.ts index ff6c5a08..54908441 100644 --- a/src/data/protocols/db/guardian/load-guardian-by-id-repository.ts +++ b/src/data/protocols/db/guardian/load-guardian-by-id-repository.ts @@ -16,5 +16,6 @@ export namespace LoadGuardianByIdRepository { verificationToken: string verificationTokenCreatedAt: Date emailConfirmation: boolean + image: string } | null } diff --git a/src/data/protocols/db/guardian/update-guardian-repository.ts b/src/data/protocols/db/guardian/update-guardian-repository.ts new file mode 100644 index 00000000..2a193138 --- /dev/null +++ b/src/data/protocols/db/guardian/update-guardian-repository.ts @@ -0,0 +1,22 @@ +export interface UpdateGuardianRepository { + update: (params: UpdateGuardianRepository.Params) => Promise +} + +export namespace UpdateGuardianRepository { + export type Params = { + guardianId: string + firstName?: string + lastName?: string + phone?: string + image?: string + } + + export type Result = { + id: string + firstName: string + lastName: string + phone: string + image: string + } | undefined + +} diff --git a/src/data/use-cases/guardian/db-update-guardian.ts b/src/data/use-cases/guardian/db-update-guardian.ts new file mode 100644 index 00000000..9165fac7 --- /dev/null +++ b/src/data/use-cases/guardian/db-update-guardian.ts @@ -0,0 +1,55 @@ +import { NotAcceptableError } from '@/application/errors' +import { type DeleteFileStorage, type FileStorage, type LoadGuardianByIdRepository, type UpdateGuardianRepository } from '@/data/protocols' +import { type UpdateGuardian } from '@/domain/use-cases' + +export class DbUpdateGuardian implements UpdateGuardian { + private readonly guardianRepository: LoadGuardianByIdRepository & UpdateGuardianRepository + private readonly fileStorage: FileStorage & DeleteFileStorage + + constructor ({ guardianRepository, fileStorage }: UpdateGuardian.Dependencies) { + this.guardianRepository = guardianRepository + this.fileStorage = fileStorage + } + + async update (guardianData: UpdateGuardian.Params): Promise { + const guardian = await this.guardianRepository.loadById(guardianData.guardianId) + if (!guardian) { + return { + isSuccess: false, + error: new NotAcceptableError('userId') + } + } + + let finalImage: string = '' + if (guardianData.image) { + const currentImage = guardian.image + const updatedImage = await this.fileStorage.save({ file: guardianData.image, fileName: `images/guardian-${guardian?.id}-${Math.trunc(Date.now() / 1000)}` }) + if (!updatedImage) { + return { + isSuccess: false, + error: new NotAcceptableError('update image failed') + } + } + if (currentImage) { + await this.fileStorage.delete({ fileUrlOrPath: currentImage }) + } + finalImage = updatedImage + } + if (!guardianData.image) { + finalImage = guardian.image + } + + const guardianUpdateResult = await this.guardianRepository.update({ + guardianId: guardian.id, + firstName: guardianData.firstName ? guardianData.firstName : guardian.firstName, + lastName: guardianData.lastName ? guardianData.lastName : guardian.lastName, + phone: guardianData.phone ? guardianData.phone : guardian.phone, + image: finalImage + }) + + return { + isSuccess: true, + data: guardianUpdateResult + } + } +} diff --git a/src/data/use-cases/guardian/index.ts b/src/data/use-cases/guardian/index.ts new file mode 100644 index 00000000..a495c45b --- /dev/null +++ b/src/data/use-cases/guardian/index.ts @@ -0,0 +1 @@ +export * from './db-update-guardian' diff --git a/src/data/use-cases/index.ts b/src/data/use-cases/index.ts index 82f1a712..42900d08 100644 --- a/src/data/use-cases/index.ts +++ b/src/data/use-cases/index.ts @@ -11,3 +11,4 @@ export * from './db-send-email' export * from './scheduler' export * from './tasks' export * from './settings' +export * from './guardian' diff --git a/src/domain/use-cases/guardian/index.ts b/src/domain/use-cases/guardian/index.ts new file mode 100644 index 00000000..36269a0e --- /dev/null +++ b/src/domain/use-cases/guardian/index.ts @@ -0,0 +1 @@ +export * from './update-guardian' diff --git a/src/domain/use-cases/guardian/update-guardian.ts b/src/domain/use-cases/guardian/update-guardian.ts new file mode 100644 index 00000000..2aa099f7 --- /dev/null +++ b/src/domain/use-cases/guardian/update-guardian.ts @@ -0,0 +1,26 @@ +import { type DeleteFileStorage, type FileStorage, type LoadGuardianByIdRepository, type UpdateGuardianRepository } from '@/data/protocols' + +export interface UpdateGuardian { + update: (params: UpdateGuardian.Params) => Promise +} + +export namespace UpdateGuardian { + export type Params = { + guardianId: string + firstName?: string + lastName?: string + phone?: string + image?: Buffer | null + } + + export type Result = { + isSuccess: boolean + error?: Error + data?: UpdateGuardianRepository.Result + } + + export type Dependencies = { + guardianRepository: LoadGuardianByIdRepository & UpdateGuardianRepository + fileStorage: FileStorage & DeleteFileStorage + } +} diff --git a/src/domain/use-cases/index.ts b/src/domain/use-cases/index.ts index 98ae3629..b12e6419 100644 --- a/src/domain/use-cases/index.ts +++ b/src/domain/use-cases/index.ts @@ -11,3 +11,4 @@ export * from './send-email' export * from './scheduler' export * from './tasks' export * from './settings' +export * from './guardian' diff --git a/src/infra/repos/postgresql/guardian-account-repository.ts b/src/infra/repos/postgresql/guardian-account-repository.ts index b86bcfb5..6e293f98 100644 --- a/src/infra/repos/postgresql/guardian-account-repository.ts +++ b/src/infra/repos/postgresql/guardian-account-repository.ts @@ -9,6 +9,7 @@ import { type UpdateVerificationTokenRepository, type UpdateEmailConfirmationRepository } from '@/data/protocols' +import { type UpdateGuardianRepository } from '@/data/protocols/db/guardian/update-guardian-repository' export class GuardianAccountRepository implements AddGuardianRepository, LoadGuardianByEmailRepository, @@ -17,7 +18,8 @@ implements AddGuardianRepository, LoadGuardianByEmailRepository, UpdateGuardianPasswordRepository, UpdateVerificationTokenRepository, UpdateEmailConfirmationRepository, - UpdateGuardianImageRepository { + UpdateGuardianImageRepository, + UpdateGuardianRepository { async add ( guardianData: AddGuardianRepository.Params ): Promise { @@ -140,17 +142,43 @@ implements AddGuardianRepository, LoadGuardianByEmailRepository, } async updateImage (params: UpdateGuardianImageRepository.Params): Promise { - const { guardianId, image } = params - const result = await db.guardian.update({ - where: { id: guardianId }, - data: { image }, - omit: { - password: true, - verificationToken: true, - verificationTokenCreatedAt: true - } - }) + try { + const { guardianId, image } = params + const result = await db.guardian.update({ + where: { id: guardianId }, + data: { image }, + omit: { + password: true, + verificationToken: true, + verificationTokenCreatedAt: true, + emailConfirmation: true, + accessToken: true + } + }) + return result + } catch (error) { + return undefined + } + } - return result + async update (params: UpdateGuardianRepository.Params): Promise { + try { + const { guardianId, ...updateData } = params + const result = await db.guardian.update({ + where: { id: guardianId }, + data: updateData, + omit: { + password: true, + verificationToken: true, + verificationTokenCreatedAt: true, + accessToken: true, + email: true, + emailConfirmation: true + } + }) + return result + } catch (error) { + return undefined + } } } diff --git a/src/main/docs/paths.ts b/src/main/docs/paths.ts index 5ff48e24..26958b5e 100644 --- a/src/main/docs/paths.ts +++ b/src/main/docs/paths.ts @@ -28,7 +28,8 @@ import { loadPetByIdPath, loadNextTasksByPetIdPath, loadPreviousTasksByPetIdPath, - loadNextTasksByPetIdAndTagIdPath + loadNextTasksByPetIdAndTagIdPath, + updateGuardianPath } from './paths/' export default { @@ -37,6 +38,7 @@ export default { '/forget-password': forgetPasswordPath, '/guardian/change-password': changePasswordPath, '/guardian/email-confirmation/{userId}': emailConfirmationPath, + '/guardian/{guardianId}': updateGuardianPath, '/waiting-code': waitingCodePath, '/pet': { ...petRegistryPath, ...loadPetsPath }, '/pet/{petId}': { ...loadPetByIdPath, ...updatePetPath, ...deletePetPath }, diff --git a/src/main/docs/paths/guardian/index.ts b/src/main/docs/paths/guardian/index.ts new file mode 100644 index 00000000..04c6b43d --- /dev/null +++ b/src/main/docs/paths/guardian/index.ts @@ -0,0 +1 @@ +export * from './update-guardian-path' diff --git a/src/main/docs/paths/guardian/update-guardian-path.ts b/src/main/docs/paths/guardian/update-guardian-path.ts new file mode 100644 index 00000000..85a4591a --- /dev/null +++ b/src/main/docs/paths/guardian/update-guardian-path.ts @@ -0,0 +1,42 @@ +import { DocBuilder } from '../../utils/doc-builder' + +export const updateGuardianPath = DocBuilder.putBuilder() + .addTags(['guardian']) + .addSummary('Update an existing guardian') + .addJwtAuthSecurity() + .addPathParameter('guardianId', 'Guardian ID', 'string') + .addMultipartFormDataBody({ + type: 'object', + properties: { + firstName: { + type: 'string', + example: 'John' + }, + lastName: { + type: 'string', + example: 'Doe' + }, + phone: { + type: 'string', + example: '11987654321' + }, + image: { + type: 'string', + format: 'binary' + } + } + }) + .addResponse(200, { + description: 'Success', + content: { + 'application/json': { + schema: { + $ref: '#/schemas/guardian' + } + } + } + }) + .addBadRequestResponse() + .addNotAcceptableResponse() + .addServerErrorResponse() + .build() diff --git a/src/main/docs/paths/index.ts b/src/main/docs/paths/index.ts index c715c1ad..873e2a91 100644 --- a/src/main/docs/paths/index.ts +++ b/src/main/docs/paths/index.ts @@ -13,3 +13,4 @@ export * from './email-confirmation-path' export * from './scheduler' export * from './tasks' export * from './settings' +export * from './guardian' diff --git a/src/main/factories/controllers/guardian/index.ts b/src/main/factories/controllers/guardian/index.ts new file mode 100644 index 00000000..55344c6c --- /dev/null +++ b/src/main/factories/controllers/guardian/index.ts @@ -0,0 +1 @@ +export * from './update-guardian-factory' diff --git a/src/main/factories/controllers/guardian/update-guardian-factory.ts b/src/main/factories/controllers/guardian/update-guardian-factory.ts new file mode 100644 index 00000000..30f15806 --- /dev/null +++ b/src/main/factories/controllers/guardian/update-guardian-factory.ts @@ -0,0 +1,19 @@ +import { UpdateGuardianController } from '@/application/controllers' +import { type Controller } from '@/application/protocols' +import { makeDbUpdateGuardian } from '../../usecases' +import { makeUpdateGuardianValidation } from '../../validations' +import { LoggerPgRepository } from '@/infra/repos/postgresql' +import { DevLoggerControllerDecorator, LoggerControllerDecorator } from '@/main/decorators' + +export const makeUpdateGuardianController = (): Controller => { + const updateGuardian = makeDbUpdateGuardian() + const validation = makeUpdateGuardianValidation() + const dependencies: UpdateGuardianController.Dependencies = { + updateGuardian, + validation + } + const updateGuardianController = new UpdateGuardianController(dependencies) + const loggerPgRepository = new LoggerPgRepository() + const loggerControllerDecorator = new LoggerControllerDecorator(updateGuardianController, loggerPgRepository) + return new DevLoggerControllerDecorator(loggerControllerDecorator) +} diff --git a/src/main/factories/controllers/index.ts b/src/main/factories/controllers/index.ts index 03d24f50..6f9bfa22 100644 --- a/src/main/factories/controllers/index.ts +++ b/src/main/factories/controllers/index.ts @@ -9,3 +9,4 @@ export * from './email-confirmation-factory' export * from './scheduler' export * from './tasks' export * from './settings' +export * from './guardian' diff --git a/src/main/factories/usecases/guardian/db-update-guardian-factory.ts b/src/main/factories/usecases/guardian/db-update-guardian-factory.ts new file mode 100644 index 00000000..44e7f47a --- /dev/null +++ b/src/main/factories/usecases/guardian/db-update-guardian-factory.ts @@ -0,0 +1,15 @@ +import { DbUpdateGuardian } from '@/data/use-cases' +import { type UpdateGuardian } from '@/domain/use-cases' +import { FirebaseStorageAdapter } from '@/infra/repos/firebase' +import { GuardianAccountRepository } from '@/infra/repos/postgresql' +import env from '@/main/config/env' + +export const makeDbUpdateGuardian = (): UpdateGuardian => { + const guardianRepository = new GuardianAccountRepository() + const fileStorage = new FirebaseStorageAdapter(env.firebase.projectId, env.firebase.storageBucket, env.firebase.serviceAccount) + const dependencies: UpdateGuardian.Dependencies = { + guardianRepository, + fileStorage + } + return new DbUpdateGuardian(dependencies) +} diff --git a/src/main/factories/usecases/guardian/index.ts b/src/main/factories/usecases/guardian/index.ts new file mode 100644 index 00000000..9ab66515 --- /dev/null +++ b/src/main/factories/usecases/guardian/index.ts @@ -0,0 +1 @@ +export * from './db-update-guardian-factory' diff --git a/src/main/factories/usecases/index.ts b/src/main/factories/usecases/index.ts index 782c9e2f..65ddf602 100644 --- a/src/main/factories/usecases/index.ts +++ b/src/main/factories/usecases/index.ts @@ -11,3 +11,4 @@ export * from './db-send-email-factory' export * from './scheduler' export * from './tasks' export * from './settings' +export * from './guardian' diff --git a/src/main/factories/validations/guardian/index.ts b/src/main/factories/validations/guardian/index.ts new file mode 100644 index 00000000..55344c6c --- /dev/null +++ b/src/main/factories/validations/guardian/index.ts @@ -0,0 +1 @@ +export * from './update-guardian-factory' diff --git a/src/main/factories/validations/guardian/update-guardian-factory.ts b/src/main/factories/validations/guardian/update-guardian-factory.ts new file mode 100644 index 00000000..0ade181c --- /dev/null +++ b/src/main/factories/validations/guardian/update-guardian-factory.ts @@ -0,0 +1,13 @@ +import { type Validation } from '@/application/protocols' +import { NameValidation, OptionalFieldValidation, PhoneValidation, ValidationComposite } from '@/application/validation' +import { NameValidatorAdapter, PhoneValidatorAdapter } from '@/infra/validators' + +export const makeUpdateGuardianValidation = (): ValidationComposite => { + const validations: Validation[] = [] + + validations.push(new OptionalFieldValidation('firstName', new NameValidation('firstName', new NameValidatorAdapter()))) + validations.push(new OptionalFieldValidation('lastName', new NameValidation('lastName', new NameValidatorAdapter()))) + validations.push(new OptionalFieldValidation('phone', new PhoneValidation('phone', new PhoneValidatorAdapter()))) + + return new ValidationComposite(validations) +} diff --git a/src/main/factories/validations/index.ts b/src/main/factories/validations/index.ts index 1bf94e87..d6af77cb 100644 --- a/src/main/factories/validations/index.ts +++ b/src/main/factories/validations/index.ts @@ -5,3 +5,4 @@ export * from './change-password-validation-factory' export * from './waiting-code-validation-factory' export * from './pet' export * from './scheduler' +export * from './guardian' diff --git a/src/main/routes/guardian-routes.ts b/src/main/routes/guardian-routes.ts new file mode 100644 index 00000000..2f80fac6 --- /dev/null +++ b/src/main/routes/guardian-routes.ts @@ -0,0 +1,8 @@ +import { type Router } from 'express' +import { accountConfirmation, auth, upload } from '../middlewares' +import { adaptRoute } from '../adapters' +import { makeUpdateGuardianController } from '../factories' + +export default (router: Router): void => { + router.put('/guardian/:guardianId', auth, accountConfirmation, upload, adaptRoute(makeUpdateGuardianController())) +} diff --git a/tests/src/application/controllers/guardian/update-guardian.spec.ts b/tests/src/application/controllers/guardian/update-guardian.spec.ts new file mode 100644 index 00000000..e91f4d18 --- /dev/null +++ b/tests/src/application/controllers/guardian/update-guardian.spec.ts @@ -0,0 +1,108 @@ +import { UpdateGuardianController } from '@/application/controllers' +import { InvalidParamError } from '@/application/errors' +import { badRequest, notAcceptable, success } from '@/application/helpers' +import { type Validation } from '@/application/protocols' +import { type UpdateGuardian } from '@/domain/use-cases' +import { makeFakeServerError, makeFakeUpdateGuardianRequest, makeFakeUpdateGuardianUseCase, makeFakeValidation, mockFakeGuardianUpdated } from '@/tests/utils' + +interface SutTypes { + sut: UpdateGuardianController + updateGuardianStub: UpdateGuardian + validationStub: Validation + +} + +const makeSut = (): SutTypes => { + const updateGuardianStub = makeFakeUpdateGuardianUseCase() + const validationStub = makeFakeValidation() + const dependencies: UpdateGuardianController.Dependencies = { + updateGuardian: updateGuardianStub, + validation: validationStub + } + const sut = new UpdateGuardianController(dependencies) + return { + sut, + updateGuardianStub, + validationStub + } +} + +describe('UpdateGuardian Controller', () => { + const httpRequest = makeFakeUpdateGuardianRequest() + + describe('UpdateGuardian', () => { + it('Should return 406 (NotAcceptable) if invalid data is provided', async () => { + const { sut, updateGuardianStub } = makeSut() + jest.spyOn(updateGuardianStub, 'update').mockResolvedValue({ + isSuccess: false, + error: new InvalidParamError('any_field') + }) + const httpResponse = await sut.handle(httpRequest) + expect(httpResponse).toEqual(notAcceptable(new InvalidParamError('any_field'))) + }) + + it('Should return 500 (ServerError) if update throws', async () => { + const { sut, updateGuardianStub } = makeSut() + jest.spyOn(updateGuardianStub, 'update').mockRejectedValue(new Error()) + const promise = await sut.handle(httpRequest) + expect(promise).toEqual(makeFakeServerError()) + }) + + it('Should call update with correct values', async () => { + const { sut, updateGuardianStub } = makeSut() + const updateSpy = jest.spyOn(updateGuardianStub, 'update') + await sut.handle(httpRequest) + expect(updateSpy).toHaveBeenCalledWith({ + guardianId: httpRequest.params.guardianId, + firstName: httpRequest.body.firstName, + lastName: httpRequest.body.lastName, + phone: httpRequest.body.phone, + image: httpRequest.file + }) + }) + }) + + describe('Validations', () => { + it('Should return 400 (BadRequest) if Validation returns an error', async () => { + const { sut, validationStub } = makeSut() + jest.spyOn(validationStub, 'validate').mockReturnValueOnce(new Error()) + + const httpResponse = await sut.handle(httpRequest) + + expect(httpResponse).toEqual(badRequest(new Error())) + }) + + it('Should call Validation with correct values', async () => { + const { sut, validationStub } = makeSut() + const validateSpy = jest.spyOn(validationStub, 'validate') + + await sut.handle(httpRequest) + + expect(validateSpy).toHaveBeenCalledWith({ ...httpRequest.body, ...httpRequest.params }) + }) + + it('should return 200 (success) if empty data is provided', async () => { + const { sut } = makeSut() + const { body, ...httpRequestWithoutBody } = { ...httpRequest } + + const httpResponse = await sut.handle(httpRequestWithoutBody) + + expect(httpResponse).toEqual(success({ ...mockFakeGuardianUpdated(), image: '' })) + }) + + it('should return 200 (success) if valid data is provided', async () => { + const { sut } = makeSut() + + const entries = Object.entries(httpRequest.body) + const f = async (prefix: any, entries: any): Promise => { + for (let i = 0; i < entries.length; i++) { + Object.assign(httpRequest, { body: { ...Object.fromEntries([...prefix, entries[i]]) } }) + const httpResponse = await sut.handle(httpRequest) + expect(httpResponse).toEqual(success(mockFakeGuardianUpdated())) + await f([...prefix, entries[i]], entries.slice(i + 1)) + } + } + await f([], entries) + }) + }) +}) diff --git a/tests/src/data/use-cases/guardian/db-update-guardian.spec.ts b/tests/src/data/use-cases/guardian/db-update-guardian.spec.ts new file mode 100644 index 00000000..d0030274 --- /dev/null +++ b/tests/src/data/use-cases/guardian/db-update-guardian.spec.ts @@ -0,0 +1,89 @@ +import { NotAcceptableError } from '@/application/errors' +import { type DeleteFileStorage, type FileStorage, type LoadGuardianByIdRepository, type UpdateGuardianRepository } from '@/data/protocols' +import { DbUpdateGuardian } from '@/data/use-cases' +import { type UpdateGuardian } from '@/domain/use-cases' +import { makeFakeFileStorage, makeFakeGuardianRepository, mockFakeGuardianUpdated } from '@/tests/utils' + +interface SutTypes { + sut: DbUpdateGuardian + guardianRepositoryStub: LoadGuardianByIdRepository & UpdateGuardianRepository + fileStorageStub: FileStorage & DeleteFileStorage +} + +const makeSut = (): SutTypes => { + const guardianRepositoryStub = makeFakeGuardianRepository() + const fileStorageStub = makeFakeFileStorage() + const dependencies: UpdateGuardian.Dependencies = { + guardianRepository: guardianRepositoryStub, + fileStorage: fileStorageStub + } + const sut = new DbUpdateGuardian(dependencies) + return { + sut, + guardianRepositoryStub, + fileStorageStub + } +} + +describe('DbUpdateGuardian use case', () => { + const params: UpdateGuardian.Params = { + guardianId: 'any_guardian_id', + firstName: 'any_first_name', + lastName: 'any_last_name', + phone: 'any_phone', + image: Buffer.from('any_image') + } + + describe('GuardianRepository', () => { + it('Should call loadById method with correct values', async () => { + const { sut, guardianRepositoryStub } = makeSut() + const loadByIdSpy = jest.spyOn(guardianRepositoryStub, 'loadById') + await sut.update(params) + expect(loadByIdSpy).toHaveBeenCalledWith(params.guardianId) + }) + + it('Should return Not Acceptable Error if incorrect guardianId is provided', async () => { + const { sut, guardianRepositoryStub } = makeSut() + jest.spyOn(guardianRepositoryStub, 'loadById').mockResolvedValueOnce(null) + const result = await sut.update(params) + expect(result).toEqual({ + isSuccess: false, + error: new NotAcceptableError('userId') + }) + }) + + it('Should throw if loadById method throws', async () => { + const { sut, guardianRepositoryStub } = makeSut() + jest.spyOn(guardianRepositoryStub, 'loadById').mockRejectedValue(new Error()) + const promise = sut.update(params) + await expect(promise).rejects.toThrow() + }) + }) + describe('FileStorage', () => { + it('Should call save method with correct values', async () => { + const { sut, fileStorageStub } = makeSut() + const saveSpy = jest.spyOn(fileStorageStub, 'save') + await sut.update(params) + expect(saveSpy).toHaveBeenCalledWith({ + file: params.image, + fileName: `images/guardian-${mockFakeGuardianUpdated()?.id}-${Math.trunc(Date.now() / 1000)}` + }) + }) + + it('Should throw if save method throws', async () => { + const { sut, fileStorageStub } = makeSut() + jest.spyOn(fileStorageStub, 'save').mockRejectedValueOnce(new Error()) + const promise = sut.update(params) + await expect(promise).rejects.toThrow() + }) + + it('Should call delete method with correct values', async () => { + const { sut, fileStorageStub } = makeSut() + const deleteSpy = jest.spyOn(fileStorageStub, 'delete') + await sut.update(params) + expect(deleteSpy).toHaveBeenCalledWith({ + fileUrlOrPath: 'any_image' + }) + }) + }) +}) diff --git a/tests/src/infra/repos/postgresql/guardian-account.spec.ts b/tests/src/infra/repos/postgresql/guardian-account.spec.ts index 37d628ff..dd10a191 100644 --- a/tests/src/infra/repos/postgresql/guardian-account.spec.ts +++ b/tests/src/infra/repos/postgresql/guardian-account.spec.ts @@ -218,4 +218,49 @@ describe('GuardianAccountRepository', () => { expect(response).toBe(true) }) }) + + describe('UpdateImage', () => { + it('Should return a guardian when image is updated', async () => { + const sut = makeSut() + const { id } = await sut.add(input) as any + const response = await sut.updateImage({ guardianId: id, image: 'any_image' }) + expect(response).toEqual({ + id: expect.any(String), + firstName: 'any_first_name', + lastName: 'any_last_name', + phone: 'any_phone', + email: 'any_email@gmail.com', + image: 'any_image' + }) + }) + + it('Should return undefined if a invalid guardian id is provided', async () => { + const sut = makeSut() + const invalidId = 'invalid_id' + const response = await sut.updateImage({ guardianId: invalidId, image: 'any_image' }) + expect(response).toBeUndefined() + }) + }) + + describe('Update', () => { + it('Should return a guardian when update success', async () => { + const sut = makeSut() + const guardian = await sut.add(input) + const updateGuardianInput = { + guardianId: guardian?.id as string, + firstName: input.firstName, + lastName: input.lastName, + phone: input.phone, + image: 'any_image' + } + const response = await sut.update(updateGuardianInput) + expect(response).toEqual({ + id: expect.any(String), + firstName: 'any_first_name', + lastName: 'any_last_name', + phone: 'any_phone', + image: 'any_image' + }) + }) + }) }) diff --git a/tests/src/main/routes/update-guardian-routes.test.ts b/tests/src/main/routes/update-guardian-routes.test.ts new file mode 100644 index 00000000..694f4b88 --- /dev/null +++ b/tests/src/main/routes/update-guardian-routes.test.ts @@ -0,0 +1,72 @@ +import request from 'supertest' +import app from '@/main/config/app' +import { PrismaHelper } from '@/tests/helpers/prisma-helper' + +describe('Update Guardian Routes', () => { + beforeAll(async () => { + await PrismaHelper.connect() + }) + + afterEach(async () => { + await PrismaHelper.clearGuardian() + }) + + afterAll(async () => { + await PrismaHelper.disconnect() + }) + + describe('PUT - /api/guardian/:guardianId route', () => { + it('Should update a guardian', async () => { + const guardian = await PrismaHelper.createGuardian() + + const { body } = await request(app) + .post('/api/login') + .send({ + email: 'johndoe@email.com', + password: 'Test@1234' + }) + + const response = await request(app) + .put(`/api/guardian/${guardian.id}`) + .set('Authorization', body.accessToken) + .field('firstName', 'John Updated') + .field('lastName', 'Doe') + .field('phone', '11987654322') + .attach('image', '') + + expect(response.status).toBe(200) + expect(response.body).toEqual({ + id: guardian.id, + firstName: 'John Updated', + lastName: 'Doe', + phone: '11987654322', + image: '' + + }) + }, 30000) + + it('Should return 400 if no access token is provided', async () => { + await request(app) + .put('/api/guardian/:guardianId') + .set('Authorization', '') + .expect(400) + }) + + it('Should return 406 (NotAcceptable) if invalid guardianId is provided', async () => { + await PrismaHelper.createGuardian() + + const { body } = await request(app) + .post('/api/login') + .send({ + email: 'johndoe@email.com', + password: 'Test@1234' + }) + + const response = await request(app) + .put('/api/guardian/b1e64ea1-0f6f-4cad-b3d6-434468cb2c5d') + .set('Authorization', body.accessToken) + + expect(response.status).toBe(406) + }) + }) +}) diff --git a/tests/utils/mocks/entities.mocks.ts b/tests/utils/mocks/entities.mocks.ts index 8e609b6f..ba5d04c4 100644 --- a/tests/utils/mocks/entities.mocks.ts +++ b/tests/utils/mocks/entities.mocks.ts @@ -11,7 +11,8 @@ import { type LoadPetByGuardianIdRepository, type LoadPetByIdRepository, type DeletePetByIdRepository, - type UpdateGuardianImageRepository + type UpdateGuardianImageRepository, + type UpdateGuardianRepository } from '@/data/protocols' import { type AppointPet } from '@/domain/use-cases' import { type Guardian } from '@/tests/utils/types' @@ -42,7 +43,7 @@ const mockFakeGuardianAdded = (): Exclude => { +const mockFakeGuardianImageUpdated = (): Exclude => { return { id: 'any_id', firstName: 'any_first_name', @@ -53,6 +54,16 @@ const mockFakeGuardianUpdated = (): Exclude => { + return { + id: 'any_id', + firstName: 'any_first_name', + lastName: 'any_last_name', + phone: 'any_phone', + image: '' + } +} + const mockFakePetAdded = (): AddPetRepository.Result => { return { id: 'any_id', @@ -197,6 +208,7 @@ const mockFakeGuardianLoaded = (): Exclude { return { body, params, userId, file } } +const makeFakeUpdateGuardianRequest = (): UpdateGuardianRequest => { + const body = { + firstName: 'any_first_name', + lastName: 'any_last_name', + phone: 'any_phone' + } + const params = { + guardianId: 'any_guardian_id' + } + const file = Buffer.from('any_image') + return { body, params, file } +} + const makeFakeDeletePetRequest = (): DeletePetRequest => { const userId = 'valid_guardian_id' const params = { @@ -178,5 +192,6 @@ export { makeFakeDeletePetRequest, makeFakeEmailConfirmationRequest, makeFakeAddTagRequest, - makeFakeAddSchedulerRequest + makeFakeAddSchedulerRequest, + makeFakeUpdateGuardianRequest } diff --git a/tests/utils/stubs/service.stub.ts b/tests/utils/stubs/service.stub.ts index 01a4e530..1dc3c105 100644 --- a/tests/utils/stubs/service.stub.ts +++ b/tests/utils/stubs/service.stub.ts @@ -8,7 +8,8 @@ import { mockFakePetByIdLoaded, mockFakePetUpdated, mockFakePetByIdDeleted, - mockFakeGuardianUpdated + mockFakeGuardianUpdated, + mockFakeGuardianImageUpdated } from '@/tests/utils' import { type EmailService, @@ -49,7 +50,8 @@ import { type LoadSettingsRepository, type UpdateSettingsRepository, type LoadNextTasksByPetIdAndTagIdRepository, - type UpdateGuardianImageRepository + type UpdateGuardianImageRepository, + type UpdateGuardianRepository } from '@/data/protocols' import { type LoadCatSizesRepository } from '@/data/protocols/db/size/load-cat-sizes-repository' import { type LoadDogSizesRepository } from '@/data/protocols/db/size/load-dog-sizes-repository' @@ -72,7 +74,8 @@ UpdateAccessTokenRepository & UpdateGuardianPasswordRepository & UpdateVerificationTokenRepository & UpdateEmailConfirmationRepository & -UpdateGuardianImageRepository => { +UpdateGuardianImageRepository & +UpdateGuardianRepository => { class GuardianRepositoryStub implements AddGuardianRepository, LoadGuardianByEmailRepository, @@ -81,7 +84,8 @@ UpdateGuardianImageRepository => { UpdateGuardianPasswordRepository, UpdateVerificationTokenRepository, UpdateEmailConfirmationRepository, - UpdateGuardianImageRepository { + UpdateGuardianImageRepository, + UpdateGuardianRepository { async add (guardian: AddGuardianRepository.Params): Promise { return mockFakeGuardianAdded() } @@ -111,6 +115,10 @@ UpdateGuardianImageRepository => { } async updateImage (params: UpdateGuardianImageRepository.Params): Promise { + return mockFakeGuardianImageUpdated() + } + + async update (params: UpdateGuardianRepository.Params): Promise { return mockFakeGuardianUpdated() } } diff --git a/tests/utils/stubs/use-case.stub.ts b/tests/utils/stubs/use-case.stub.ts index 2266b9a3..e1c8055b 100644 --- a/tests/utils/stubs/use-case.stub.ts +++ b/tests/utils/stubs/use-case.stub.ts @@ -24,7 +24,8 @@ import { type LoadSettings, type UpdateSettings, type LoadPetById, - type LoadNextTasksByPetIdAndTagId + type LoadNextTasksByPetIdAndTagId, + type UpdateGuardian } from '@/domain/use-cases' import { mockTokenService } from '@/tests/utils/stubs/service.stub' import { mockFakeAppointPet, mockFakePetUpdated, mockFakePetByGuardianIdLoaded, mockFakeSpecieAdded, makeFakeGuardianData, mockFakeBreedAdded, mockFakeSizeAdded, mockFakePetByIdLoaded } from '../mocks' @@ -60,6 +61,25 @@ const makeFakeAddGuardianUseCase = (): AddGuardian => { return new AddGuardianStub() } +const makeFakeUpdateGuardianUseCase = (): UpdateGuardian => { + class UpdateGuardianStub implements UpdateGuardian { + async update (params: UpdateGuardian.Params): Promise { + const result = { + id: mockGuardianUseCase.id, + firstName: mockGuardianUseCase.firstName, + lastName: mockGuardianUseCase.lastName, + phone: mockGuardianUseCase.phone, + image: mockGuardianUseCase.image + } + return { + isSuccess: true, + data: result + } + } + } + return new UpdateGuardianStub() +} + const makeFakeEmailConfirmationUseCase = (): EmailConfirmation => { class EmailConfirmationStub implements EmailConfirmation { async confirm (userId: EmailConfirmation.Params): Promise { @@ -472,6 +492,7 @@ const makeFakeUpdateSettingsUseCase = (): UpdateSettings => { export { makeFakeAddGuardianUseCase, makeFakeAddPetUseCase, + makeFakeUpdateGuardianUseCase, makeFakeLoadPetsUseCase, makeFakeLoadPetByIdUseCase, makeFakeUpdatePetUseCase, diff --git a/tests/utils/types/request.type.ts b/tests/utils/types/request.type.ts index c2f214b6..91b249d4 100644 --- a/tests/utils/types/request.type.ts +++ b/tests/utils/types/request.type.ts @@ -74,6 +74,18 @@ interface UpdatePetRequest { file?: Buffer } +interface UpdateGuardianRequest { + body: { + firstName: string + lastName: string + phone: string + } + params: { + guardianId: string + } + file?: Buffer +} + interface DeletePetRequest { userId: string params: { @@ -122,5 +134,6 @@ export { type DeletePetRequest, type EmailConfirmationRequest, type AddTagRequest, - type AddSchedulerRequest + type AddSchedulerRequest, + type UpdateGuardianRequest }