From 30130279c761d6b24ad656345477f2e12bea0fa5 Mon Sep 17 00:00:00 2001 From: Foqsz Date: Tue, 7 Apr 2026 12:31:59 -0300 Subject: [PATCH 01/20] Move registration endpoint to new RegisterController Separated the client registration POST endpoint from ClientsController and placed it in a new RegisterController for improved organization and clearer separation of concerns. No changes to endpoint logic. --- .../Controllers/ClientsController.cs | 14 +------------ .../Controllers/RegisterController.cs | 21 +++++++++++++++++++ 2 files changed, 22 insertions(+), 13 deletions(-) create mode 100644 ProductClientHub.API/Controllers/RegisterController.cs diff --git a/ProductClientHub.API/Controllers/ClientsController.cs b/ProductClientHub.API/Controllers/ClientsController.cs index 4f7176b..f660739 100644 --- a/ProductClientHub.API/Controllers/ClientsController.cs +++ b/ProductClientHub.API/Controllers/ClientsController.cs @@ -1,11 +1,9 @@ -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc; using ProductClientHub.API.Attributes; using ProductClientHub.Application.UseCases.Clients.ChangePassword; using ProductClientHub.Application.UseCases.Users.Delete; using ProductClientHub.Application.UseCases.Users.GetAll; using ProductClientHub.Application.UseCases.Users.GetById; -using ProductClientHub.Application.UseCases.Users.Register; using ProductClientHub.Application.UseCases.Users.Update; using ProductClientHub.Communication.Requests; using ProductClientHub.Communication.Responses; @@ -17,16 +15,6 @@ namespace ProductClientHub.API.Controllers; [ApiController] public class ClientsController : ControllerBase { - [HttpPost] - [ProducesResponseType(typeof(ResponseShortClientJson), StatusCodes.Status201Created)] - [ProducesResponseType(typeof(ResponseErrorMessagesJson), StatusCodes.Status400BadRequest)] - public async Task Register([FromBody] RequestClientJson request, [FromServices] IRegisterClientUseCase useCase) - { - var response = await useCase.Execute(request); - - return Created(string.Empty, response); - } - [HttpPost] [Route("changePassword/{clientId:guid}")] [ProducesResponseType(StatusCodes.Status204NoContent)] diff --git a/ProductClientHub.API/Controllers/RegisterController.cs b/ProductClientHub.API/Controllers/RegisterController.cs new file mode 100644 index 0000000..737fffd --- /dev/null +++ b/ProductClientHub.API/Controllers/RegisterController.cs @@ -0,0 +1,21 @@ +using Microsoft.AspNetCore.Mvc; +using ProductClientHub.Application.UseCases.Users.Register; +using ProductClientHub.Communication.Requests; +using ProductClientHub.Communication.Responses; + +namespace ProductClientHub.API.Controllers; + +[Route("api/[controller]")] +[ApiController] +public class RegisterController : ControllerBase +{ + [HttpPost] + [ProducesResponseType(typeof(ResponseShortClientJson), StatusCodes.Status201Created)] + [ProducesResponseType(typeof(ResponseErrorMessagesJson), StatusCodes.Status400BadRequest)] + public async Task Register([FromBody] RequestClientJson request, [FromServices] IRegisterClientUseCase useCase) + { + var response = await useCase.Execute(request); + + return Created(string.Empty, response); + } +} From 42bcc0f024b37742d06a230a7de45feb2ed768d6 Mon Sep 17 00:00:00 2001 From: Foqsz Date: Tue, 7 Apr 2026 12:33:04 -0300 Subject: [PATCH 02/20] Update password and email validation in client use cases - Changed password validation error to use LOGIN_INVALID message. - Added email uniqueness check when updating client info; throws EmailAlreadyExistsException if email is already in use. --- .../UseCases/Clients/ChangePassword/ChangePasswordUseCase.cs | 2 +- .../UseCases/Clients/Update/UpdateClientUseCase.cs | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/ProductClientHub.Application/UseCases/Clients/ChangePassword/ChangePasswordUseCase.cs b/ProductClientHub.Application/UseCases/Clients/ChangePassword/ChangePasswordUseCase.cs index 71efa09..27faa97 100644 --- a/ProductClientHub.Application/UseCases/Clients/ChangePassword/ChangePasswordUseCase.cs +++ b/ProductClientHub.Application/UseCases/Clients/ChangePassword/ChangePasswordUseCase.cs @@ -37,7 +37,7 @@ public async Task Execute(Guid clientId, RequestChangePassword request) var changeVerify = _passwordEncripter.IsValid(request.NewPassword, client.Password); if(changeVerify.IsTrue()) - throw new ChangePasswordException(ResourceMessagesExceptions.PASSWORD_INVALID); + throw new ChangePasswordException(ResourceMessagesExceptions.LOGIN_INVALID); client.Password = _passwordEncripter.Encrypt(request.NewPassword); diff --git a/ProductClientHub.Application/UseCases/Clients/Update/UpdateClientUseCase.cs b/ProductClientHub.Application/UseCases/Clients/Update/UpdateClientUseCase.cs index 3c7ce56..45423b0 100644 --- a/ProductClientHub.Application/UseCases/Clients/Update/UpdateClientUseCase.cs +++ b/ProductClientHub.Application/UseCases/Clients/Update/UpdateClientUseCase.cs @@ -34,6 +34,11 @@ public async Task Execute(Guid clientId, RequestShort if (client is null) throw new NotFoundException(ResourceMessagesExceptions.CLIENT_NOCONTENT); + var emailExist = await _clientReadOnlyRepository.EmailAlreadyExists(request.Email); + + if(emailExist is not null) + throw new EmailAlreadyExistsException(ResourceMessagesExceptions.EMAIL_INVALID); + client.Name = request.Name; client.Email = request.Email; From 54ed0e30b2e1d5a5e07956f96643903079ec2865 Mon Sep 17 00:00:00 2001 From: Foqsz Date: Tue, 7 Apr 2026 12:34:06 -0300 Subject: [PATCH 03/20] Update login error messages to cover email and password Replaced PASSWORD_INVALID with LOGIN_INVALID in all resource files and code. Updated InvalidLoginException to use the new resource, ensuring error messages indicate either email or password may be invalid. Improved localization and clarity of login error handling across supported languages. --- .../ExceptionsBase/InvalidLoginException.cs | 2 +- .../ResourceMessagesExceptions.Designer.cs | 18 +++++++++--------- .../ResourceMessagesExceptions.EN.resx | 4 ++-- .../ResourceMessagesExceptions.fr.resx | 4 ++-- .../ResourceMessagesExceptions.pt-PT.resx | 4 ++-- .../ResourceMessagesExceptions.resx | 4 ++-- 6 files changed, 18 insertions(+), 18 deletions(-) diff --git a/ProductClientHub.Exceptions/ExceptionsBase/InvalidLoginException.cs b/ProductClientHub.Exceptions/ExceptionsBase/InvalidLoginException.cs index b3982c3..0714ec1 100644 --- a/ProductClientHub.Exceptions/ExceptionsBase/InvalidLoginException.cs +++ b/ProductClientHub.Exceptions/ExceptionsBase/InvalidLoginException.cs @@ -4,7 +4,7 @@ namespace ProductClientHub.Exceptions.ExceptionsBase; public class InvalidLoginException : ProductClientHubException { - public InvalidLoginException() : base(ResourceMessagesExceptions.PASSWORD_INVALID) + public InvalidLoginException() : base(ResourceMessagesExceptions.LOGIN_INVALID) { } public override List GetErrors() => new List { Message }; diff --git a/ProductClientHub.Exceptions/ExceptionsBase/ResourceMessagesExceptions.Designer.cs b/ProductClientHub.Exceptions/ExceptionsBase/ResourceMessagesExceptions.Designer.cs index f3f043d..c1a7057 100644 --- a/ProductClientHub.Exceptions/ExceptionsBase/ResourceMessagesExceptions.Designer.cs +++ b/ProductClientHub.Exceptions/ExceptionsBase/ResourceMessagesExceptions.Designer.cs @@ -88,29 +88,29 @@ public static string EMAIL_INVALID { } /// - /// Looks up a localized string similar to Token Inválido.. + /// Looks up a localized string similar to Senha ou E-mail inválidos.. /// - public static string NO_TOKEN { + public static string LOGIN_INVALID { get { - return ResourceManager.GetString("NO_TOKEN", resourceCulture); + return ResourceManager.GetString("LOGIN_INVALID", resourceCulture); } } /// - /// Looks up a localized string similar to Você não está logado e não pode fazer a requisição.. + /// Looks up a localized string similar to Token Inválido.. /// - public static string NOT_LOGGED { + public static string NO_TOKEN { get { - return ResourceManager.GetString("NOT_LOGGED", resourceCulture); + return ResourceManager.GetString("NO_TOKEN", resourceCulture); } } /// - /// Looks up a localized string similar to Senha inválida.. + /// Looks up a localized string similar to Você não está logado e não pode fazer a requisição.. /// - public static string PASSWORD_INVALID { + public static string NOT_LOGGED { get { - return ResourceManager.GetString("PASSWORD_INVALID", resourceCulture); + return ResourceManager.GetString("NOT_LOGGED", resourceCulture); } } diff --git a/ProductClientHub.Exceptions/ExceptionsBase/ResourceMessagesExceptions.EN.resx b/ProductClientHub.Exceptions/ExceptionsBase/ResourceMessagesExceptions.EN.resx index 9ff8fea..ffdfba7 100644 --- a/ProductClientHub.Exceptions/ExceptionsBase/ResourceMessagesExceptions.EN.resx +++ b/ProductClientHub.Exceptions/ExceptionsBase/ResourceMessagesExceptions.EN.resx @@ -141,8 +141,8 @@ Invalid token. - - Password invalid + + Password or e-mail invalids You are not logged in and cannot make the request. diff --git a/ProductClientHub.Exceptions/ExceptionsBase/ResourceMessagesExceptions.fr.resx b/ProductClientHub.Exceptions/ExceptionsBase/ResourceMessagesExceptions.fr.resx index 942b882..57afe44 100644 --- a/ProductClientHub.Exceptions/ExceptionsBase/ResourceMessagesExceptions.fr.resx +++ b/ProductClientHub.Exceptions/ExceptionsBase/ResourceMessagesExceptions.fr.resx @@ -141,8 +141,8 @@ Token Invalide. - - Password invalide. + + Password/e-mail invalide. Vous n'êtes pas connecté et ne pouvez pas effectuer cette demande. diff --git a/ProductClientHub.Exceptions/ExceptionsBase/ResourceMessagesExceptions.pt-PT.resx b/ProductClientHub.Exceptions/ExceptionsBase/ResourceMessagesExceptions.pt-PT.resx index b1aca6b..fd27c61 100644 --- a/ProductClientHub.Exceptions/ExceptionsBase/ResourceMessagesExceptions.pt-PT.resx +++ b/ProductClientHub.Exceptions/ExceptionsBase/ResourceMessagesExceptions.pt-PT.resx @@ -141,8 +141,8 @@ Token Inválido. - - Senha inválida. + + Senha ou e-mail inválidos. Não está logado e não pode fazer o pedido. diff --git a/ProductClientHub.Exceptions/ExceptionsBase/ResourceMessagesExceptions.resx b/ProductClientHub.Exceptions/ExceptionsBase/ResourceMessagesExceptions.resx index 0f42636..1203b65 100644 --- a/ProductClientHub.Exceptions/ExceptionsBase/ResourceMessagesExceptions.resx +++ b/ProductClientHub.Exceptions/ExceptionsBase/ResourceMessagesExceptions.resx @@ -148,8 +148,8 @@ Token Inválido. - - Senha inválida. + + Senha ou E-mail inválidos. Você não está logado e não pode fazer a requisição. From da1174aae33aa58a71931687d19ec96ef621c439 Mon Sep 17 00:00:00 2001 From: Foqsz Date: Wed, 8 Apr 2026 09:49:23 -0300 Subject: [PATCH 04/20] Refactor product use cases to use authenticated user ID Removed explicit clientId parameters from product endpoints and use cases. Now, the authenticated user's ID is obtained via the ILoggedUser service for all product operations. Updated controller routes, use case interfaces, and implementations accordingly. This enhances security and maintainability by ensuring actions are always performed in the context of the logged-in user. Also simplified UpdateClientUseCase with null-coalescing exception handling. --- .../Controllers/ProductsController.cs | 15 ++++++--------- .../Clients/Update/UpdateClientUseCase.cs | 6 +----- .../Products/Delete/DeleteProductUseCase.cs | 16 +++++++++++----- .../Products/Delete/IDeleteProductUseCase.cs | 2 +- .../Register/IRegisterProductUseCase.cs | 2 +- .../Register/RegisterProductUseCase.cs | 14 ++++++++++---- .../Products/Update/IUploadProductUseCase.cs | 2 +- .../Products/Update/UploadProductUseCase.cs | 18 ++++++++++-------- 8 files changed, 41 insertions(+), 34 deletions(-) diff --git a/ProductClientHub.API/Controllers/ProductsController.cs b/ProductClientHub.API/Controllers/ProductsController.cs index 9b4abc3..e4b82f7 100644 --- a/ProductClientHub.API/Controllers/ProductsController.cs +++ b/ProductClientHub.API/Controllers/ProductsController.cs @@ -16,41 +16,38 @@ namespace ProductClientHub.API.Controllers; public class ProductsController : ControllerBase { [HttpPost] - [Route("${clientId:guid}")] [ProducesResponseType(typeof(ResponseShortProductJson), StatusCodes.Status201Created)] [ProducesResponseType(typeof(ResponseErrorMessagesJson), StatusCodes.Status400BadRequest)] [ProducesResponseType(typeof(ResponseErrorMessagesJson), StatusCodes.Status404NotFound)] public async Task Register([FromBody] RequestProductJson request, - [FromRoute] Guid clientId, [FromServices] IRegisterProductUseCase useCase) { - var response = await useCase.Execute(clientId, request); + var response = await useCase.Execute(request); return Created(string.Empty, response); } [HttpPut] - [Route("{productId:guid}/client/{clientId:guid}")] + [Route("{productId:guid}")] [ProducesResponseType(typeof(ResponseShortProductJson), StatusCodes.Status200OK)] [ProducesResponseType(typeof(ResponseErrorMessagesJson), StatusCodes.Status400BadRequest)] [ProducesResponseType(typeof(ResponseErrorMessagesJson), StatusCodes.Status404NotFound)] public async Task Update([FromBody] RequestProductJson request, [FromRoute] Guid productId, - [FromRoute] Guid clientId, [FromServices] IUploadProductUseCase useCase) { - var response = await useCase.Execute(clientId, productId, request); + var response = await useCase.Execute(productId, request); return Ok(response); } [HttpDelete] - [Route("{productId:guid}/client/{clientId:guid}")] + [Route("{productId:guid}")] [ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(typeof(ResponseErrorMessagesJson), StatusCodes.Status404NotFound)] - public async Task Delete([FromRoute] Guid productId, [FromRoute] Guid clientId, [FromServices] IDeleteProductUseCase useCase) + public async Task Delete([FromRoute] Guid productId, [FromServices] IDeleteProductUseCase useCase) { - await useCase.Execute(clientId, productId); + await useCase.Execute(productId); return NoContent(); } diff --git a/ProductClientHub.Application/UseCases/Clients/Update/UpdateClientUseCase.cs b/ProductClientHub.Application/UseCases/Clients/Update/UpdateClientUseCase.cs index 45423b0..a3a9f48 100644 --- a/ProductClientHub.Application/UseCases/Clients/Update/UpdateClientUseCase.cs +++ b/ProductClientHub.Application/UseCases/Clients/Update/UpdateClientUseCase.cs @@ -1,6 +1,5 @@ using Mapster; using ProductClientHub.Application.UseCases.Clients.Update; -using ProductClientHub.Application.UseCases.Users.SharedValidator; using ProductClientHub.Communication.Requests; using ProductClientHub.Communication.Responses; using ProductClientHub.Domain.Extensions; @@ -29,10 +28,7 @@ public async Task Execute(Guid clientId, RequestShort { Validate(request); - var client = await _clientReadOnlyRepository.GetById(clientId); - - if (client is null) - throw new NotFoundException(ResourceMessagesExceptions.CLIENT_NOCONTENT); + var client = await _clientReadOnlyRepository.GetById(clientId) ?? throw new NotFoundException(ResourceMessagesExceptions.CLIENT_NOCONTENT); var emailExist = await _clientReadOnlyRepository.EmailAlreadyExists(request.Email); diff --git a/ProductClientHub.Application/UseCases/Products/Delete/DeleteProductUseCase.cs b/ProductClientHub.Application/UseCases/Products/Delete/DeleteProductUseCase.cs index e7888a1..eaee1d1 100644 --- a/ProductClientHub.Application/UseCases/Products/Delete/DeleteProductUseCase.cs +++ b/ProductClientHub.Application/UseCases/Products/Delete/DeleteProductUseCase.cs @@ -1,6 +1,7 @@ using ProductClientHub.Domain.Extensions; using ProductClientHub.Domain.Repositories.Product; using ProductClientHub.Domain.Repositories.UnitOfWork; +using ProductClientHub.Domain.Services.LoggedUser; using ProductClientHub.Exceptions.ExceptionsBase; namespace ProductClientHub.Application.UseCases.Products.Delete; @@ -9,19 +10,24 @@ public class DeleteProductUseCase : IDeleteProductUseCase { private readonly IDeleteProductOnlyRepository _productsWriteOnlyRepository; private readonly IUnitOfWork _unitOfWork; + private readonly ILoggedUser _loggedUser; - public DeleteProductUseCase(IDeleteProductOnlyRepository productsWriteOnlyRepository, - IUnitOfWork unitOfWork) + public DeleteProductUseCase(IDeleteProductOnlyRepository productsWriteOnlyRepository, + IUnitOfWork unitOfWork, + ILoggedUser loggedUser) { _productsWriteOnlyRepository = productsWriteOnlyRepository; _unitOfWork = unitOfWork; + _loggedUser = loggedUser; } - public async Task Execute(Guid clientId, Guid productId) + public async Task Execute(Guid productId) { - var getProduct = await _productsWriteOnlyRepository.Delete(clientId, productId); + var client = await _loggedUser.User(); - if(getProduct.IsFalse()) + var productExist = await _productsWriteOnlyRepository.Delete(client.Id, productId); + + if(productExist.IsFalse()) throw new NoContentException(ResourceMessagesExceptions.PRODUCT_INVALID); await _unitOfWork.Commit(); diff --git a/ProductClientHub.Application/UseCases/Products/Delete/IDeleteProductUseCase.cs b/ProductClientHub.Application/UseCases/Products/Delete/IDeleteProductUseCase.cs index 8f9b0f5..4c568aa 100644 --- a/ProductClientHub.Application/UseCases/Products/Delete/IDeleteProductUseCase.cs +++ b/ProductClientHub.Application/UseCases/Products/Delete/IDeleteProductUseCase.cs @@ -2,5 +2,5 @@ public interface IDeleteProductUseCase { - Task Execute(Guid clientId, Guid productId); + Task Execute(Guid productId); } diff --git a/ProductClientHub.Application/UseCases/Products/Register/IRegisterProductUseCase.cs b/ProductClientHub.Application/UseCases/Products/Register/IRegisterProductUseCase.cs index 7dbb674..bdc868b 100644 --- a/ProductClientHub.Application/UseCases/Products/Register/IRegisterProductUseCase.cs +++ b/ProductClientHub.Application/UseCases/Products/Register/IRegisterProductUseCase.cs @@ -5,5 +5,5 @@ namespace ProductClientHub.Application.UseCases.Products.Register; public interface IRegisterProductUseCase { - Task Execute(Guid clientId, RequestProductJson request); + Task Execute(RequestProductJson request); } diff --git a/ProductClientHub.Application/UseCases/Products/Register/RegisterProductUseCase.cs b/ProductClientHub.Application/UseCases/Products/Register/RegisterProductUseCase.cs index 45abc34..e4296fc 100644 --- a/ProductClientHub.Application/UseCases/Products/Register/RegisterProductUseCase.cs +++ b/ProductClientHub.Application/UseCases/Products/Register/RegisterProductUseCase.cs @@ -6,6 +6,7 @@ using ProductClientHub.Domain.Repositories.Client; using ProductClientHub.Domain.Repositories.Product; using ProductClientHub.Domain.Repositories.UnitOfWork; +using ProductClientHub.Domain.Services.LoggedUser; using ProductClientHub.Exceptions.ExceptionsBase; namespace ProductClientHub.Application.UseCases.Products.Register; @@ -15,22 +16,27 @@ public class RegisterProductUseCase : IRegisterProductUseCase private readonly IProductsWriteOnlyRepository _productsWriteOnlyRepository; private readonly IClientReadOnlyRepository _clientReadOnlyRepository; private readonly IUnitOfWork _unitOfWork; + private readonly ILoggedUser _loggedUser; public RegisterProductUseCase(IProductsWriteOnlyRepository productsWriteOnlyRepository, IUnitOfWork unitOfWork, - IClientReadOnlyRepository clientReadOnlyRepository) + IClientReadOnlyRepository clientReadOnlyRepository, + ILoggedUser loggedUser) { _productsWriteOnlyRepository = productsWriteOnlyRepository; _unitOfWork = unitOfWork; _clientReadOnlyRepository = clientReadOnlyRepository; + _loggedUser = loggedUser; } - public async Task Execute(Guid clientId, RequestProductJson request) + public async Task Execute(RequestProductJson request) { - await Validate(clientId, request); + var user = await _loggedUser.User(); + + await Validate(user.Id, request); var entity = request.Adapt(); - entity.ClientId = clientId; + entity.ClientId = user.Id; await _productsWriteOnlyRepository.Add(entity); await _unitOfWork.Commit(); diff --git a/ProductClientHub.Application/UseCases/Products/Update/IUploadProductUseCase.cs b/ProductClientHub.Application/UseCases/Products/Update/IUploadProductUseCase.cs index a6fbeed..64957df 100644 --- a/ProductClientHub.Application/UseCases/Products/Update/IUploadProductUseCase.cs +++ b/ProductClientHub.Application/UseCases/Products/Update/IUploadProductUseCase.cs @@ -5,5 +5,5 @@ namespace ProductClientHub.Application.UseCases.Products.Update; public interface IUploadProductUseCase { - Task Execute(Guid clientId, Guid productId, RequestProductJson request); + Task Execute(Guid productId, RequestProductJson request); } diff --git a/ProductClientHub.Application/UseCases/Products/Update/UploadProductUseCase.cs b/ProductClientHub.Application/UseCases/Products/Update/UploadProductUseCase.cs index 174b9e1..471784b 100644 --- a/ProductClientHub.Application/UseCases/Products/Update/UploadProductUseCase.cs +++ b/ProductClientHub.Application/UseCases/Products/Update/UploadProductUseCase.cs @@ -6,6 +6,7 @@ using ProductClientHub.Domain.Repositories.Client; using ProductClientHub.Domain.Repositories.Product; using ProductClientHub.Domain.Repositories.UnitOfWork; +using ProductClientHub.Domain.Services.LoggedUser; using ProductClientHub.Exceptions.ExceptionsBase; namespace ProductClientHub.Application.UseCases.Products.Update; @@ -16,32 +17,33 @@ public class UploadProductUseCase : IUploadProductUseCase private readonly IClientReadOnlyRepository _clientReadOnlyRepository; private readonly IProductsReadOnlyRepository _productsReadOnlyRepository; private readonly IUploadProductOnlyRepository _productWriteOnlyRepository; + private readonly ILoggedUser _loggedUser; public UploadProductUseCase(IUnitOfWork unitOfWork, IUploadProductOnlyRepository productWriteOnlyRepository, IClientReadOnlyRepository clientReadOnlyRepository, - IProductsReadOnlyRepository productsReadOnlyRepository) + IProductsReadOnlyRepository productsReadOnlyRepository, + ILoggedUser loggedUser) { _unitOfWork = unitOfWork; _productWriteOnlyRepository = productWriteOnlyRepository; _clientReadOnlyRepository = clientReadOnlyRepository; _productsReadOnlyRepository = productsReadOnlyRepository; + _loggedUser = loggedUser; } - public async Task Execute(Guid clientId, Guid productId, RequestProductJson request) + public async Task Execute(Guid productId, RequestProductJson request) { - await Validate(clientId, request); + var client = await _loggedUser.User(); + await Validate(client.Id, request); - var product = await _productsReadOnlyRepository.GetById(productId); - - if (product == null) - throw new NotFoundException(ResourceMessagesExceptions.PRODUCT_NOTFOUND); + var product = await _productsReadOnlyRepository.GetById(productId) ?? throw new NotFoundException(ResourceMessagesExceptions.PRODUCT_NOTFOUND); product.Name = request.Name; product.Brand = request.Brand; product.Price = request.Price; - await _productWriteOnlyRepository.Update(clientId, productId, product); + await _productWriteOnlyRepository.Update(client.Id, productId, product); await _unitOfWork.Commit(); return product.Adapt(); From f7251257e99d7c6b8ba620a8b297765802715b9a Mon Sep 17 00:00:00 2001 From: Foqsz Date: Wed, 8 Apr 2026 11:23:58 -0300 Subject: [PATCH 05/20] Add unit tests for UpdateClientUseCase and refactor helpers Added three unit tests covering success, client not found, and email already exists scenarios for UpdateClientUseCase. Refactored the CreateUseCase helper to accept a clientExist parameter and improved repository setup logic. Included necessary using statements for test utilities and exception handling. --- .../Client/Update/UpdateClientUseCaseTest.cs | 61 +++++++++++++++++-- 1 file changed, 57 insertions(+), 4 deletions(-) diff --git a/UseCase.Test/Client/Update/UpdateClientUseCaseTest.cs b/UseCase.Test/Client/Update/UpdateClientUseCaseTest.cs index 83e3a9f..97679ed 100644 --- a/UseCase.Test/Client/Update/UpdateClientUseCaseTest.cs +++ b/UseCase.Test/Client/Update/UpdateClientUseCaseTest.cs @@ -1,23 +1,76 @@ using CommonTestUtilities.Cryptografhy; +using CommonTestUtilities.Entities; using CommonTestUtilities.Repositories; +using CommonTestUtilities.Requests; using ProductClientHub.Application.UseCases.Users.Update; using ProductClientHub.Domain.Extensions; +using ProductClientHub.Exceptions.ExceptionsBase; +using Shouldly; namespace UseCase.Test.Client.Update; public class UpdateClientUseCaseTest { + [Fact] + public async Task UpdateClient_Sucess() + { + (var client, _) = ClientBuilder.Build(); + + var clientRequest = RequestShortClientJsonBuilder.Build(client.Name, client.Email); + + var useCase = CreateUseCase(client, emailExistsTest: false, clientExist: true); + + var result = await useCase.Execute(client.Id, clientRequest); + + result.ShouldNotBeNull(); + result.ShouldSatisfyAllConditions( + () => result.Name.ShouldBe(clientRequest.Name), + () => result.Email.ShouldBe(clientRequest.Email) + ); + } + + [Fact] + public async Task UpdateClient_Error_ClientNotExists() + { + (var client, _) = ClientBuilder.Build(); + + var clientRequest = RequestShortClientJsonBuilder.Build(client.Name, client.Email); + + var useCase = CreateUseCase(client, emailExistsTest: false, clientExist: false); + + var resultException = await Should.ThrowAsync(async () => await useCase.Execute(client.Id, clientRequest)); - private static UpdateClientUseCase CreateUseCase(ProductClientHub.Domain.Entities.Client? client, bool emailExistsTest) + resultException.ShouldNotBeNull(); + resultException.ShouldSatisfyAllConditions(() => resultException.Message.ShouldBe(ResourceMessagesExceptions.CLIENT_NOCONTENT)); + } + + [Fact] + public async Task UpdateClient_Error_EmailExist() + { + (var client, _) = ClientBuilder.Build(); + + var clientRequest = RequestShortClientJsonBuilder.Build(client.Name, client.Email); + + var useCase = CreateUseCase(client, emailExistsTest: true, clientExist: true); + + var resultException = await Should.ThrowAsync(async () => await useCase.Execute(client.Id, clientRequest)); + + resultException.ShouldNotBeNull(); + resultException.ShouldSatisfyAllConditions(() => resultException.Message.ShouldBe(ResourceMessagesExceptions.EMAIL_INVALID)); + } + + private static UpdateClientUseCase CreateUseCase(ProductClientHub.Domain.Entities.Client? client, bool emailExistsTest, bool clientExist) { var clientWriteOnlyRepository = ClientWriteOnlyRepositoryBuilder.Build(); var clientReadOnlyRepository = new ClientReadOnlyRepositoryBuilder(); var unitOfWork = UnitOfWorkBuilder.Build(); - if(client is not null && emailExistsTest.IsTrue()) - { + if(client is not null && clientExist.IsTrue()) + clientReadOnlyRepository.GetById(client); + + if(emailExistsTest.IsTrue()) clientReadOnlyRepository.EmailAlreadyExists(client); - } + return new UpdateClientUseCase(clientWriteOnlyRepository, clientReadOnlyRepository.Build(), unitOfWork); } } From b353c73e16cf97dd531ac4cdc1f1982eb5425b6c Mon Sep 17 00:00:00 2001 From: Foqsz Date: Thu, 9 Apr 2026 11:41:11 -0300 Subject: [PATCH 06/20] Integrate RabbitMQ messaging for client creation events Added RabbitMQ support with configuration in appsettings, a new IMessagePublisher abstraction, and implementations for publishing (RabbitMqPublisher) and consuming (ClientCreatedConsumer) messages. RegisterClientUseCase now publishes to the clients.created queue after registration. Updated dependency injection and tests to support messaging. Added RabbitMQ.Client NuGet package and RabbitMqOptions for configuration binding. Enables event-driven communication for client creation. --- .../Messaging/MessagePublisherBuilder.cs | 17 +++++ .../appsettings.Development.json | 7 ++ ProductClientHub.API/appsettings.json | 7 ++ .../Clients/Register/RegisterClientUseCase.cs | 14 +++- .../Services/Messaging/IMessagePublisher.cs | 6 ++ .../DependencyInjectionExtension.cs | 10 +++ .../RabbitMq/ClientCreatedConsumer.cs | 75 +++++++++++++++++++ .../Messaging/RabbitMq/RabbitMqOptions.cs | 12 +++ .../Messaging/RabbitMq/RabbitMqPublisher.cs | 52 +++++++++++++ .../ProductClientHub.Infrastructure.csproj | 1 + .../Register/RegisterClientUseCaseTest.cs | 5 +- 11 files changed, 204 insertions(+), 2 deletions(-) create mode 100644 CommonTestUtilities/Messaging/MessagePublisherBuilder.cs create mode 100644 ProductClientHub.Domain/Services/Messaging/IMessagePublisher.cs create mode 100644 ProductClientHub.Infrastructure/Messaging/RabbitMq/ClientCreatedConsumer.cs create mode 100644 ProductClientHub.Infrastructure/Messaging/RabbitMq/RabbitMqOptions.cs create mode 100644 ProductClientHub.Infrastructure/Messaging/RabbitMq/RabbitMqPublisher.cs diff --git a/CommonTestUtilities/Messaging/MessagePublisherBuilder.cs b/CommonTestUtilities/Messaging/MessagePublisherBuilder.cs new file mode 100644 index 0000000..4d2e424 --- /dev/null +++ b/CommonTestUtilities/Messaging/MessagePublisherBuilder.cs @@ -0,0 +1,17 @@ +using Moq; +using ProductClientHub.Domain.Services.Messaging; + +namespace CommonTestUtilities.Messaging; + +public static class MessagePublisherBuilder +{ + public static IMessagePublisher Build() + { + var mock = new Mock(); + + mock.Setup(publisher => publisher.PublishAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(Task.CompletedTask); + + return mock.Object; + } +} diff --git a/ProductClientHub.API/appsettings.Development.json b/ProductClientHub.API/appsettings.Development.json index f336dbf..cce5961 100644 --- a/ProductClientHub.API/appsettings.Development.json +++ b/ProductClientHub.API/appsettings.Development.json @@ -5,5 +5,12 @@ "Jwt": { "SigningKey": "abjkl012mno3-64BYTES45c123def456ghi789pq", "ExpirationTimeMinutes": 1000 + }, + "RabbitMQ": { + "HostName": "localhost", + "Port": 5672, + "UserName": "guest", + "Password": "guest", + "QueueName": "clients.created" } } \ No newline at end of file diff --git a/ProductClientHub.API/appsettings.json b/ProductClientHub.API/appsettings.json index 10f68b8..8871e92 100644 --- a/ProductClientHub.API/appsettings.json +++ b/ProductClientHub.API/appsettings.json @@ -5,5 +5,12 @@ "Microsoft.AspNetCore": "Warning" } }, + "RabbitMQ": { + "HostName": "localhost", + "Port": 5672, + "UserName": "guest", + "Password": "guest", + "QueueName": "clients.created" + }, "AllowedHosts": "*" } diff --git a/ProductClientHub.Application/UseCases/Clients/Register/RegisterClientUseCase.cs b/ProductClientHub.Application/UseCases/Clients/Register/RegisterClientUseCase.cs index fbab7bf..6ec5899 100644 --- a/ProductClientHub.Application/UseCases/Clients/Register/RegisterClientUseCase.cs +++ b/ProductClientHub.Application/UseCases/Clients/Register/RegisterClientUseCase.cs @@ -7,6 +7,7 @@ using ProductClientHub.Domain.Repositories.Client; using ProductClientHub.Domain.Repositories.UnitOfWork; using ProductClientHub.Domain.Security.Cryptography; +using ProductClientHub.Domain.Services.Messaging; using ProductClientHub.Exceptions.ExceptionsBase; namespace ProductClientHub.Application.UseCases.Users.Register; @@ -17,16 +18,19 @@ public class RegisterClientUseCase : IRegisterClientUseCase private readonly IClientReadOnlyRepository _clientReadOnlyRepository; private readonly IUnitOfWork _unitOfWork; private readonly IPasswordEncripter _passwordEncripter; + private readonly IMessagePublisher _messagePublisher; public RegisterClientUseCase(IClientWriteOnlyRepository clientWriteOnlyRepository, IUnitOfWork unitOfWork, IClientReadOnlyRepository clientReadOnlyRepository, - IPasswordEncripter passwordEncripter) + IPasswordEncripter passwordEncripter, + IMessagePublisher messagePublisher) { _clientWriteOnlyRepository = clientWriteOnlyRepository; _unitOfWork = unitOfWork; _clientReadOnlyRepository = clientReadOnlyRepository; _passwordEncripter = passwordEncripter; + _messagePublisher = messagePublisher; } public async Task Execute(RequestClientJson request) @@ -47,6 +51,14 @@ public async Task Execute(RequestClientJson request) await _clientWriteOnlyRepository.Add(entity); await _unitOfWork.Commit(); + await _messagePublisher.PublishAsync("clients.created", new + { + entity.Id, + entity.Name, + entity.Email, + entity.CreatedOn + }); + return entity.Adapt(); } diff --git a/ProductClientHub.Domain/Services/Messaging/IMessagePublisher.cs b/ProductClientHub.Domain/Services/Messaging/IMessagePublisher.cs new file mode 100644 index 0000000..23be0c2 --- /dev/null +++ b/ProductClientHub.Domain/Services/Messaging/IMessagePublisher.cs @@ -0,0 +1,6 @@ +namespace ProductClientHub.Domain.Services.Messaging; + +public interface IMessagePublisher +{ + Task PublishAsync(string queueName, T message, CancellationToken cancellationToken = default); +} diff --git a/ProductClientHub.Infrastructure/DependencyInjectionExtension.cs b/ProductClientHub.Infrastructure/DependencyInjectionExtension.cs index 3e43d39..a09ce39 100644 --- a/ProductClientHub.Infrastructure/DependencyInjectionExtension.cs +++ b/ProductClientHub.Infrastructure/DependencyInjectionExtension.cs @@ -22,7 +22,9 @@ using ProductClientHub.Infrastructure.Security.Tokens.Acess.Generator; using ProductClientHub.Infrastructure.Security.Tokens.Acess.Validator; using ProductClientHub.Domain.Services.LoggedUser; +using ProductClientHub.Domain.Services.Messaging; using ProductClientHub.Infrastructure.Services; +using ProductClientHub.Infrastructure.Messaging.RabbitMq; namespace ProductClientHub.Infrastructure; @@ -36,6 +38,7 @@ public static void AddInfrastructure(this IServiceCollection services, IConfigur AddTokens(services, configuration); AddPasswordEncrpter(services); AddLoggedUser(services); + AddMessaging(services, configuration); } public static void AddLogger(this IHostBuilder builder, IConfiguration configuration) @@ -77,6 +80,13 @@ private static void AddTokens(IServiceCollection services, IConfiguration config private static void AddLoggedUser(IServiceCollection services) => services.AddScoped(); + private static void AddMessaging(IServiceCollection services, IConfiguration configuration) + { + services.Configure(configuration.GetSection(RabbitMqOptions.SectionName)); + services.AddSingleton(); + services.AddHostedService(); + } + private static void AddDbContext_PostgreSql(IServiceCollection services, IConfiguration configuration) { var connectionString = configuration.ConnectionString(); diff --git a/ProductClientHub.Infrastructure/Messaging/RabbitMq/ClientCreatedConsumer.cs b/ProductClientHub.Infrastructure/Messaging/RabbitMq/ClientCreatedConsumer.cs new file mode 100644 index 0000000..24f5191 --- /dev/null +++ b/ProductClientHub.Infrastructure/Messaging/RabbitMq/ClientCreatedConsumer.cs @@ -0,0 +1,75 @@ +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using RabbitMQ.Client; +using RabbitMQ.Client.Events; +using System.Text; + +namespace ProductClientHub.Infrastructure.Messaging.RabbitMq; + +public sealed class ClientCreatedConsumer : BackgroundService +{ + private readonly RabbitMqOptions _options; + private readonly ILogger _logger; + private IConnection? _connection; + private IChannel? _channel; + + public ClientCreatedConsumer(IOptions options, ILogger logger) + { + _options = options.Value; + _logger = logger; + } + + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + var factory = new ConnectionFactory + { + HostName = _options.HostName, + Port = _options.Port, + UserName = _options.UserName, + Password = _options.Password + }; + + _connection = await factory.CreateConnectionAsync(stoppingToken); + _channel = await _connection.CreateChannelAsync(cancellationToken: stoppingToken); + + await _channel.QueueDeclareAsync( + queue: _options.QueueName, + durable: true, + exclusive: false, + autoDelete: false, + arguments: null, + cancellationToken: stoppingToken); + + var consumer = new AsyncEventingBasicConsumer(_channel); + consumer.ReceivedAsync += async (_, eventArgs) => + { + var body = eventArgs.Body.ToArray(); + var message = Encoding.UTF8.GetString(body); + + _logger.LogInformation("Mensagem recebida da fila {Queue}: {Message}", _options.QueueName, message); + + await _channel.BasicAckAsync(eventArgs.DeliveryTag, multiple: false, cancellationToken: stoppingToken); + }; + + await _channel.BasicConsumeAsync( + queue: _options.QueueName, + autoAck: false, + consumer: consumer, + cancellationToken: stoppingToken); + + await Task.Delay(Timeout.Infinite, stoppingToken); + } + + public override async Task StopAsync(CancellationToken cancellationToken) + { + if (_channel is not null) + await _channel.DisposeAsync(); + + if (_connection is not null) + await _connection.DisposeAsync(); + + await base.StopAsync(cancellationToken); + } + +} diff --git a/ProductClientHub.Infrastructure/Messaging/RabbitMq/RabbitMqOptions.cs b/ProductClientHub.Infrastructure/Messaging/RabbitMq/RabbitMqOptions.cs new file mode 100644 index 0000000..3599c45 --- /dev/null +++ b/ProductClientHub.Infrastructure/Messaging/RabbitMq/RabbitMqOptions.cs @@ -0,0 +1,12 @@ +namespace ProductClientHub.Infrastructure.Messaging.RabbitMq; + +public sealed class RabbitMqOptions +{ + public const string SectionName = "RabbitMQ"; + + public string HostName { get; set; } = "localhost"; + public int Port { get; set; } = 5672; + public string UserName { get; set; } = "guest"; + public string Password { get; set; } = "guest"; + public string QueueName { get; set; } = "clients.created"; +} diff --git a/ProductClientHub.Infrastructure/Messaging/RabbitMq/RabbitMqPublisher.cs b/ProductClientHub.Infrastructure/Messaging/RabbitMq/RabbitMqPublisher.cs new file mode 100644 index 0000000..d40cfed --- /dev/null +++ b/ProductClientHub.Infrastructure/Messaging/RabbitMq/RabbitMqPublisher.cs @@ -0,0 +1,52 @@ +using Microsoft.Extensions.Options; +using ProductClientHub.Domain.Services.Messaging; +using RabbitMQ.Client; +using System.Text; +using System.Text.Json; + +namespace ProductClientHub.Infrastructure.Messaging.RabbitMq; + +public sealed class RabbitMqPublisher : IMessagePublisher +{ + private readonly RabbitMqOptions _options; + + public RabbitMqPublisher(IOptions options) + { + _options = options.Value; + } + + public async Task PublishAsync(string queueName, T message, CancellationToken cancellationToken = default) + { + var factory = new ConnectionFactory + { + HostName = _options.HostName, + Port = _options.Port, + UserName = _options.UserName, + Password = _options.Password + }; + + await using var connection = await factory.CreateConnectionAsync(cancellationToken); + await using var channel = await connection.CreateChannelAsync(cancellationToken: cancellationToken); + + await channel.QueueDeclareAsync( + queue: queueName, + durable: true, + exclusive: false, + autoDelete: false, + arguments: null, + cancellationToken: cancellationToken); + + var payload = JsonSerializer.Serialize(message); + var body = Encoding.UTF8.GetBytes(payload); + + var properties = new BasicProperties { Persistent = true }; + + await channel.BasicPublishAsync( + exchange: string.Empty, + routingKey: queueName, + mandatory: false, + basicProperties: properties, + body: body, + cancellationToken: cancellationToken); + } +} diff --git a/ProductClientHub.Infrastructure/ProductClientHub.Infrastructure.csproj b/ProductClientHub.Infrastructure/ProductClientHub.Infrastructure.csproj index 39137b7..c3cb5ec 100644 --- a/ProductClientHub.Infrastructure/ProductClientHub.Infrastructure.csproj +++ b/ProductClientHub.Infrastructure/ProductClientHub.Infrastructure.csproj @@ -23,6 +23,7 @@ + diff --git a/UseCase.Test/Client/Register/RegisterClientUseCaseTest.cs b/UseCase.Test/Client/Register/RegisterClientUseCaseTest.cs index 3568676..7320c8a 100644 --- a/UseCase.Test/Client/Register/RegisterClientUseCaseTest.cs +++ b/UseCase.Test/Client/Register/RegisterClientUseCaseTest.cs @@ -1,5 +1,6 @@ using CommonTestUtilities.Cryptografhy; using CommonTestUtilities.Entities; +using CommonTestUtilities.Messaging; using CommonTestUtilities.Repositories; using CommonTestUtilities.Requests; using ProductClientHub.Application.UseCases.Users.Register; @@ -56,6 +57,7 @@ private static RegisterClientUseCase CreateUseCase(ProductClientHub.Domain.Entit var clientReadOnlyRepository = new ClientReadOnlyRepositoryBuilder(); var unitOfWork = UnitOfWorkBuilder.Build(); var passwordEncripterBuilder = PasswordEncripterBuilder.Build(); + var messagePublisher = MessagePublisherBuilder.Build(); if(client is not null && emailExistsTest.IsTrue()) { @@ -67,6 +69,7 @@ private static RegisterClientUseCase CreateUseCase(ProductClientHub.Domain.Entit clientWriteOnlyRepository, unitOfWork, clientReadOnlyRepository.Build(), - passwordEncripterBuilder); + passwordEncripterBuilder, + messagePublisher); } } From 8e76df49d58b6e4d0f81f68b6c583503db7520bf Mon Sep 17 00:00:00 2001 From: Foqsz Date: Sat, 11 Apr 2026 11:04:26 -0300 Subject: [PATCH 07/20] # Add EnableConsumer option to control RabbitMQ consumer Introduced an EnableConsumer setting in RabbitMQ config (default true) to allow enabling or disabling the ClientCreatedConsumer hosted service via configuration. Updated RabbitMqOptions and conditional service registration accordingly. --- ProductClientHub.API/appsettings.Development.json | 3 ++- ProductClientHub.API/appsettings.json | 3 ++- .../DependencyInjectionExtension.cs | 7 ++++++- .../Messaging/RabbitMq/RabbitMqOptions.cs | 1 + 4 files changed, 11 insertions(+), 3 deletions(-) diff --git a/ProductClientHub.API/appsettings.Development.json b/ProductClientHub.API/appsettings.Development.json index cce5961..032e185 100644 --- a/ProductClientHub.API/appsettings.Development.json +++ b/ProductClientHub.API/appsettings.Development.json @@ -11,6 +11,7 @@ "Port": 5672, "UserName": "guest", "Password": "guest", - "QueueName": "clients.created" + "QueueName": "clients.created", + "EnableConsumer": true } } \ No newline at end of file diff --git a/ProductClientHub.API/appsettings.json b/ProductClientHub.API/appsettings.json index 8871e92..7564d02 100644 --- a/ProductClientHub.API/appsettings.json +++ b/ProductClientHub.API/appsettings.json @@ -10,7 +10,8 @@ "Port": 5672, "UserName": "guest", "Password": "guest", - "QueueName": "clients.created" + "QueueName": "clients.created", + "EnableConsumer": true }, "AllowedHosts": "*" } diff --git a/ProductClientHub.Infrastructure/DependencyInjectionExtension.cs b/ProductClientHub.Infrastructure/DependencyInjectionExtension.cs index a09ce39..070a988 100644 --- a/ProductClientHub.Infrastructure/DependencyInjectionExtension.cs +++ b/ProductClientHub.Infrastructure/DependencyInjectionExtension.cs @@ -84,7 +84,12 @@ private static void AddMessaging(IServiceCollection services, IConfiguration con { services.Configure(configuration.GetSection(RabbitMqOptions.SectionName)); services.AddSingleton(); - services.AddHostedService(); + + var rabbitMqOptions = configuration.GetSection(RabbitMqOptions.SectionName).Get(); + if (rabbitMqOptions?.EnableConsumer is not false) + { + services.AddHostedService(); + } } private static void AddDbContext_PostgreSql(IServiceCollection services, IConfiguration configuration) diff --git a/ProductClientHub.Infrastructure/Messaging/RabbitMq/RabbitMqOptions.cs b/ProductClientHub.Infrastructure/Messaging/RabbitMq/RabbitMqOptions.cs index 3599c45..111a6fd 100644 --- a/ProductClientHub.Infrastructure/Messaging/RabbitMq/RabbitMqOptions.cs +++ b/ProductClientHub.Infrastructure/Messaging/RabbitMq/RabbitMqOptions.cs @@ -9,4 +9,5 @@ public sealed class RabbitMqOptions public string UserName { get; set; } = "guest"; public string Password { get; set; } = "guest"; public string QueueName { get; set; } = "clients.created"; + public bool EnableConsumer { get; set; } = true; } From 7af0a1c8362b0062511041c32569685e4a587179 Mon Sep 17 00:00:00 2001 From: Foqsz Date: Sun, 12 Apr 2026 23:35:29 -0300 Subject: [PATCH 08/20] # Add RabbitMQ fanout exchange and multi-queue consumers Refactor messaging to use a fanout exchange with separate audit and welcome queues for client events. Update configuration and options to support ExchangeName, ClientCreatedQueueName, and ClientWelcomeQueueName. Modify ClientCreatedConsumer to bind to the exchange and consume from the audit queue. Add ClientWelcomeConsumer to process welcome messages. Update publisher to publish to the exchange. Register both consumers as hosted services. Enables multiple consumers to independently process client creation events. --- .../appsettings.Development.json | 3 + ProductClientHub.API/appsettings.json | 3 + .../DependencyInjectionExtension.cs | 1 + .../RabbitMq/ClientCreatedConsumer.cs | 20 +++- .../RabbitMq/ClientWelcomeConsumer.cs | 97 +++++++++++++++++++ .../Messaging/RabbitMq/RabbitMqOptions.cs | 3 + .../Messaging/RabbitMq/RabbitMqPublisher.cs | 8 +- 7 files changed, 128 insertions(+), 7 deletions(-) create mode 100644 ProductClientHub.Infrastructure/Messaging/RabbitMq/ClientWelcomeConsumer.cs diff --git a/ProductClientHub.API/appsettings.Development.json b/ProductClientHub.API/appsettings.Development.json index 032e185..59e5c13 100644 --- a/ProductClientHub.API/appsettings.Development.json +++ b/ProductClientHub.API/appsettings.Development.json @@ -12,6 +12,9 @@ "UserName": "guest", "Password": "guest", "QueueName": "clients.created", + "ExchangeName": "clients.exchange", + "ClientCreatedQueueName": "clients.created.audit", + "ClientWelcomeQueueName": "clients.created.welcome", "EnableConsumer": true } } \ No newline at end of file diff --git a/ProductClientHub.API/appsettings.json b/ProductClientHub.API/appsettings.json index 7564d02..f9c5ea6 100644 --- a/ProductClientHub.API/appsettings.json +++ b/ProductClientHub.API/appsettings.json @@ -11,6 +11,9 @@ "UserName": "guest", "Password": "guest", "QueueName": "clients.created", + "ExchangeName": "clients.exchange", + "ClientCreatedQueueName": "clients.created.audit", + "ClientWelcomeQueueName": "clients.created.welcome", "EnableConsumer": true }, "AllowedHosts": "*" diff --git a/ProductClientHub.Infrastructure/DependencyInjectionExtension.cs b/ProductClientHub.Infrastructure/DependencyInjectionExtension.cs index 070a988..00da013 100644 --- a/ProductClientHub.Infrastructure/DependencyInjectionExtension.cs +++ b/ProductClientHub.Infrastructure/DependencyInjectionExtension.cs @@ -89,6 +89,7 @@ private static void AddMessaging(IServiceCollection services, IConfiguration con if (rabbitMqOptions?.EnableConsumer is not false) { services.AddHostedService(); + services.AddHostedService(); } } diff --git a/ProductClientHub.Infrastructure/Messaging/RabbitMq/ClientCreatedConsumer.cs b/ProductClientHub.Infrastructure/Messaging/RabbitMq/ClientCreatedConsumer.cs index 24f5191..1298c1b 100644 --- a/ProductClientHub.Infrastructure/Messaging/RabbitMq/ClientCreatedConsumer.cs +++ b/ProductClientHub.Infrastructure/Messaging/RabbitMq/ClientCreatedConsumer.cs @@ -33,27 +33,41 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken) _connection = await factory.CreateConnectionAsync(stoppingToken); _channel = await _connection.CreateChannelAsync(cancellationToken: stoppingToken); + await _channel.ExchangeDeclareAsync( + exchange: _options.ExchangeName, + type: ExchangeType.Fanout, + durable: true, + autoDelete: false, + arguments: null, + cancellationToken: stoppingToken); + await _channel.QueueDeclareAsync( - queue: _options.QueueName, + queue: _options.ClientCreatedQueueName, durable: true, exclusive: false, autoDelete: false, arguments: null, cancellationToken: stoppingToken); + await _channel.QueueBindAsync( + queue: _options.ClientCreatedQueueName, + exchange: _options.ExchangeName, + routingKey: string.Empty, + cancellationToken: stoppingToken); + var consumer = new AsyncEventingBasicConsumer(_channel); consumer.ReceivedAsync += async (_, eventArgs) => { var body = eventArgs.Body.ToArray(); var message = Encoding.UTF8.GetString(body); - _logger.LogInformation("Mensagem recebida da fila {Queue}: {Message}", _options.QueueName, message); + _logger.LogInformation("Mensagem recebida da fila {Queue}: {Message}", _options.ClientCreatedQueueName, message); await _channel.BasicAckAsync(eventArgs.DeliveryTag, multiple: false, cancellationToken: stoppingToken); }; await _channel.BasicConsumeAsync( - queue: _options.QueueName, + queue: _options.ClientCreatedQueueName, autoAck: false, consumer: consumer, cancellationToken: stoppingToken); diff --git a/ProductClientHub.Infrastructure/Messaging/RabbitMq/ClientWelcomeConsumer.cs b/ProductClientHub.Infrastructure/Messaging/RabbitMq/ClientWelcomeConsumer.cs new file mode 100644 index 0000000..cd3ca55 --- /dev/null +++ b/ProductClientHub.Infrastructure/Messaging/RabbitMq/ClientWelcomeConsumer.cs @@ -0,0 +1,97 @@ +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using RabbitMQ.Client; +using RabbitMQ.Client.Events; +using System.Text; +using System.Text.Json; + +namespace ProductClientHub.Infrastructure.Messaging.RabbitMq; + +public sealed class ClientWelcomeConsumer : BackgroundService +{ + private readonly RabbitMqOptions _options; + private readonly ILogger _logger; + private IConnection? _connection; + private IChannel? _channel; + + public ClientWelcomeConsumer(IOptions options, ILogger logger) + { + _options = options.Value; + _logger = logger; + } + + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + var factory = new ConnectionFactory + { + HostName = _options.HostName, + Port = _options.Port, + UserName = _options.UserName, + Password = _options.Password + }; + + _connection = await factory.CreateConnectionAsync(stoppingToken); + _channel = await _connection.CreateChannelAsync(cancellationToken: stoppingToken); + + await _channel.ExchangeDeclareAsync( + exchange: _options.ExchangeName, + type: ExchangeType.Fanout, + durable: true, + autoDelete: false, + arguments: null, + cancellationToken: stoppingToken); + + await _channel.QueueDeclareAsync( + queue: _options.ClientWelcomeQueueName, + durable: true, + exclusive: false, + autoDelete: false, + arguments: null, + cancellationToken: stoppingToken); + + await _channel.QueueBindAsync( + queue: _options.ClientWelcomeQueueName, + exchange: _options.ExchangeName, + routingKey: string.Empty, + cancellationToken: stoppingToken); + + var consumer = new AsyncEventingBasicConsumer(_channel); + consumer.ReceivedAsync += async (_, eventArgs) => + { + var body = eventArgs.Body.ToArray(); + var message = Encoding.UTF8.GetString(body); + + var name = "cliente"; + using var document = JsonDocument.Parse(message); + if (document.RootElement.TryGetProperty("Name", out var nameProperty) || + document.RootElement.TryGetProperty("name", out nameProperty)) + { + name = nameProperty.GetString() ?? name; + } + + _logger.LogInformation("Boas-vindas enviadas para {Name}", name); + + await _channel.BasicAckAsync(eventArgs.DeliveryTag, multiple: false, cancellationToken: stoppingToken); + }; + + await _channel.BasicConsumeAsync( + queue: _options.ClientWelcomeQueueName, + autoAck: false, + consumer: consumer, + cancellationToken: stoppingToken); + + await Task.Delay(Timeout.Infinite, stoppingToken); + } + + public override async Task StopAsync(CancellationToken cancellationToken) + { + if (_channel is not null) + await _channel.DisposeAsync(); + + if (_connection is not null) + await _connection.DisposeAsync(); + + await base.StopAsync(cancellationToken); + } +} diff --git a/ProductClientHub.Infrastructure/Messaging/RabbitMq/RabbitMqOptions.cs b/ProductClientHub.Infrastructure/Messaging/RabbitMq/RabbitMqOptions.cs index 111a6fd..e5d89fe 100644 --- a/ProductClientHub.Infrastructure/Messaging/RabbitMq/RabbitMqOptions.cs +++ b/ProductClientHub.Infrastructure/Messaging/RabbitMq/RabbitMqOptions.cs @@ -9,5 +9,8 @@ public sealed class RabbitMqOptions public string UserName { get; set; } = "guest"; public string Password { get; set; } = "guest"; public string QueueName { get; set; } = "clients.created"; + public string ExchangeName { get; set; } = "clients.exchange"; + public string ClientCreatedQueueName { get; set; } = "clients.created.audit"; + public string ClientWelcomeQueueName { get; set; } = "clients.created.welcome"; public bool EnableConsumer { get; set; } = true; } diff --git a/ProductClientHub.Infrastructure/Messaging/RabbitMq/RabbitMqPublisher.cs b/ProductClientHub.Infrastructure/Messaging/RabbitMq/RabbitMqPublisher.cs index d40cfed..550e726 100644 --- a/ProductClientHub.Infrastructure/Messaging/RabbitMq/RabbitMqPublisher.cs +++ b/ProductClientHub.Infrastructure/Messaging/RabbitMq/RabbitMqPublisher.cs @@ -28,10 +28,10 @@ public async Task PublishAsync(string queueName, T message, CancellationToken await using var connection = await factory.CreateConnectionAsync(cancellationToken); await using var channel = await connection.CreateChannelAsync(cancellationToken: cancellationToken); - await channel.QueueDeclareAsync( - queue: queueName, + await channel.ExchangeDeclareAsync( + exchange: _options.ExchangeName, + type: ExchangeType.Fanout, durable: true, - exclusive: false, autoDelete: false, arguments: null, cancellationToken: cancellationToken); @@ -42,7 +42,7 @@ await channel.QueueDeclareAsync( var properties = new BasicProperties { Persistent = true }; await channel.BasicPublishAsync( - exchange: string.Empty, + exchange: _options.ExchangeName, routingKey: queueName, mandatory: false, basicProperties: properties, From 03daf8bbdf037c9fb13f0ecae4d7f7531d8e246b Mon Sep 17 00:00:00 2001 From: Foqsz Date: Tue, 14 Apr 2026 10:23:03 -0300 Subject: [PATCH 09/20] # Fix ChangePassword logic and add unit tests Corrected password verification in ChangePasswordUseCase to validate the current password instead of the new one. Added RequestChangePasswordBuilder for test data creation. Introduced ChangePasswordUseCaseTest with cases for success, client not found, and invalid password scenarios. --- .../Requests/RequestChangePasswordBuilder.cs | 15 ++++ .../ChangePassword/ChangePasswordUseCase.cs | 4 +- .../ChangePasswordUseCaseTest.cs | 73 +++++++++++++++++++ 3 files changed, 90 insertions(+), 2 deletions(-) create mode 100644 CommonTestUtilities/Requests/RequestChangePasswordBuilder.cs create mode 100644 UseCase.Test/Client/ChangePassword/ChangePasswordUseCaseTest.cs diff --git a/CommonTestUtilities/Requests/RequestChangePasswordBuilder.cs b/CommonTestUtilities/Requests/RequestChangePasswordBuilder.cs new file mode 100644 index 0000000..1286077 --- /dev/null +++ b/CommonTestUtilities/Requests/RequestChangePasswordBuilder.cs @@ -0,0 +1,15 @@ +using ProductClientHub.Communication.Requests; + +namespace CommonTestUtilities.Requests; + +public class RequestChangePasswordBuilder +{ + public static RequestChangePassword Build(string currentPassword, string newPassword) + { + return new RequestChangePassword + { + CurrentPassword = currentPassword, + NewPassword = newPassword + }; + } +} diff --git a/ProductClientHub.Application/UseCases/Clients/ChangePassword/ChangePasswordUseCase.cs b/ProductClientHub.Application/UseCases/Clients/ChangePassword/ChangePasswordUseCase.cs index 27faa97..7939379 100644 --- a/ProductClientHub.Application/UseCases/Clients/ChangePassword/ChangePasswordUseCase.cs +++ b/ProductClientHub.Application/UseCases/Clients/ChangePassword/ChangePasswordUseCase.cs @@ -34,9 +34,9 @@ public async Task Execute(Guid clientId, RequestChangePassword request) if(client is null) throw new NotFoundException(ResourceMessagesExceptions.CLIENT_NOCONTENT); - var changeVerify = _passwordEncripter.IsValid(request.NewPassword, client.Password); + var changeVerify = _passwordEncripter.IsValid(request.CurrentPassword, client.Password); - if(changeVerify.IsTrue()) + if(changeVerify.IsFalse()) throw new ChangePasswordException(ResourceMessagesExceptions.LOGIN_INVALID); client.Password = _passwordEncripter.Encrypt(request.NewPassword); diff --git a/UseCase.Test/Client/ChangePassword/ChangePasswordUseCaseTest.cs b/UseCase.Test/Client/ChangePassword/ChangePasswordUseCaseTest.cs new file mode 100644 index 0000000..e7a5720 --- /dev/null +++ b/UseCase.Test/Client/ChangePassword/ChangePasswordUseCaseTest.cs @@ -0,0 +1,73 @@ +using CommonTestUtilities.Cryptografhy; +using CommonTestUtilities.Entities; +using CommonTestUtilities.Repositories; +using CommonTestUtilities.Requests; +using ProductClientHub.Application.UseCases.Clients.ChangePassword; +using ProductClientHub.Domain.Extensions; +using ProductClientHub.Exceptions.ExceptionsBase; +using Shouldly; + +namespace UseCase.Test.Client.ChangePassword; + +public class ChangePasswordUseCaseTest +{ + [Fact] + public async Task ChangePasswordUseCase_Success() + { + (var client, var password) = ClientBuilder.Build(); + + var newPassword = RequestChangePasswordBuilder.Build(currentPassword: password, newPassword: "newPassword123"); + + var useCase = CreateUseCase(client, clientNull: false); + + var response = useCase.Execute(client.Id, newPassword); + + await response.ShouldNotBeNull(); + response.IsCompletedSuccessfully.ShouldBeTrue(); + response.IsCompleted.ShouldBeTrue(); + } + + [Fact] + public async Task ChangePasswordUseCase_ClientNull() + { + (var client, var password) = ClientBuilder.Build(); + + var newPassword = RequestChangePasswordBuilder.Build(currentPassword: password, newPassword: "newPassword123"); + + var useCase = CreateUseCase(client, clientNull: true); + + var exception = await Should.ThrowAsync(async () => await useCase.Execute(client.Id, newPassword)); + + exception.Message.ShouldBe(ResourceMessagesExceptions.CLIENT_NOCONTENT); + } + + [Fact] + public async Task ChangePasswordUseCase_PasswordInvalid() + { + (var client, _) = ClientBuilder.Build(); + + var newPassword = RequestChangePasswordBuilder.Build(currentPassword: "123123", newPassword: "newpassword123"); + + var useCase = CreateUseCase(client, clientNull: false); + + var exception = await Should.ThrowAsync(async () => await useCase.Execute(client.Id, newPassword)); + + exception.Message.ShouldBe(ResourceMessagesExceptions.LOGIN_INVALID); + } + + private static ChangePasswordUseCase CreateUseCase(ProductClientHub.Domain.Entities.Client? client, bool clientNull) + { + var passwordEncripter = PasswordEncripterBuilder.Build(); + var clientWriteOnlyRepository = ClientWriteOnlyRepositoryBuilder.Build(); + var clientReadOnlyRepository = new ClientReadOnlyRepositoryBuilder(); + var unitOfWork = UnitOfWorkBuilder.Build(); + + if(client is not null && clientNull.IsFalse()) + { + clientReadOnlyRepository.GetById(client); + passwordEncripter.Encrypt(client!.Password); + } + + return new ChangePasswordUseCase(passwordEncripter, clientWriteOnlyRepository, unitOfWork, clientReadOnlyRepository.Build()); + } +} From 91cba1de3007d207c0239fdf082916f5be7c6fde Mon Sep 17 00:00:00 2001 From: Foqsz Date: Wed, 15 Apr 2026 10:29:02 -0300 Subject: [PATCH 10/20] # Add integration tests for Get All Clients endpoint Introduce integration testing setup for the ProductClientHub API, including a custom WebApplicationFactory with in-memory configuration and fake dependencies. Add integration tests for the GET /api/clients endpoint, covering both success and no-content scenarios. Update Program.cs to skip DB migration in testing, add a partial Program class for test referencing, and update the test project file to reference the API. Remove minor unrelated/conflicting code. --- ProductClientHub.API/Program.Public.cs | 3 + ProductClientHub.API/Program.cs | 3 +- .../RabbitMq/ClientCreatedConsumer.cs | 1 - .../GetAll/GetAllClientsIntegrationTest.cs | 72 +++++++++++++++++ WebApi.Test/CustomWebApplicationFactory.cs | 80 +++++++++++++++++++ WebApi.Test/WebApi.Test.csproj | 4 + 6 files changed, 161 insertions(+), 2 deletions(-) create mode 100644 ProductClientHub.API/Program.Public.cs create mode 100644 WebApi.Test/Client/GetAll/GetAllClientsIntegrationTest.cs create mode 100644 WebApi.Test/CustomWebApplicationFactory.cs diff --git a/ProductClientHub.API/Program.Public.cs b/ProductClientHub.API/Program.Public.cs new file mode 100644 index 0000000..03bc67e --- /dev/null +++ b/ProductClientHub.API/Program.Public.cs @@ -0,0 +1,3 @@ +public partial class Program +{ +} diff --git a/ProductClientHub.API/Program.cs b/ProductClientHub.API/Program.cs index f7050da..96b50a6 100644 --- a/ProductClientHub.API/Program.cs +++ b/ProductClientHub.API/Program.cs @@ -107,7 +107,8 @@ [new OpenApiSecuritySchemeReference(AUTHENTICATION_TYPE, document)] = [] app.MapControllers(); -MigrateDatabase(); +if (app.Environment.IsEnvironment("Testing").Equals(false)) + MigrateDatabase(); app.Run(); diff --git a/ProductClientHub.Infrastructure/Messaging/RabbitMq/ClientCreatedConsumer.cs b/ProductClientHub.Infrastructure/Messaging/RabbitMq/ClientCreatedConsumer.cs index 4ba1f71..a6138aa 100644 --- a/ProductClientHub.Infrastructure/Messaging/RabbitMq/ClientCreatedConsumer.cs +++ b/ProductClientHub.Infrastructure/Messaging/RabbitMq/ClientCreatedConsumer.cs @@ -46,7 +46,6 @@ await _channel.QueueDeclareAsync( durable: true, exclusive: false, autoDelete: false, -https://github.com/Foqsz/ProductClientHub/pull/31/conflict?name=ProductClientHub.Infrastructure%252FMessaging%252FRabbitMq%252FRabbitMqPublisher.cs&base_oid=550e726c94d056a6d7a9eca0fd1c987fc2baa257&head_oid=d40cfeda05d2ea167e1c6aaa27c0a79700781e44 arguments: null, cancellationToken: stoppingToken); await _channel.QueueBindAsync( diff --git a/WebApi.Test/Client/GetAll/GetAllClientsIntegrationTest.cs b/WebApi.Test/Client/GetAll/GetAllClientsIntegrationTest.cs new file mode 100644 index 0000000..23ccb1a --- /dev/null +++ b/WebApi.Test/Client/GetAll/GetAllClientsIntegrationTest.cs @@ -0,0 +1,72 @@ +using System.Net; +using System.Net.Http.Headers; +using System.Net.Http.Json; +using Microsoft.AspNetCore.Mvc.Testing; +using ClientEntity = ProductClientHub.Domain.Entities.Client; +using ProductClientHub.Communication.Responses; +using Shouldly; + +namespace WebApi.Test.Client.GetAll; + +public class GetAllClientsIntegrationTest : IClassFixture +{ + private readonly CustomWebApplicationFactory _factory; + private readonly HttpClient _httpClient; + + public GetAllClientsIntegrationTest(CustomWebApplicationFactory factory) + { + _factory = factory; + _httpClient = factory.CreateClient(new WebApplicationFactoryClientOptions + { + BaseAddress = new Uri("https://localhost") + }); + + _httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", "fake-token"); + } + + [Fact] + public async Task GetAll_ShouldReturnSucess_WhenThereAreClientsInTheSystem() + { + _factory.ClientsToReturn = + [ + new ClientEntity + { + Id = Guid.Parse("aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"), + Name = "Client 1", + Email = "client1@email.com", + Password = "password123" + }, + new ClientEntity + { + Id = Guid.Parse("bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb"), + Name = "Client 2", + Email = "client2@email.com", + Password = "password456" + } + ]; + + var response = await _httpClient.GetAsync("/api/clients"); + + response.StatusCode.ShouldBe(HttpStatusCode.OK); + + var body = await response.Content.ReadFromJsonAsync(); + + body.ShouldNotBeNull(); + body!.Clients.Count.ShouldBe(2); + body.Clients[0].Id.ShouldBe(Guid.Parse("aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa")); + body.Clients[0].Name.ShouldBe("Client 1"); + body.Clients[1].Id.ShouldBe(Guid.Parse("bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb")); + body.Clients[1].Name.ShouldBe("Client 2"); + } + + + [Fact] + public async Task GetAll_ShouldReturnNoContent_WhenThereAreNoClients() + { + _factory.ClientsToReturn = []; + + var response = await _httpClient.GetAsync("/api/clients"); + + response.StatusCode.ShouldBe(HttpStatusCode.NoContent); + } +} diff --git a/WebApi.Test/CustomWebApplicationFactory.cs b/WebApi.Test/CustomWebApplicationFactory.cs new file mode 100644 index 0000000..e1151a3 --- /dev/null +++ b/WebApi.Test/CustomWebApplicationFactory.cs @@ -0,0 +1,80 @@ +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Mvc.Testing; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using ClientEntity = ProductClientHub.Domain.Entities.Client; +using ProductClientHub.Domain.Repositories.Client; +using ProductClientHub.Domain.Security.Tokens; + +namespace WebApi.Test; + +public class CustomWebApplicationFactory : WebApplicationFactory +{ + private readonly TestClientStore _clientStore = new(); + + public IList ClientsToReturn + { + get => _clientStore.Clients; + set => _clientStore.Clients = value; + } + + protected override void ConfigureWebHost(IWebHostBuilder builder) + { + builder.UseEnvironment("Testing"); + + builder.ConfigureAppConfiguration((_, configBuilder) => + { + configBuilder.AddInMemoryCollection(new Dictionary + { + ["ConnectionStrings:Connection"] = "Host=localhost;Port=5432;Database=ignored;Username=ignored;Password=ignored", + ["Jwt:SigningKey"] = "integration_tests_signing_key_12345678901234567890", + ["Jwt:ExpirationTimeMinutes"] = "60", + ["RabbitMQ:EnableConsumer"] = "false" + }); + }); + + builder.ConfigureServices(services => + { + services.AddSingleton(_clientStore); + + services.RemoveAll(); + services.RemoveAll(); + + services.AddScoped(); + services.AddScoped(); + }); + } + + private sealed class FakeAccessTokenValidator : IAccessTokenValidator + { + public Guid ValidateAnGetUserIdentifier(string token) + => Guid.Parse("11111111-1111-1111-1111-111111111111"); + } + + private sealed class FakeClientReadOnlyRepository : IClientReadOnlyRepository + { + private readonly TestClientStore _clientStore; + + public FakeClientReadOnlyRepository(TestClientStore clientStore) + { + _clientStore = clientStore; + } + + public Task EmailAlreadyExists(string email) + => Task.FromResult(null); + + public Task> GetAll() + => Task.FromResult>(_clientStore.Clients); + + public Task GetById(Guid clientId) + => Task.FromResult(null); + + public Task ExistActiveClientWithIdentifier(Guid clientIdentifier) => Task.FromResult(true); + } + + private sealed class TestClientStore + { + public IList Clients { get; set; } = []; + } +} diff --git a/WebApi.Test/WebApi.Test.csproj b/WebApi.Test/WebApi.Test.csproj index 10af298..b8dcfae 100644 --- a/WebApi.Test/WebApi.Test.csproj +++ b/WebApi.Test/WebApi.Test.csproj @@ -20,4 +20,8 @@ + + + + \ No newline at end of file From a57d1799331cddb853e007f40dfb9019799a5a1d Mon Sep 17 00:00:00 2001 From: Foqsz Date: Wed, 15 Apr 2026 11:51:01 -0300 Subject: [PATCH 11/20] # Implement GetById logic and add integration tests Updated CustomWebApplicationFactory to return clients by ID from the in-memory store. Added GetByIdClientIntegrationTest to verify GET /api/clients/{id} returns 200 OK for existing clients and 404 Not Found for missing clients, using xUnit and Shouldly. --- .../GetById/GetByIdClientIntegrationTest.cs | 63 +++++++++++++++++++ WebApi.Test/CustomWebApplicationFactory.cs | 5 +- 2 files changed, 67 insertions(+), 1 deletion(-) create mode 100644 WebApi.Test/Client/GetById/GetByIdClientIntegrationTest.cs diff --git a/WebApi.Test/Client/GetById/GetByIdClientIntegrationTest.cs b/WebApi.Test/Client/GetById/GetByIdClientIntegrationTest.cs new file mode 100644 index 0000000..fddeae8 --- /dev/null +++ b/WebApi.Test/Client/GetById/GetByIdClientIntegrationTest.cs @@ -0,0 +1,63 @@ +using Microsoft.AspNetCore.Mvc.Testing; +using Shouldly; +using System.Net.Http.Headers; +using ClientEntity = ProductClientHub.Domain.Entities.Client; + +namespace WebApi.Test.Client.GetById; + +public class GetByIdClientIntegrationTest : IClassFixture +{ + private readonly CustomWebApplicationFactory _factory; + private readonly HttpClient _httpClient; + + public GetByIdClientIntegrationTest(CustomWebApplicationFactory factory) + { + _factory = factory; + _httpClient = factory.CreateClient(new WebApplicationFactoryClientOptions + { + BaseAddress = new Uri("https://localhost") + }); + + _httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", "fake-token"); + } + + [Fact] + public async Task GetClientById_ShouldReturnSucess() + { + var clientId = Guid.Parse("aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"); + + _factory.ClientsToReturn = + [ + new ClientEntity + { + Id = clientId, + Name = "Client 1", + Email = "client@gmail.com", + Password = "teste123", + } + ]; + + var response = await _httpClient.GetAsync($"/api/clients/{clientId}"); + + response.StatusCode.ShouldBe(System.Net.HttpStatusCode.OK); + } + + [Fact] + public async Task GetClientById_ShouldReturnNotFound() + { + _factory.ClientsToReturn = + [ + new ClientEntity + { + Id = Guid.Parse("12345678-1234-1234-1234-123456789012"), + Name = "Client 1", + Email = "clientt@gmail.com", + Password = "teste123", + } + ]; + + var response = await _httpClient.GetAsync($"/api/clients/{Guid.Parse("aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa")}"); + + response.StatusCode.ShouldBe(System.Net.HttpStatusCode.NotFound); + } +} diff --git a/WebApi.Test/CustomWebApplicationFactory.cs b/WebApi.Test/CustomWebApplicationFactory.cs index e1151a3..9504e1e 100644 --- a/WebApi.Test/CustomWebApplicationFactory.cs +++ b/WebApi.Test/CustomWebApplicationFactory.cs @@ -68,7 +68,10 @@ public Task> GetAll() => Task.FromResult>(_clientStore.Clients); public Task GetById(Guid clientId) - => Task.FromResult(null); + { + var client = _clientStore.Clients.FirstOrDefault(c => c.Id == clientId); + return Task.FromResult(client); + } public Task ExistActiveClientWithIdentifier(Guid clientIdentifier) => Task.FromResult(true); } From 9c4ad17fbf2b1abb523abce95965c1df3375a0de Mon Sep 17 00:00:00 2001 From: Foqsz Date: Thu, 16 Apr 2026 13:25:28 -0300 Subject: [PATCH 12/20] # Add integration test for client deletion endpoint Introduce DeleteClientIntegrationTest to verify client deletion via the API using an in-memory store and fake authentication. Update CustomWebApplicationFactory to support test isolation by removing hosted services and providing a FakeDeleteClientRepository. This enables reliable, database-independent integration testing for client deletion. --- .../Delete/DeleteClientIntegrationTest.cs | 44 +++++++++++++++++++ WebApi.Test/CustomWebApplicationFactory.cs | 26 +++++++++++ 2 files changed, 70 insertions(+) create mode 100644 WebApi.Test/Client/Delete/DeleteClientIntegrationTest.cs diff --git a/WebApi.Test/Client/Delete/DeleteClientIntegrationTest.cs b/WebApi.Test/Client/Delete/DeleteClientIntegrationTest.cs new file mode 100644 index 0000000..53667da --- /dev/null +++ b/WebApi.Test/Client/Delete/DeleteClientIntegrationTest.cs @@ -0,0 +1,44 @@ +using Microsoft.AspNetCore.Mvc.Testing; +using Shouldly; +using System.Net.Http.Headers; + +namespace WebApi.Test.Client.Delete; + +public class DeleteClientIntegrationTest : IClassFixture +{ + private readonly CustomWebApplicationFactory _factory; + private readonly HttpClient _httpClient; + + public DeleteClientIntegrationTest(CustomWebApplicationFactory factory) + { + _factory = factory; + _httpClient = _factory.CreateClient(new WebApplicationFactoryClientOptions + { + BaseAddress = new Uri("https://localhost") + }); + + _httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", "fake-token"); + } + + + [Fact] + public async Task Delete_ShouldReturnNoContent_WhenClientIsDeleted() + { + var clientId = Guid.Parse("aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"); + + _factory.ClientsToReturn = + [ + new ProductClientHub.Domain.Entities.Client + { + Id = clientId, + Name = "Client 1", + Email = "oioi@gmail.com", + Password = "password123", + Products = new List() + } + ]; + + var response = await _httpClient.DeleteAsync($"/api/clients/{clientId}"); + response.StatusCode.ShouldBe(System.Net.HttpStatusCode.NoContent); + } +} \ No newline at end of file diff --git a/WebApi.Test/CustomWebApplicationFactory.cs b/WebApi.Test/CustomWebApplicationFactory.cs index 9504e1e..9266e41 100644 --- a/WebApi.Test/CustomWebApplicationFactory.cs +++ b/WebApi.Test/CustomWebApplicationFactory.cs @@ -3,6 +3,7 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Hosting; using ClientEntity = ProductClientHub.Domain.Entities.Client; using ProductClientHub.Domain.Repositories.Client; using ProductClientHub.Domain.Security.Tokens; @@ -40,9 +41,14 @@ protected override void ConfigureWebHost(IWebHostBuilder builder) services.RemoveAll(); services.RemoveAll(); + services.RemoveAll(); + + // Remove all hosted services to prevent background services from running during tests + services.RemoveAll(typeof(IHostedService)); services.AddScoped(); services.AddScoped(); + services.AddScoped(); }); } @@ -76,6 +82,26 @@ public Task> GetAll() public Task ExistActiveClientWithIdentifier(Guid clientIdentifier) => Task.FromResult(true); } + private sealed class FakeDeleteClientRepository : IDeleteClientRepository + { + private readonly TestClientStore _clientStore; + + public FakeDeleteClientRepository(TestClientStore clientStore) + { + _clientStore = clientStore; + } + + public Task Delete(Guid clientId) + { + var client = _clientStore.Clients.FirstOrDefault(c => c.Id == clientId); + if (client != null) + { + _clientStore.Clients.Remove(client); + } + return Task.CompletedTask; + } + } + private sealed class TestClientStore { public IList Clients { get; set; } = []; From 8111877b1d58948b280f9fb5aa0c829a49258481 Mon Sep 17 00:00:00 2001 From: Foqsz Date: Sun, 19 Apr 2026 20:00:00 -0300 Subject: [PATCH 13/20] feat: remove open api --- ProductClientHub.API/Program.cs | 37 --------------------------------- 1 file changed, 37 deletions(-) diff --git a/ProductClientHub.API/Program.cs b/ProductClientHub.API/Program.cs index 96b50a6..5838f12 100644 --- a/ProductClientHub.API/Program.cs +++ b/ProductClientHub.API/Program.cs @@ -32,41 +32,6 @@ [new OpenApiSecuritySchemeReference(AUTHENTICATION_TYPE, document)] = [] }); }); -//builder.Services.AddOpenApi("v1", o => -//{ -// o.AddDocumentTransformer((document, context, CancellationToken) => -// { -// document.Info = new() -// { -// Title = "Product Client Hub API", -// Version = "v1", -// Description = "API for managing product clients and their interactions.", -// Contact = new() -// { -// Name = "Product Client Hub Team", -// Email = "teste@gmail.com" -// } -// }; -// -// document.Servers = -// [ -// new() -// { -// Url = "https://localhost:5001", -// Description = "Local Development Server" -// }, -// ]; -// -// document.ExternalDocs = new() -// { -// Description = "API Documentation", -// Url = new Uri("https://localhost:5001/swagger/index.html") -// }; -// -// return Task.CompletedTask; -// }); -//}); - //Add global exception filter builder.Services.AddMvc(option => option.Filters.Add(typeof(ExceptionFilter))); @@ -101,8 +66,6 @@ [new OpenApiSecuritySchemeReference(AUTHENTICATION_TYPE, document)] = [] app.UseCors("CorsPolicy"); -//app.MapOpenApi("/doc/{documentName}.json"); - app.UseAuthorization(); app.MapControllers(); From 1b9657a70666c91d04c7af2c3e8f2681421c9b57 Mon Sep 17 00:00:00 2001 From: Foqsz Date: Mon, 20 Apr 2026 09:38:40 -0300 Subject: [PATCH 14/20] feat: update client integration test. --- .../Update/UpdateClientIntegrationTest.cs | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 WebApi.Test/Client/Update/UpdateClientIntegrationTest.cs diff --git a/WebApi.Test/Client/Update/UpdateClientIntegrationTest.cs b/WebApi.Test/Client/Update/UpdateClientIntegrationTest.cs new file mode 100644 index 0000000..ee3a107 --- /dev/null +++ b/WebApi.Test/Client/Update/UpdateClientIntegrationTest.cs @@ -0,0 +1,43 @@ +using Microsoft.AspNetCore.Mvc.Testing; +using Newtonsoft.Json; +using Shouldly; +using System.Net.Http.Headers; +using ClientEntity = ProductClientHub.Domain.Entities.Client; + +namespace WebApi.Test.Client.Update; + +public class UpdateClientIntegrationTest : IClassFixture +{ + private readonly CustomWebApplicationFactory _factory; + private readonly HttpClient _httpClient; + + public UpdateClientIntegrationTest(CustomWebApplicationFactory factory) + { + _factory = factory; + _httpClient = factory.CreateClient(new WebApplicationFactoryClientOptions + { + BaseAddress = new Uri("https://localhost") + }); + + _httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", "fake-token"); + } + + [Fact] + public async Task UpdateClientTest_Sucess() + { + var client = _factory.ClientsToReturn = + [ + new ClientEntity + { + Name = "Update Client", + Email = "updateclient@gmail.com" + } + ]; + + client[0].Id = Guid.NewGuid(); + + var response = await _httpClient.PutAsync($"/api/clients/{client[0].Id}", new StringContent(JsonConvert.SerializeObject(client[0]), System.Text.Encoding.UTF8, "application/json")); + + response.StatusCode.ShouldBe(System.Net.HttpStatusCode.OK); + } +} From 7e385342a9650c2a17c9a9351cf7d8baaf22c862 Mon Sep 17 00:00:00 2001 From: Foqsz Date: Mon, 20 Apr 2026 09:38:51 -0300 Subject: [PATCH 15/20] =?UTF-8?q?feat:=20Moq=20realizado=20para=20a=20inte?= =?UTF-8?q?gra=C3=A7=C3=A3o=20test?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- WebApi.Test/CustomWebApplicationFactory.cs | 39 +++++++++++++++++++++- 1 file changed, 38 insertions(+), 1 deletion(-) diff --git a/WebApi.Test/CustomWebApplicationFactory.cs b/WebApi.Test/CustomWebApplicationFactory.cs index 9266e41..5412d41 100644 --- a/WebApi.Test/CustomWebApplicationFactory.cs +++ b/WebApi.Test/CustomWebApplicationFactory.cs @@ -4,9 +4,10 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Hosting; -using ClientEntity = ProductClientHub.Domain.Entities.Client; using ProductClientHub.Domain.Repositories.Client; +using ProductClientHub.Domain.Repositories.UnitOfWork; using ProductClientHub.Domain.Security.Tokens; +using ClientEntity = ProductClientHub.Domain.Entities.Client; namespace WebApi.Test; @@ -40,15 +41,19 @@ protected override void ConfigureWebHost(IWebHostBuilder builder) services.AddSingleton(_clientStore); services.RemoveAll(); + services.RemoveAll(); services.RemoveAll(); services.RemoveAll(); + services.RemoveAll(); // Remove all hosted services to prevent background services from running during tests services.RemoveAll(typeof(IHostedService)); services.AddScoped(); + services.AddScoped(); services.AddScoped(); services.AddScoped(); + services.AddScoped(); }); } @@ -102,6 +107,38 @@ public Task Delete(Guid clientId) } } + private sealed class FakeClientWriteOnlyRepository : IClientWriteOnlyRepository + { + private readonly TestClientStore _clientStore; + + public FakeClientWriteOnlyRepository(TestClientStore clientStore) + { + _clientStore = clientStore; + } + + public Task Add(ClientEntity client) + { + _clientStore.Clients.Add(client); + return Task.CompletedTask; + } + + public Task Update(ClientEntity client) + { + var existingClient = _clientStore.Clients.FirstOrDefault(c => c.Id == client.Id); + if (existingClient != null) + { + existingClient.Name = client.Name; + existingClient.Email = client.Email; + } + return Task.FromResult(existingClient); + } + } + + private sealed class FakeUnitOfWork : IUnitOfWork + { + public Task Commit() => Task.CompletedTask; + } + private sealed class TestClientStore { public IList Clients { get; set; } = []; From fee32f4148bd610fb8dfe04e8904bd3755e39818 Mon Sep 17 00:00:00 2001 From: Foqsz Date: Mon, 20 Apr 2026 10:10:38 -0300 Subject: [PATCH 16/20] feat: teste para caso ja exista um e-mail na tentativa de update. --- .../Update/UpdateClientIntegrationTest.cs | 26 +++++++++++++++++++ WebApi.Test/CustomWebApplicationFactory.cs | 5 +++- 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/WebApi.Test/Client/Update/UpdateClientIntegrationTest.cs b/WebApi.Test/Client/Update/UpdateClientIntegrationTest.cs index ee3a107..32eef69 100644 --- a/WebApi.Test/Client/Update/UpdateClientIntegrationTest.cs +++ b/WebApi.Test/Client/Update/UpdateClientIntegrationTest.cs @@ -40,4 +40,30 @@ public async Task UpdateClientTest_Sucess() response.StatusCode.ShouldBe(System.Net.HttpStatusCode.OK); } + + [Fact] + public async Task UpdateClientTest_Error_EmailExist() + { + var client = _factory.ClientsToReturn = + [ + new ClientEntity + { + Name = "Update Client", + Email = "updateclient@gmail.com" + }, + + new ClientEntity + { + Name = "Update Client 2", + Email = "updateclient@gmail.com" + } + ]; + + client[0].Id = Guid.NewGuid(); + client[1].Id = Guid.NewGuid(); + + var response = await _httpClient.PutAsync($"/api/clients/{client[1].Id}", new StringContent(JsonConvert.SerializeObject(client[1]), System.Text.Encoding.UTF8, "application/json")); + + response.StatusCode.ShouldBe(System.Net.HttpStatusCode.BadRequest); + } } diff --git a/WebApi.Test/CustomWebApplicationFactory.cs b/WebApi.Test/CustomWebApplicationFactory.cs index 5412d41..dacaa7e 100644 --- a/WebApi.Test/CustomWebApplicationFactory.cs +++ b/WebApi.Test/CustomWebApplicationFactory.cs @@ -73,7 +73,10 @@ public FakeClientReadOnlyRepository(TestClientStore clientStore) } public Task EmailAlreadyExists(string email) - => Task.FromResult(null); + { + var client = _clientStore.Clients.FirstOrDefault(c => c.Email == email); + return Task.FromResult(client); + } public Task> GetAll() => Task.FromResult>(_clientStore.Clients); From 00cb12a774980b11863f6a06d1d4fc070d7755da Mon Sep 17 00:00:00 2001 From: Foqsz Date: Wed, 22 Apr 2026 10:18:44 -0300 Subject: [PATCH 17/20] feat: adicionando logged user ao teste --- .../LoggedUser/LoggedUserBuilder.cs | 17 +++++++++++++++++ .../Controllers/ClientsController.cs | 5 ++--- .../Clients/Update/IUpdateClientUseCase.cs | 2 +- .../Clients/Update/UpdateClientUseCase.cs | 12 +++++++++--- 4 files changed, 29 insertions(+), 7 deletions(-) create mode 100644 CommonTestUtilities/LoggedUser/LoggedUserBuilder.cs diff --git a/CommonTestUtilities/LoggedUser/LoggedUserBuilder.cs b/CommonTestUtilities/LoggedUser/LoggedUserBuilder.cs new file mode 100644 index 0000000..8596660 --- /dev/null +++ b/CommonTestUtilities/LoggedUser/LoggedUserBuilder.cs @@ -0,0 +1,17 @@ +using Moq; +using ProductClientHub.Domain.Entities; +using ProductClientHub.Domain.Services.LoggedUser; + +namespace CommonTestUtilities.LoggedUser; + +public class LoggedUserBuilder +{ + public static ILoggedUser Build(Client client) + { + var mock = new Mock(); + + mock.Setup(c => c.User()).ReturnsAsync(client); + + return mock.Object; + } +} \ No newline at end of file diff --git a/ProductClientHub.API/Controllers/ClientsController.cs b/ProductClientHub.API/Controllers/ClientsController.cs index f660739..acd8151 100644 --- a/ProductClientHub.API/Controllers/ClientsController.cs +++ b/ProductClientHub.API/Controllers/ClientsController.cs @@ -27,13 +27,12 @@ public async Task ChangePassword([FromRoute] Guid clientId, [From } [HttpPut] - [Route("{clientId:guid}")] [ProducesResponseType(typeof(ResponseClientUpdatedJson), StatusCodes.Status200OK)] [ProducesResponseType(typeof(ResponseErrorMessagesJson), StatusCodes.Status404NotFound)] [ProducesResponseType(typeof(ResponseErrorMessagesJson), StatusCodes.Status400BadRequest)] - public async Task Update([FromRoute] Guid clientId, [FromBody] RequestShortClientJson request, [FromServices] IUpdateClientUseCase useCase) + public async Task Update([FromBody] RequestShortClientJson request, [FromServices] IUpdateClientUseCase useCase) { - var response = await useCase.Execute(clientId, request); + var response = await useCase.Execute(request); return Ok(response); } diff --git a/ProductClientHub.Application/UseCases/Clients/Update/IUpdateClientUseCase.cs b/ProductClientHub.Application/UseCases/Clients/Update/IUpdateClientUseCase.cs index f71a9f0..7fec365 100644 --- a/ProductClientHub.Application/UseCases/Clients/Update/IUpdateClientUseCase.cs +++ b/ProductClientHub.Application/UseCases/Clients/Update/IUpdateClientUseCase.cs @@ -5,5 +5,5 @@ namespace ProductClientHub.Application.UseCases.Users.Update; public interface IUpdateClientUseCase { - Task Execute(Guid clientId, RequestShortClientJson request); + Task Execute(RequestShortClientJson request); } diff --git a/ProductClientHub.Application/UseCases/Clients/Update/UpdateClientUseCase.cs b/ProductClientHub.Application/UseCases/Clients/Update/UpdateClientUseCase.cs index a3a9f48..1d4f5f6 100644 --- a/ProductClientHub.Application/UseCases/Clients/Update/UpdateClientUseCase.cs +++ b/ProductClientHub.Application/UseCases/Clients/Update/UpdateClientUseCase.cs @@ -5,6 +5,7 @@ using ProductClientHub.Domain.Extensions; using ProductClientHub.Domain.Repositories.Client; using ProductClientHub.Domain.Repositories.UnitOfWork; +using ProductClientHub.Domain.Services.LoggedUser; using ProductClientHub.Exceptions.ExceptionsBase; namespace ProductClientHub.Application.UseCases.Users.Update; @@ -14,21 +15,26 @@ public class UpdateClientUseCase : IUpdateClientUseCase private readonly IClientWriteOnlyRepository _clientWriteOnlyRepository; private readonly IClientReadOnlyRepository _clientReadOnlyRepository; private readonly IUnitOfWork _unitOfWork; + private readonly ILoggedUser _loggedUser; public UpdateClientUseCase(IClientWriteOnlyRepository clientWriteOnlyRepository, IClientReadOnlyRepository clientReadOnlyRepository, - IUnitOfWork unitOfWork) + IUnitOfWork unitOfWork, + ILoggedUser loggedUser) { _clientWriteOnlyRepository = clientWriteOnlyRepository; _clientReadOnlyRepository = clientReadOnlyRepository; _unitOfWork = unitOfWork; + _loggedUser = loggedUser; } - public async Task Execute(Guid clientId, RequestShortClientJson request) + public async Task Execute(RequestShortClientJson request) { Validate(request); - var client = await _clientReadOnlyRepository.GetById(clientId) ?? throw new NotFoundException(ResourceMessagesExceptions.CLIENT_NOCONTENT); + var userLogged = await _loggedUser.User(); + + var client = await _clientReadOnlyRepository.GetById(userLogged.Id) ?? throw new NotFoundException(ResourceMessagesExceptions.CLIENT_NOCONTENT); var emailExist = await _clientReadOnlyRepository.EmailAlreadyExists(request.Email); From 5c6fe048b9d5171b9ed6d7a8d90073ad27f0b249 Mon Sep 17 00:00:00 2001 From: Foqsz Date: Wed, 22 Apr 2026 10:19:28 -0300 Subject: [PATCH 18/20] feat: agora nao preciso mais do clientid no endpoint, ja recebo via logged user. --- .../Client/Update/UpdateClientUseCaseTest.cs | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/UseCase.Test/Client/Update/UpdateClientUseCaseTest.cs b/UseCase.Test/Client/Update/UpdateClientUseCaseTest.cs index 97679ed..b362745 100644 --- a/UseCase.Test/Client/Update/UpdateClientUseCaseTest.cs +++ b/UseCase.Test/Client/Update/UpdateClientUseCaseTest.cs @@ -1,10 +1,12 @@ using CommonTestUtilities.Cryptografhy; using CommonTestUtilities.Entities; +using CommonTestUtilities.LoggedUser; using CommonTestUtilities.Repositories; using CommonTestUtilities.Requests; using ProductClientHub.Application.UseCases.Users.Update; using ProductClientHub.Domain.Extensions; using ProductClientHub.Exceptions.ExceptionsBase; +using ProductClientHub.Infrastructure.Services; using Shouldly; namespace UseCase.Test.Client.Update; @@ -20,7 +22,7 @@ public async Task UpdateClient_Sucess() var useCase = CreateUseCase(client, emailExistsTest: false, clientExist: true); - var result = await useCase.Execute(client.Id, clientRequest); + var result = await useCase.Execute(clientRequest); result.ShouldNotBeNull(); result.ShouldSatisfyAllConditions( @@ -38,7 +40,7 @@ public async Task UpdateClient_Error_ClientNotExists() var useCase = CreateUseCase(client, emailExistsTest: false, clientExist: false); - var resultException = await Should.ThrowAsync(async () => await useCase.Execute(client.Id, clientRequest)); + var resultException = await Should.ThrowAsync(async () => await useCase.Execute(clientRequest)); resultException.ShouldNotBeNull(); resultException.ShouldSatisfyAllConditions(() => resultException.Message.ShouldBe(ResourceMessagesExceptions.CLIENT_NOCONTENT)); @@ -53,7 +55,7 @@ public async Task UpdateClient_Error_EmailExist() var useCase = CreateUseCase(client, emailExistsTest: true, clientExist: true); - var resultException = await Should.ThrowAsync(async () => await useCase.Execute(client.Id, clientRequest)); + var resultException = await Should.ThrowAsync(async () => await useCase.Execute(clientRequest)); resultException.ShouldNotBeNull(); resultException.ShouldSatisfyAllConditions(() => resultException.Message.ShouldBe(ResourceMessagesExceptions.EMAIL_INVALID)); @@ -64,13 +66,14 @@ private static UpdateClientUseCase CreateUseCase(ProductClientHub.Domain.Entitie var clientWriteOnlyRepository = ClientWriteOnlyRepositoryBuilder.Build(); var clientReadOnlyRepository = new ClientReadOnlyRepositoryBuilder(); var unitOfWork = UnitOfWorkBuilder.Build(); + var loggedUser = LoggedUserBuilder.Build(client!); - if(client is not null && clientExist.IsTrue()) + if (client is not null && clientExist.IsTrue()) clientReadOnlyRepository.GetById(client); if(emailExistsTest.IsTrue()) clientReadOnlyRepository.EmailAlreadyExists(client); - return new UpdateClientUseCase(clientWriteOnlyRepository, clientReadOnlyRepository.Build(), unitOfWork); + return new UpdateClientUseCase(clientWriteOnlyRepository, clientReadOnlyRepository.Build(), unitOfWork, loggedUser); } } From adeeacba3918edb9d2086d31165f35d5b1ee6b89 Mon Sep 17 00:00:00 2001 From: Foqsz Date: Fri, 24 Apr 2026 09:52:04 -0300 Subject: [PATCH 19/20] =?UTF-8?q?fix:=20fiz=20esse=20ajuste=20pq=20mesmo?= =?UTF-8?q?=20se=20o=20usuario=20tentasse=20trocar=20seu=20e-mail=20pra=20?= =?UTF-8?q?o=20atual,=20estava=20disparando=20erro,=20o=20que=20nao=20faz?= =?UTF-8?q?=20sentido,=20se=20o=20e-mail=20=C3=A9=20dele,=20pode=20permane?= =?UTF-8?q?cer=20asssim.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../UseCases/Clients/Update/UpdateClientUseCase.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ProductClientHub.Application/UseCases/Clients/Update/UpdateClientUseCase.cs b/ProductClientHub.Application/UseCases/Clients/Update/UpdateClientUseCase.cs index 1d4f5f6..9ca9d96 100644 --- a/ProductClientHub.Application/UseCases/Clients/Update/UpdateClientUseCase.cs +++ b/ProductClientHub.Application/UseCases/Clients/Update/UpdateClientUseCase.cs @@ -38,7 +38,7 @@ public async Task Execute(RequestShortClientJson requ var emailExist = await _clientReadOnlyRepository.EmailAlreadyExists(request.Email); - if(emailExist is not null) + if(emailExist is not null && emailExist.Id != userLogged.Id) throw new EmailAlreadyExistsException(ResourceMessagesExceptions.EMAIL_INVALID); client.Name = request.Name; From 68a0b6441a1ed40218307206fff553fcddc7f880 Mon Sep 17 00:00:00 2001 From: Foqsz Date: Fri, 24 Apr 2026 09:52:21 -0300 Subject: [PATCH 20/20] =?UTF-8?q?feat:=20teste=20de=20integra=C3=A7=C3=A3o?= =?UTF-8?q?=20para=20o=20update=20do=20usuario.=20Tudo=20ok,=20devidamente?= =?UTF-8?q?=20testado.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Token/JwtTokenGeneratorBuilder.cs | 9 ++++ .../Update/UpdateClientIntegrationTest.cs | 54 +++++++++++++------ WebApi.Test/CustomWebApplicationFactory.cs | 19 +++++++ WebApi.Test/WebApi.Test.csproj | 1 + 4 files changed, 67 insertions(+), 16 deletions(-) create mode 100644 CommonTestUtilities/Token/JwtTokenGeneratorBuilder.cs diff --git a/CommonTestUtilities/Token/JwtTokenGeneratorBuilder.cs b/CommonTestUtilities/Token/JwtTokenGeneratorBuilder.cs new file mode 100644 index 0000000..33e4f60 --- /dev/null +++ b/CommonTestUtilities/Token/JwtTokenGeneratorBuilder.cs @@ -0,0 +1,9 @@ +using ProductClientHub.Domain.Security.Tokens; +using ProductClientHub.Infrastructure.Security.Tokens.Acess.Generator; + +namespace CommonTestUtilities.Token; + +public class JwtTokenGeneratorBuilder +{ + public static IAccessTokenGenerator Build() => new JwtTokenGenerator(expirationTimeMinutes: 5, signingKey: "ttttttttttttttttttttttttttttttttttttttttt"); +} diff --git a/WebApi.Test/Client/Update/UpdateClientIntegrationTest.cs b/WebApi.Test/Client/Update/UpdateClientIntegrationTest.cs index 32eef69..a2c7d91 100644 --- a/WebApi.Test/Client/Update/UpdateClientIntegrationTest.cs +++ b/WebApi.Test/Client/Update/UpdateClientIntegrationTest.cs @@ -1,7 +1,11 @@ -using Microsoft.AspNetCore.Mvc.Testing; +using CommonTestUtilities.Token; +using Microsoft.AspNetCore.Mvc.Testing; using Newtonsoft.Json; +using Newtonsoft.Json.Linq; using Shouldly; +using System.Net; using System.Net.Http.Headers; +using System.Text; using ClientEntity = ProductClientHub.Domain.Entities.Client; namespace WebApi.Test.Client.Update; @@ -18,8 +22,6 @@ public UpdateClientIntegrationTest(CustomWebApplicationFactory factory) { BaseAddress = new Uri("https://localhost") }); - - _httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", "fake-token"); } [Fact] @@ -30,13 +32,17 @@ public async Task UpdateClientTest_Sucess() new ClientEntity { Name = "Update Client", - Email = "updateclient@gmail.com" - } + Email = "updateclientTEST@gmail.com" + }, ]; client[0].Id = Guid.NewGuid(); - var response = await _httpClient.PutAsync($"/api/clients/{client[0].Id}", new StringContent(JsonConvert.SerializeObject(client[0]), System.Text.Encoding.UTF8, "application/json")); + var token = JwtTokenGeneratorBuilder.Build().Generate(client[0].Id); + + _httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token); + + var response = await _httpClient.PutAsync($"/api/clients", new StringContent(JsonConvert.SerializeObject(client[0]), System.Text.Encoding.UTF8, "application/json")); response.StatusCode.ShouldBe(System.Net.HttpStatusCode.OK); } @@ -47,23 +53,39 @@ public async Task UpdateClientTest_Error_EmailExist() var client = _factory.ClientsToReturn = [ new ClientEntity - { - Name = "Update Client", - Email = "updateclient@gmail.com" - }, + { + Name = "User Logado", + Email = "user1@gmail.com" + }, new ClientEntity - { - Name = "Update Client 2", - Email = "updateclient@gmail.com" - } + { + Name = "Outro User", + Email = "user2@gmail.com" + } ]; client[0].Id = Guid.NewGuid(); client[1].Id = Guid.NewGuid(); - var response = await _httpClient.PutAsync($"/api/clients/{client[1].Id}", new StringContent(JsonConvert.SerializeObject(client[1]), System.Text.Encoding.UTF8, "application/json")); + var token = JwtTokenGeneratorBuilder.Build().Generate(client[0].Id); + + _httpClient.DefaultRequestHeaders.Authorization = + new AuthenticationHeaderValue("Bearer", token); + + var updateRequest = new ClientEntity + { + Id = client[0].Id, + Name = "Update", + Email = client[1].Email + }; + + var response = await _httpClient.PutAsync( + "/api/clients", + new StringContent(JsonConvert.SerializeObject(updateRequest), + Encoding.UTF8, + "application/json")); - response.StatusCode.ShouldBe(System.Net.HttpStatusCode.BadRequest); + response.StatusCode.ShouldBe(HttpStatusCode.BadRequest); } } diff --git a/WebApi.Test/CustomWebApplicationFactory.cs b/WebApi.Test/CustomWebApplicationFactory.cs index dacaa7e..f8c1096 100644 --- a/WebApi.Test/CustomWebApplicationFactory.cs +++ b/WebApi.Test/CustomWebApplicationFactory.cs @@ -7,6 +7,7 @@ using ProductClientHub.Domain.Repositories.Client; using ProductClientHub.Domain.Repositories.UnitOfWork; using ProductClientHub.Domain.Security.Tokens; +using ProductClientHub.Domain.Services.LoggedUser; using ClientEntity = ProductClientHub.Domain.Entities.Client; namespace WebApi.Test; @@ -45,6 +46,7 @@ protected override void ConfigureWebHost(IWebHostBuilder builder) services.RemoveAll(); services.RemoveAll(); services.RemoveAll(); + services.RemoveAll(); // Remove all hosted services to prevent background services from running during tests services.RemoveAll(typeof(IHostedService)); @@ -54,6 +56,7 @@ protected override void ConfigureWebHost(IWebHostBuilder builder) services.AddScoped(); services.AddScoped(); services.AddScoped(); + services.AddScoped(); }); } @@ -137,6 +140,22 @@ public Task Add(ClientEntity client) } } + private sealed class FakeLoggedUser : ILoggedUser + { + private readonly TestClientStore _clientStore; + + public FakeLoggedUser(TestClientStore clientStore) + { + _clientStore = clientStore; + } + + public Task User() + { + var user = _clientStore.Clients.First(); + return Task.FromResult(user); + } + } + private sealed class FakeUnitOfWork : IUnitOfWork { public Task Commit() => Task.CompletedTask; diff --git a/WebApi.Test/WebApi.Test.csproj b/WebApi.Test/WebApi.Test.csproj index b8dcfae..a356379 100644 --- a/WebApi.Test/WebApi.Test.csproj +++ b/WebApi.Test/WebApi.Test.csproj @@ -21,6 +21,7 @@ +