From 0c69b4fdb698a60f56c0ea9340c2a179abe91ce9 Mon Sep 17 00:00:00 2001 From: Evandro Costa Date: Mon, 30 Mar 2026 21:26:15 -0300 Subject: [PATCH 01/25] feat(application): add IAuthenticatedRequest interface --- .../Common/Interfaces/IAuthenticatedRequest.cs | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 src/Voltiq.Application/Common/Interfaces/IAuthenticatedRequest.cs diff --git a/src/Voltiq.Application/Common/Interfaces/IAuthenticatedRequest.cs b/src/Voltiq.Application/Common/Interfaces/IAuthenticatedRequest.cs new file mode 100644 index 0000000..a218b4d --- /dev/null +++ b/src/Voltiq.Application/Common/Interfaces/IAuthenticatedRequest.cs @@ -0,0 +1,8 @@ +using MediatR; + +namespace Voltiq.Application.Common.Interfaces; + +public interface IAuthenticatedRequest : IRequest +{ + Guid UserId { get; set; } +} From 57edd3f632c28b848f0584e27140b1ba20767f3d Mon Sep 17 00:00:00 2001 From: Evandro Costa Date: Mon, 30 Mar 2026 21:37:30 -0300 Subject: [PATCH 02/25] test(application): add AuthorizationBehavior tests (red) --- .../Behaviors/AuthorizationBehaviorTests.cs | 69 +++++++++++++++++++ 1 file changed, 69 insertions(+) create mode 100644 tests/Voltiq.Application.Tests/Common/Behaviors/AuthorizationBehaviorTests.cs diff --git a/tests/Voltiq.Application.Tests/Common/Behaviors/AuthorizationBehaviorTests.cs b/tests/Voltiq.Application.Tests/Common/Behaviors/AuthorizationBehaviorTests.cs new file mode 100644 index 0000000..7fc7bed --- /dev/null +++ b/tests/Voltiq.Application.Tests/Common/Behaviors/AuthorizationBehaviorTests.cs @@ -0,0 +1,69 @@ +using ErrorOr; +using MediatR; +using Moq; +using Shouldly; +using Voltiq.Application.Common.Behaviors; +using Voltiq.Application.Common.Interfaces; +using Voltiq.Exceptions.Resources; + +namespace Voltiq.Application.Tests.Common.Behaviors; + +public class AuthorizationBehaviorTests +{ + private readonly Mock _currentUserServiceMock = new(); + private readonly Guid _userId = Guid.NewGuid(); + + private AuthorizationBehavior> CreateBehavior() => + new(_currentUserServiceMock.Object); + + [Fact] + public async Task Handle_WhenUserIsAuthenticated_ShouldPopulateUserIdAndCallNext() + { + _currentUserServiceMock.Setup(s => s.UserId).Returns(_userId); + var request = new TestAuthRequest(); + var nextCalled = false; + + var behavior = CreateBehavior(); + var result = await behavior.Handle( + request, + ct => + { + nextCalled = true; + return Task.FromResult>("ok"); + }, + CancellationToken.None); + + result.IsError.ShouldBeFalse(); + result.Value.ShouldBe("ok"); + nextCalled.ShouldBeTrue(); + request.UserId.ShouldBe(_userId); + } + + [Fact] + public async Task Handle_WhenUserIdIsEmpty_ShouldReturnUnauthorizedWithoutCallingNext() + { + _currentUserServiceMock.Setup(s => s.UserId).Returns(Guid.Empty); + var request = new TestAuthRequest(); + var nextCalled = false; + + var behavior = CreateBehavior(); + var result = await behavior.Handle( + request, + ct => + { + nextCalled = true; + return Task.FromResult>("ok"); + }, + CancellationToken.None); + + result.IsError.ShouldBeTrue(); + result.FirstError.Type.ShouldBe(ErrorType.Unauthorized); + result.FirstError.Description.ShouldBe(ResourceErrorMessages.TITULO_NAO_AUTORIZADO); + nextCalled.ShouldBeFalse(); + } + + private sealed class TestAuthRequest : IAuthenticatedRequest> + { + public Guid UserId { get; set; } + } +} From 1a28e49ac2f474c8c9d0fd69c5efd03d882cf0df Mon Sep 17 00:00:00 2001 From: Evandro Costa Date: Mon, 30 Mar 2026 21:45:24 -0300 Subject: [PATCH 03/25] feat(application): add AuthorizationBehavior pipeline behavior --- .../Common/Behaviors/AuthorizationBehavior.cs | 47 +++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 src/Voltiq.Application/Common/Behaviors/AuthorizationBehavior.cs diff --git a/src/Voltiq.Application/Common/Behaviors/AuthorizationBehavior.cs b/src/Voltiq.Application/Common/Behaviors/AuthorizationBehavior.cs new file mode 100644 index 0000000..7342053 --- /dev/null +++ b/src/Voltiq.Application/Common/Behaviors/AuthorizationBehavior.cs @@ -0,0 +1,47 @@ +using System.Reflection; +using ErrorOr; +using MediatR; +using Voltiq.Application.Common.Interfaces; +using Voltiq.Exceptions.Resources; + +namespace Voltiq.Application.Common.Behaviors; + +public sealed class AuthorizationBehavior(ICurrentUserService currentUserService) + : IPipelineBehavior + where TRequest : IAuthenticatedRequest + where TResponse : IErrorOr +{ + public async Task Handle( + TRequest request, + RequestHandlerDelegate next, + CancellationToken cancellationToken) + { + var userId = currentUserService.UserId; + + if (userId == Guid.Empty) + { + var error = Error.Unauthorized(description: ResourceErrorMessages.TITULO_NAO_AUTORIZADO); + return CreateUnauthorized(error); + } + + request.UserId = userId; + return await next(cancellationToken); + } + + private static TResponse CreateUnauthorized(Error error) + { + var responseType = typeof(TResponse); + + if (responseType.IsGenericType && responseType.GetGenericTypeDefinition() == typeof(ErrorOr<>)) + { + var typeArg = responseType.GetGenericArguments()[0]; + var implicitOp = typeof(ErrorOr<>) + .MakeGenericType(typeArg) + .GetMethod("op_Implicit", BindingFlags.Static | BindingFlags.Public, [typeof(Error)])!; + + return (TResponse)implicitOp.Invoke(null, [error])!; + } + + throw new InvalidOperationException($"Unexpected TResponse type: {responseType}"); + } +} From e6f32249595c765f35da468f715fdf18073a2764 Mon Sep 17 00:00:00 2001 From: Evandro Costa Date: Mon, 30 Mar 2026 21:45:36 -0300 Subject: [PATCH 04/25] feat(application): register AuthorizationBehavior in MediatR pipeline --- src/Voltiq.Application/DependencyInjection.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Voltiq.Application/DependencyInjection.cs b/src/Voltiq.Application/DependencyInjection.cs index 8432af1..9b05df0 100644 --- a/src/Voltiq.Application/DependencyInjection.cs +++ b/src/Voltiq.Application/DependencyInjection.cs @@ -19,6 +19,7 @@ public static void AddApplication(this IServiceCollection services, throw new InvalidOperationException("MediatR license key is not configured."); cfg.RegisterServicesFromAssembly(Assembly.GetExecutingAssembly()); cfg.AddBehavior(typeof(IPipelineBehavior<,>), typeof(ValidationBehavior<,>)); + cfg.AddBehavior(typeof(IPipelineBehavior<,>), typeof(AuthorizationBehavior<,>)); }); services.AddValidatorsFromAssembly(Assembly.GetExecutingAssembly()); From 554c575f47e2a27dc710fd3b27466873870d770d Mon Sep 17 00:00:00 2001 From: Evandro Costa Date: Mon, 30 Mar 2026 21:50:02 -0300 Subject: [PATCH 05/25] refactor(clients): migrate GetClientsQuery to IAuthenticatedRequest --- .../Clients/Queries/GetClients/GetClientsQuery.cs | 7 +++++-- .../Queries/GetClients/GetClientsQueryHandler.cs | 14 ++------------ 2 files changed, 7 insertions(+), 14 deletions(-) diff --git a/src/Voltiq.Application/Features/Clients/Queries/GetClients/GetClientsQuery.cs b/src/Voltiq.Application/Features/Clients/Queries/GetClients/GetClientsQuery.cs index 45630d5..e347e23 100644 --- a/src/Voltiq.Application/Features/Clients/Queries/GetClients/GetClientsQuery.cs +++ b/src/Voltiq.Application/Features/Clients/Queries/GetClients/GetClientsQuery.cs @@ -1,6 +1,9 @@ using ErrorOr; -using MediatR; +using Voltiq.Application.Common.Interfaces; namespace Voltiq.Application.Features.Clients.Queries.GetClients; -public sealed record GetClientsQuery : IRequest>>; +public sealed record GetClientsQuery : IAuthenticatedRequest>> +{ + public Guid UserId { get; set; } +} diff --git a/src/Voltiq.Application/Features/Clients/Queries/GetClients/GetClientsQueryHandler.cs b/src/Voltiq.Application/Features/Clients/Queries/GetClients/GetClientsQueryHandler.cs index 242dc68..1475885 100644 --- a/src/Voltiq.Application/Features/Clients/Queries/GetClients/GetClientsQueryHandler.cs +++ b/src/Voltiq.Application/Features/Clients/Queries/GetClients/GetClientsQueryHandler.cs @@ -1,27 +1,17 @@ using ErrorOr; using MediatR; -using Voltiq.Application.Common.Interfaces; using Voltiq.Application.Mappings.Clients; using Voltiq.Domain.Interfaces.Repositories.Client; -using Voltiq.Exceptions.Resources; namespace Voltiq.Application.Features.Clients.Queries.GetClients; -public sealed class GetClientsQueryHandler( - IClientRepository clientRepository, - ICurrentUserService currentUserService) +public sealed class GetClientsQueryHandler(IClientRepository clientRepository) : IRequestHandler>> { public async Task>> Handle(GetClientsQuery request, CancellationToken cancellationToken) { - var userId = currentUserService.UserId; - - if (userId == Guid.Empty) - return Error.Unauthorized(description: ResourceErrorMessages.TITULO_NAO_AUTORIZADO); - - var clients = await clientRepository.GetByUserIdAsync(userId, cancellationToken); + var clients = await clientRepository.GetByUserIdAsync(request.UserId, cancellationToken); return clients.Select(c => c.ToResponse()).ToList(); } } - From affc81d28b741b68f0487e1b03c3165ed5a76865 Mon Sep 17 00:00:00 2001 From: Evandro Costa Date: Mon, 30 Mar 2026 21:50:44 -0300 Subject: [PATCH 06/25] refactor(clients): migrate GetClientByIdQuery to IAuthenticatedRequest --- .../Queries/GetClientById/GetClientByIdQuery.cs | 7 +++++-- .../GetClientById/GetClientByIdQueryHandler.cs | 13 ++----------- 2 files changed, 7 insertions(+), 13 deletions(-) diff --git a/src/Voltiq.Application/Features/Clients/Queries/GetClientById/GetClientByIdQuery.cs b/src/Voltiq.Application/Features/Clients/Queries/GetClientById/GetClientByIdQuery.cs index d384840..fbacd73 100644 --- a/src/Voltiq.Application/Features/Clients/Queries/GetClientById/GetClientByIdQuery.cs +++ b/src/Voltiq.Application/Features/Clients/Queries/GetClientById/GetClientByIdQuery.cs @@ -1,6 +1,9 @@ using ErrorOr; -using MediatR; +using Voltiq.Application.Common.Interfaces; namespace Voltiq.Application.Features.Clients.Queries.GetClientById; -public sealed record GetClientByIdQuery(Guid Id) : IRequest>; +public sealed record GetClientByIdQuery(Guid Id) : IAuthenticatedRequest> +{ + public Guid UserId { get; set; } +} diff --git a/src/Voltiq.Application/Features/Clients/Queries/GetClientById/GetClientByIdQueryHandler.cs b/src/Voltiq.Application/Features/Clients/Queries/GetClientById/GetClientByIdQueryHandler.cs index 3850bbd..b852e59 100644 --- a/src/Voltiq.Application/Features/Clients/Queries/GetClientById/GetClientByIdQueryHandler.cs +++ b/src/Voltiq.Application/Features/Clients/Queries/GetClientById/GetClientByIdQueryHandler.cs @@ -1,25 +1,17 @@ using ErrorOr; using MediatR; -using Voltiq.Application.Common.Interfaces; using Voltiq.Application.Mappings.Clients; using Voltiq.Domain.Interfaces.Repositories.Client; using Voltiq.Exceptions.Resources; namespace Voltiq.Application.Features.Clients.Queries.GetClientById; -public sealed class GetClientByIdQueryHandler( - IClientRepository clientRepository, - ICurrentUserService currentUserService) +public sealed class GetClientByIdQueryHandler(IClientRepository clientRepository) : IRequestHandler> { public async Task> Handle(GetClientByIdQuery request, CancellationToken cancellationToken) { - var userId = currentUserService.UserId; - - if (userId == Guid.Empty) - return Error.Unauthorized(description: ResourceErrorMessages.TITULO_NAO_AUTORIZADO); - - var client = await clientRepository.GetByIdAndUserIdAsync(request.Id, userId, cancellationToken); + var client = await clientRepository.GetByIdAndUserIdAsync(request.Id, request.UserId, cancellationToken); if (client is null) return Error.NotFound(description: ResourceErrorMessages.CLIENTE_NAO_ENCONTRADO); @@ -27,4 +19,3 @@ public async Task> Handle(GetClientByIdQuery request, Ca return client.ToResponse(); } } - From 8812db9f1c310b1e63f9fd54adfe5d349ac5e379 Mon Sep 17 00:00:00 2001 From: Evandro Costa Date: Mon, 30 Mar 2026 21:52:09 -0300 Subject: [PATCH 07/25] refactor(clients): migrate RegisterClientCommand to IAuthenticatedRequest --- .../RegisterClient/RegisterClientCommand.cs | 7 +++++-- .../RegisterClient/RegisterClientCommandHandler.cs | 13 +++---------- 2 files changed, 8 insertions(+), 12 deletions(-) diff --git a/src/Voltiq.Application/Features/Clients/Commands/RegisterClient/RegisterClientCommand.cs b/src/Voltiq.Application/Features/Clients/Commands/RegisterClient/RegisterClientCommand.cs index b7dbb3b..49219b6 100644 --- a/src/Voltiq.Application/Features/Clients/Commands/RegisterClient/RegisterClientCommand.cs +++ b/src/Voltiq.Application/Features/Clients/Commands/RegisterClient/RegisterClientCommand.cs @@ -1,5 +1,5 @@ using ErrorOr; -using MediatR; +using Voltiq.Application.Common.Interfaces; namespace Voltiq.Application.Features.Clients.Commands.RegisterClient; @@ -11,4 +11,7 @@ public sealed record RegisterClientCommand( string Number, string City, string State, - string ZipCode) : IRequest>; + string ZipCode) : IAuthenticatedRequest> +{ + public Guid UserId { get; set; } +} diff --git a/src/Voltiq.Application/Features/Clients/Commands/RegisterClient/RegisterClientCommandHandler.cs b/src/Voltiq.Application/Features/Clients/Commands/RegisterClient/RegisterClientCommandHandler.cs index d8a8d6d..291f646 100644 --- a/src/Voltiq.Application/Features/Clients/Commands/RegisterClient/RegisterClientCommandHandler.cs +++ b/src/Voltiq.Application/Features/Clients/Commands/RegisterClient/RegisterClientCommandHandler.cs @@ -1,6 +1,5 @@ using ErrorOr; using MediatR; -using Voltiq.Application.Common.Interfaces; using Voltiq.Application.Mappings.Clients; using Voltiq.Domain.Entities; using Voltiq.Domain.Interfaces; @@ -12,29 +11,23 @@ namespace Voltiq.Application.Features.Clients.Commands.RegisterClient; public sealed class RegisterClientCommandHandler( IClientRepository clientRepository, - IUnitOfWork unitOfWork, - ICurrentUserService currentUserService) + IUnitOfWork unitOfWork) : IRequestHandler> { public async Task> Handle(RegisterClientCommand request, CancellationToken cancellationToken) { - var userId = currentUserService.UserId; - - if (userId == Guid.Empty) - return Error.Unauthorized(description: ResourceErrorMessages.TITULO_NAO_AUTORIZADO); - var email = Email.Create(request.Email).Value; var emailExists = await clientRepository.ExistsWithEmailForUserAsync( - email, userId, cancellationToken: cancellationToken); + email, request.UserId, cancellationToken: cancellationToken); if (emailExists) return Error.Conflict(description: ResourceErrorMessages.CLIENTE_EMAIL_JA_CADASTRADO); var address = Address.Create(request.Street, request.Number, request.City, request.State, request.ZipCode); - var client = Client.Register(userId, request.Name, request.Phone, email, address); + var client = Client.Register(request.UserId, request.Name, request.Phone, email, address); await clientRepository.AddAsync(client, cancellationToken); await unitOfWork.SaveChangesAsync(cancellationToken); From 94bf656e66ef98102c13f7e887d8dcf5c1925c25 Mon Sep 17 00:00:00 2001 From: Evandro Costa Date: Mon, 30 Mar 2026 21:52:50 -0300 Subject: [PATCH 08/25] refactor(clients): migrate UpdateClientCommand to IAuthenticatedRequest --- .../Commands/UpdateClient/UpdateClientCommand.cs | 7 +++++-- .../UpdateClient/UpdateClientCommandHandler.cs | 13 +++---------- 2 files changed, 8 insertions(+), 12 deletions(-) diff --git a/src/Voltiq.Application/Features/Clients/Commands/UpdateClient/UpdateClientCommand.cs b/src/Voltiq.Application/Features/Clients/Commands/UpdateClient/UpdateClientCommand.cs index f3d75c8..8993f29 100644 --- a/src/Voltiq.Application/Features/Clients/Commands/UpdateClient/UpdateClientCommand.cs +++ b/src/Voltiq.Application/Features/Clients/Commands/UpdateClient/UpdateClientCommand.cs @@ -1,5 +1,5 @@ using ErrorOr; -using MediatR; +using Voltiq.Application.Common.Interfaces; namespace Voltiq.Application.Features.Clients.Commands.UpdateClient; @@ -12,4 +12,7 @@ public sealed record UpdateClientCommand( string Number, string City, string State, - string ZipCode) : IRequest>; + string ZipCode) : IAuthenticatedRequest> +{ + public Guid UserId { get; set; } +} diff --git a/src/Voltiq.Application/Features/Clients/Commands/UpdateClient/UpdateClientCommandHandler.cs b/src/Voltiq.Application/Features/Clients/Commands/UpdateClient/UpdateClientCommandHandler.cs index 790e451..733a12a 100644 --- a/src/Voltiq.Application/Features/Clients/Commands/UpdateClient/UpdateClientCommandHandler.cs +++ b/src/Voltiq.Application/Features/Clients/Commands/UpdateClient/UpdateClientCommandHandler.cs @@ -1,6 +1,5 @@ using ErrorOr; using MediatR; -using Voltiq.Application.Common.Interfaces; using Voltiq.Domain.Interfaces; using Voltiq.Domain.Interfaces.Repositories.Client; using Voltiq.Domain.ValueObjects; @@ -10,20 +9,14 @@ namespace Voltiq.Application.Features.Clients.Commands.UpdateClient; public sealed class UpdateClientCommandHandler( IClientRepository clientRepository, - IUnitOfWork unitOfWork, - ICurrentUserService currentUserService) + IUnitOfWork unitOfWork) : IRequestHandler> { public async Task> Handle(UpdateClientCommand request, CancellationToken cancellationToken) { - var userId = currentUserService.UserId; - - if (userId == Guid.Empty) - return Error.Unauthorized(description: ResourceErrorMessages.TITULO_NAO_AUTORIZADO); - var client = - await clientRepository.GetByIdAndUserIdAsync(request.Id, userId, cancellationToken); + await clientRepository.GetByIdAndUserIdAsync(request.Id, request.UserId, cancellationToken); if (client is null) return Error.NotFound(description: ResourceErrorMessages.CLIENTE_NAO_ENCONTRADO); @@ -31,7 +24,7 @@ public async Task> Handle(UpdateClientCommand request, var email = Email.Create(request.Email).Value; var emailExists = await clientRepository.ExistsWithEmailForUserAsync( - email, userId, request.Id, cancellationToken); + email, request.UserId, request.Id, cancellationToken); if (emailExists) return Error.Conflict(description: ResourceErrorMessages.CLIENTE_EMAIL_JA_CADASTRADO); From c9a044ac9282f8ec978746672af5df3409df8ec7 Mon Sep 17 00:00:00 2001 From: Evandro Costa Date: Mon, 30 Mar 2026 21:53:54 -0300 Subject: [PATCH 09/25] refactor(clients): migrate DeleteClientCommand to IAuthenticatedRequest --- .../Commands/DeleteClient/DeleteClientCommand.cs | 7 +++++-- .../DeleteClient/DeleteClientCommandHandler.cs | 11 ++--------- 2 files changed, 7 insertions(+), 11 deletions(-) diff --git a/src/Voltiq.Application/Features/Clients/Commands/DeleteClient/DeleteClientCommand.cs b/src/Voltiq.Application/Features/Clients/Commands/DeleteClient/DeleteClientCommand.cs index 1f43b36..f3b991c 100644 --- a/src/Voltiq.Application/Features/Clients/Commands/DeleteClient/DeleteClientCommand.cs +++ b/src/Voltiq.Application/Features/Clients/Commands/DeleteClient/DeleteClientCommand.cs @@ -1,6 +1,9 @@ using ErrorOr; -using MediatR; +using Voltiq.Application.Common.Interfaces; namespace Voltiq.Application.Features.Clients.Commands.DeleteClient; -public sealed record DeleteClientCommand(Guid Id) : IRequest>; +public sealed record DeleteClientCommand(Guid Id) : IAuthenticatedRequest> +{ + public Guid UserId { get; set; } +} diff --git a/src/Voltiq.Application/Features/Clients/Commands/DeleteClient/DeleteClientCommandHandler.cs b/src/Voltiq.Application/Features/Clients/Commands/DeleteClient/DeleteClientCommandHandler.cs index 523fda5..5032df1 100644 --- a/src/Voltiq.Application/Features/Clients/Commands/DeleteClient/DeleteClientCommandHandler.cs +++ b/src/Voltiq.Application/Features/Clients/Commands/DeleteClient/DeleteClientCommandHandler.cs @@ -1,6 +1,5 @@ using ErrorOr; using MediatR; -using Voltiq.Application.Common.Interfaces; using Voltiq.Domain.Interfaces; using Voltiq.Domain.Interfaces.Repositories.Client; using Voltiq.Exceptions.Resources; @@ -9,20 +8,14 @@ namespace Voltiq.Application.Features.Clients.Commands.DeleteClient; public sealed class DeleteClientCommandHandler( IClientRepository clientRepository, - IUnitOfWork unitOfWork, - ICurrentUserService currentUserService) + IUnitOfWork unitOfWork) : IRequestHandler> { public async Task> Handle(DeleteClientCommand request, CancellationToken cancellationToken) { - var userId = currentUserService.UserId; - - if (userId == Guid.Empty) - return Error.Unauthorized(description: ResourceErrorMessages.TITULO_NAO_AUTORIZADO); - var client = - await clientRepository.GetByIdAndUserIdAsync(request.Id, userId, cancellationToken); + await clientRepository.GetByIdAndUserIdAsync(request.Id, request.UserId, cancellationToken); if (client is null) return Error.NotFound(description: ResourceErrorMessages.CLIENTE_NAO_ENCONTRADO); From 13b80557b33cec5c74ddf7d1a73eafb2235093df Mon Sep 17 00:00:00 2001 From: Evandro Costa Date: Mon, 30 Mar 2026 21:54:30 -0300 Subject: [PATCH 10/25] refactor(users): migrate GetCurrentUserQuery to IAuthenticatedRequest --- .../Queries/GetCurrentUser/GetCurrentUserQuery.cs | 8 ++++++-- .../GetCurrentUser/GetCurrentUserQueryHandler.cs | 15 +++------------ 2 files changed, 9 insertions(+), 14 deletions(-) diff --git a/src/Voltiq.Application/Features/Users/Queries/GetCurrentUser/GetCurrentUserQuery.cs b/src/Voltiq.Application/Features/Users/Queries/GetCurrentUser/GetCurrentUserQuery.cs index 5a62bff..d02b366 100644 --- a/src/Voltiq.Application/Features/Users/Queries/GetCurrentUser/GetCurrentUserQuery.cs +++ b/src/Voltiq.Application/Features/Users/Queries/GetCurrentUser/GetCurrentUserQuery.cs @@ -1,6 +1,10 @@ using ErrorOr; -using MediatR; +using Voltiq.Application.Common.Interfaces; +using Voltiq.Application.Features.Users.Queries.GetCurrentUser; namespace Voltiq.Application.Features.Users.Queries.GetCurrentUser; -public sealed record GetCurrentUserQuery : IRequest>; +public sealed record GetCurrentUserQuery : IAuthenticatedRequest> +{ + public Guid UserId { get; set; } +} diff --git a/src/Voltiq.Application/Features/Users/Queries/GetCurrentUser/GetCurrentUserQueryHandler.cs b/src/Voltiq.Application/Features/Users/Queries/GetCurrentUser/GetCurrentUserQueryHandler.cs index 23f54dc..55a22b7 100644 --- a/src/Voltiq.Application/Features/Users/Queries/GetCurrentUser/GetCurrentUserQueryHandler.cs +++ b/src/Voltiq.Application/Features/Users/Queries/GetCurrentUser/GetCurrentUserQueryHandler.cs @@ -1,6 +1,5 @@ using ErrorOr; using MediatR; -using Voltiq.Application.Common.Interfaces; using Voltiq.Application.Mappings.Users; using Voltiq.Domain.Entities; using Voltiq.Domain.Interfaces.Repositories; @@ -8,24 +7,16 @@ namespace Voltiq.Application.Features.Users.Queries.GetCurrentUser; -public sealed class GetCurrentUserQueryHandler( - ICurrentUserService currentUserService, - IRepository userRepository) +public sealed class GetCurrentUserQueryHandler(IRepository userRepository) : IRequestHandler> { public async Task> Handle(GetCurrentUserQuery request, CancellationToken cancellationToken) { - var userId = currentUserService.UserId; - - if (userId == Guid.Empty) - return Error.NotFound(description: string.Format( - ResourceErrorMessages.ENTIDADE_NAO_ENCONTRADA, nameof(User), currentUserService.UserId)); - - var user = await userRepository.GetByIdAsync(userId, cancellationToken); + var user = await userRepository.GetByIdAsync(request.UserId, cancellationToken); if (user is null) return Error.NotFound(description: string.Format( - ResourceErrorMessages.ENTIDADE_NAO_ENCONTRADA, nameof(User), userId)); + ResourceErrorMessages.ENTIDADE_NAO_ENCONTRADA, nameof(User), request.UserId)); return user.ToGetUserResponse(); } From 715b0ba3aaa200357c7c301c51c3bf991e3e85ef Mon Sep 17 00:00:00 2001 From: Evandro Costa Date: Mon, 30 Mar 2026 21:55:37 -0300 Subject: [PATCH 11/25] test(clients): adapt GetClientsQueryHandlerTests to IAuthenticatedRequest --- .../Queries/GetClientsQueryHandlerTests.cs | 23 +++---------------- 1 file changed, 3 insertions(+), 20 deletions(-) diff --git a/tests/Voltiq.Application.Tests/Features/Clients/Queries/GetClientsQueryHandlerTests.cs b/tests/Voltiq.Application.Tests/Features/Clients/Queries/GetClientsQueryHandlerTests.cs index 691b711..736339f 100644 --- a/tests/Voltiq.Application.Tests/Features/Clients/Queries/GetClientsQueryHandlerTests.cs +++ b/tests/Voltiq.Application.Tests/Features/Clients/Queries/GetClientsQueryHandlerTests.cs @@ -1,8 +1,6 @@ using ErrorOr; using Moq; using Shouldly; -using Voltiq.Application.Common.Interfaces; -using Voltiq.Application.Features.Clients; using Voltiq.Application.Features.Clients.Queries.GetClients; using Voltiq.Domain.Entities; using Voltiq.Domain.Interfaces.Repositories.Client; @@ -13,12 +11,11 @@ namespace Voltiq.Application.Tests.Features.Clients.Queries; public class GetClientsQueryHandlerTests { private readonly Mock _clientRepoMock = new(); - private readonly Mock _currentUserServiceMock = new(); private readonly Guid _userId = Guid.NewGuid(); private GetClientsQueryHandler CreateHandler() => - new(_clientRepoMock.Object, _currentUserServiceMock.Object); + new(_clientRepoMock.Object); private static Client MakeClient(Guid userId, string name = "João Silva") => Client.Register(userId, name, "(11) 99999-9999", Email.Create("joao@example.com").Value, @@ -32,13 +29,12 @@ public async Task Handle_ShouldReturnClientsForCurrentUser() MakeClient(_userId, "João Silva"), MakeClient(_userId, "Maria Santos"), }; - _currentUserServiceMock.Setup(s => s.UserId).Returns(_userId); _clientRepoMock .Setup(r => r.GetByUserIdAsync(_userId, It.IsAny())) .ReturnsAsync(clients); var handler = CreateHandler(); - var result = await handler.Handle(new GetClientsQuery(), CancellationToken.None); + var result = await handler.Handle(new GetClientsQuery { UserId = _userId }, CancellationToken.None); result.IsError.ShouldBeFalse(); result.Value.Count.ShouldBe(2); @@ -49,27 +45,14 @@ public async Task Handle_ShouldReturnClientsForCurrentUser() [Fact] public async Task Handle_WhenNoClients_ShouldReturnEmptyList() { - _currentUserServiceMock.Setup(s => s.UserId).Returns(_userId); _clientRepoMock .Setup(r => r.GetByUserIdAsync(_userId, It.IsAny())) .ReturnsAsync([]); var handler = CreateHandler(); - var result = await handler.Handle(new GetClientsQuery(), CancellationToken.None); + var result = await handler.Handle(new GetClientsQuery { UserId = _userId }, CancellationToken.None); result.IsError.ShouldBeFalse(); result.Value.ShouldBeEmpty(); } - - [Fact] - public async Task Handle_WhenUserIdIsInvalid_ShouldReturnUnauthorizedError() - { - _currentUserServiceMock.Setup(s => s.UserId).Returns(Guid.Empty); - - var handler = CreateHandler(); - var result = await handler.Handle(new GetClientsQuery(), CancellationToken.None); - - result.IsError.ShouldBeTrue(); - result.FirstError.Type.ShouldBe(ErrorType.Unauthorized); - } } From 484ec14d0b36523ea21c6a59c3bb93843631b7e8 Mon Sep 17 00:00:00 2001 From: Evandro Costa Date: Mon, 30 Mar 2026 21:58:06 -0300 Subject: [PATCH 12/25] test(clients): adapt GetClientByIdQueryHandlerTests to IAuthenticatedRequest --- .../Queries/GetClientByIdQueryHandlerTests.cs | 23 +++---------------- 1 file changed, 3 insertions(+), 20 deletions(-) diff --git a/tests/Voltiq.Application.Tests/Features/Clients/Queries/GetClientByIdQueryHandlerTests.cs b/tests/Voltiq.Application.Tests/Features/Clients/Queries/GetClientByIdQueryHandlerTests.cs index ec877fb..ad8c3fe 100644 --- a/tests/Voltiq.Application.Tests/Features/Clients/Queries/GetClientByIdQueryHandlerTests.cs +++ b/tests/Voltiq.Application.Tests/Features/Clients/Queries/GetClientByIdQueryHandlerTests.cs @@ -1,8 +1,6 @@ using ErrorOr; using Moq; using Shouldly; -using Voltiq.Application.Common.Interfaces; -using Voltiq.Application.Features.Clients; using Voltiq.Application.Features.Clients.Queries.GetClientById; using Voltiq.Domain.Entities; using Voltiq.Domain.Interfaces.Repositories.Client; @@ -14,12 +12,11 @@ namespace Voltiq.Application.Tests.Features.Clients.Queries; public class GetClientByIdQueryHandlerTests { private readonly Mock _clientRepoMock = new(); - private readonly Mock _currentUserServiceMock = new(); private readonly Guid _userId = Guid.NewGuid(); private GetClientByIdQueryHandler CreateHandler() => - new(_clientRepoMock.Object, _currentUserServiceMock.Object); + new(_clientRepoMock.Object); private static Client MakeClient(Guid userId) { @@ -32,13 +29,12 @@ private static Client MakeClient(Guid userId) public async Task Handle_WhenClientExists_ShouldReturnClientResponse() { var client = MakeClient(_userId); - _currentUserServiceMock.Setup(s => s.UserId).Returns(_userId); _clientRepoMock .Setup(r => r.GetByIdAndUserIdAsync(client.Id, _userId, It.IsAny())) .ReturnsAsync(client); var handler = CreateHandler(); - var result = await handler.Handle(new GetClientByIdQuery(client.Id), CancellationToken.None); + var result = await handler.Handle(new GetClientByIdQuery(client.Id) { UserId = _userId }, CancellationToken.None); result.IsError.ShouldBeFalse(); result.Value.Id.ShouldBe(client.Id); @@ -51,28 +47,15 @@ public async Task Handle_WhenClientExists_ShouldReturnClientResponse() [Fact] public async Task Handle_WhenClientNotFound_ShouldReturnNotFoundError() { - _currentUserServiceMock.Setup(s => s.UserId).Returns(_userId); _clientRepoMock .Setup(r => r.GetByIdAndUserIdAsync(It.IsAny(), _userId, It.IsAny())) .ReturnsAsync((Client?)null); var handler = CreateHandler(); - var result = await handler.Handle(new GetClientByIdQuery(Guid.NewGuid()), CancellationToken.None); + var result = await handler.Handle(new GetClientByIdQuery(Guid.NewGuid()) { UserId = _userId }, CancellationToken.None); result.IsError.ShouldBeTrue(); result.FirstError.Type.ShouldBe(ErrorType.NotFound); result.FirstError.Description.ShouldBe(ResourceErrorMessages.CLIENTE_NAO_ENCONTRADO); } - - [Fact] - public async Task Handle_WhenUserIdIsInvalid_ShouldReturnUnauthorizedError() - { - _currentUserServiceMock.Setup(s => s.UserId).Returns(Guid.Empty); - - var handler = CreateHandler(); - var result = await handler.Handle(new GetClientByIdQuery(Guid.NewGuid()), CancellationToken.None); - - result.IsError.ShouldBeTrue(); - result.FirstError.Type.ShouldBe(ErrorType.Unauthorized); - } } From 3d40b4db6da47d35a323341f6257fda5a9b78ce6 Mon Sep 17 00:00:00 2001 From: Evandro Costa Date: Mon, 30 Mar 2026 21:58:48 -0300 Subject: [PATCH 13/25] test(clients): adapt RegisterClientCommandHandlerTests to IAuthenticatedRequest --- .../RegisterClientCommandHandlerTests.cs | 29 +++---------------- 1 file changed, 4 insertions(+), 25 deletions(-) diff --git a/tests/Voltiq.Application.Tests/Features/Clients/Commands/RegisterClientCommandHandlerTests.cs b/tests/Voltiq.Application.Tests/Features/Clients/Commands/RegisterClientCommandHandlerTests.cs index 9d38a28..b110aa0 100644 --- a/tests/Voltiq.Application.Tests/Features/Clients/Commands/RegisterClientCommandHandlerTests.cs +++ b/tests/Voltiq.Application.Tests/Features/Clients/Commands/RegisterClientCommandHandlerTests.cs @@ -1,7 +1,6 @@ using ErrorOr; using Moq; using Shouldly; -using Voltiq.Application.Common.Interfaces; using Voltiq.Application.Features.Clients.Commands.RegisterClient; using Voltiq.Domain.Entities; using Voltiq.Domain.Interfaces; @@ -14,28 +13,23 @@ namespace Voltiq.Application.Tests.Features.Clients.Commands; public class RegisterClientCommandHandlerTests { private readonly Mock _clientRepoMock = new(); - private readonly Mock _currentUserServiceMock = new(); private readonly Mock _unitOfWorkMock = new(); private readonly Guid _userId = Guid.NewGuid(); private RegisterClientCommandHandler CreateHandler() { - return new RegisterClientCommandHandler(_clientRepoMock.Object, _unitOfWorkMock.Object, - _currentUserServiceMock.Object); + return new RegisterClientCommandHandler(_clientRepoMock.Object, _unitOfWorkMock.Object); } - private static RegisterClientCommand ValidCommand() - { - return new RegisterClientCommand("João Silva", "(11) 99999-9999", "joao@example.com", + private RegisterClientCommand ValidCommand() => + new("João Silva", "(11) 99999-9999", "joao@example.com", "Rua das Flores", "123", - "São Paulo", "SP", "01310-100"); - } + "São Paulo", "SP", "01310-100") { UserId = _userId }; [Fact] public async Task Handle_WithValidCommand_ShouldRegisterClientAndReturnResponse() { - _currentUserServiceMock.Setup(s => s.UserId).Returns(_userId); _clientRepoMock .Setup(r => r.ExistsWithEmailForUserAsync(It.IsAny(), _userId, null, It.IsAny())) @@ -59,7 +53,6 @@ public async Task Handle_WithValidCommand_ShouldRegisterClientAndReturnResponse( [Fact] public async Task Handle_WhenEmailAlreadyExistsForUser_ShouldReturnConflictError() { - _currentUserServiceMock.Setup(s => s.UserId).Returns(_userId); _clientRepoMock .Setup(r => r.ExistsWithEmailForUserAsync(It.IsAny(), _userId, null, It.IsAny())) @@ -74,18 +67,4 @@ public async Task Handle_WhenEmailAlreadyExistsForUser_ShouldReturnConflictError _clientRepoMock.Verify(r => r.AddAsync(It.IsAny(), It.IsAny()), Times.Never); } - - [Fact] - public async Task Handle_WhenUserIdIsInvalid_ShouldReturnUnauthorizedError() - { - _currentUserServiceMock.Setup(s => s.UserId).Returns(Guid.Empty); - - var handler = CreateHandler(); - var result = await handler.Handle(ValidCommand(), CancellationToken.None); - - result.IsError.ShouldBeTrue(); - result.FirstError.Type.ShouldBe(ErrorType.Unauthorized); - _clientRepoMock.Verify(r => r.AddAsync(It.IsAny(), It.IsAny()), - Times.Never); - } } From e9651cd1c4d16d4464c61b6e48f3a7eb59fc4fc3 Mon Sep 17 00:00:00 2001 From: Evandro Costa Date: Mon, 30 Mar 2026 21:59:31 -0300 Subject: [PATCH 14/25] test(clients): adapt UpdateClientCommandHandlerTests to IAuthenticatedRequest --- .../UpdateClientCommandHandlerTests.cs | 30 +++---------------- 1 file changed, 4 insertions(+), 26 deletions(-) diff --git a/tests/Voltiq.Application.Tests/Features/Clients/Commands/UpdateClientCommandHandlerTests.cs b/tests/Voltiq.Application.Tests/Features/Clients/Commands/UpdateClientCommandHandlerTests.cs index db6533e..fa8ecc2 100644 --- a/tests/Voltiq.Application.Tests/Features/Clients/Commands/UpdateClientCommandHandlerTests.cs +++ b/tests/Voltiq.Application.Tests/Features/Clients/Commands/UpdateClientCommandHandlerTests.cs @@ -1,7 +1,6 @@ using ErrorOr; using Moq; using Shouldly; -using Voltiq.Application.Common.Interfaces; using Voltiq.Application.Features.Clients.Commands.UpdateClient; using Voltiq.Domain.Entities; using Voltiq.Domain.Interfaces; @@ -14,15 +13,13 @@ namespace Voltiq.Application.Tests.Features.Clients.Commands; public class UpdateClientCommandHandlerTests { private readonly Mock _clientRepoMock = new(); - private readonly Mock _currentUserServiceMock = new(); private readonly Mock _unitOfWorkMock = new(); private readonly Guid _userId = Guid.NewGuid(); private UpdateClientCommandHandler CreateHandler() { - return new UpdateClientCommandHandler(_clientRepoMock.Object, _unitOfWorkMock.Object, - _currentUserServiceMock.Object); + return new UpdateClientCommandHandler(_clientRepoMock.Object, _unitOfWorkMock.Object); } private static Client MakeClient(Guid userId) @@ -36,7 +33,6 @@ private static Client MakeClient(Guid userId) public async Task Handle_WithValidCommand_ShouldUpdateClientAndReturnUpdated() { var client = MakeClient(_userId); - _currentUserServiceMock.Setup(s => s.UserId).Returns(_userId); _clientRepoMock .Setup(r => r.GetByIdAndUserIdAsync(client.Id, _userId, It.IsAny())) .ReturnsAsync(client); @@ -47,7 +43,7 @@ public async Task Handle_WithValidCommand_ShouldUpdateClientAndReturnUpdated() var command = new UpdateClientCommand( client.Id, "Maria Souza", "(11) 88888-8888", "maria@example.com", - "Av. Paulista", "1000", "São Paulo", "SP", "01311-100"); + "Av. Paulista", "1000", "São Paulo", "SP", "01311-100") { UserId = _userId }; var handler = CreateHandler(); var result = await handler.Handle(command, CancellationToken.None); @@ -60,7 +56,6 @@ public async Task Handle_WithValidCommand_ShouldUpdateClientAndReturnUpdated() public async Task Handle_WhenEmailAlreadyExistsForAnotherClient_ShouldReturnConflictError() { var client = MakeClient(_userId); - _currentUserServiceMock.Setup(s => s.UserId).Returns(_userId); _clientRepoMock .Setup(r => r.GetByIdAndUserIdAsync(client.Id, _userId, It.IsAny())) .ReturnsAsync(client); @@ -71,7 +66,7 @@ public async Task Handle_WhenEmailAlreadyExistsForAnotherClient_ShouldReturnConf var command = new UpdateClientCommand( client.Id, "Maria Souza", "(11) 88888-8888", "outro@example.com", - "Av. Paulista", "1000", "São Paulo", "SP", "01311-100"); + "Av. Paulista", "1000", "São Paulo", "SP", "01311-100") { UserId = _userId }; var handler = CreateHandler(); var result = await handler.Handle(command, CancellationToken.None); @@ -85,7 +80,6 @@ public async Task Handle_WhenEmailAlreadyExistsForAnotherClient_ShouldReturnConf [Fact] public async Task Handle_WhenClientNotFound_ShouldReturnNotFoundError() { - _currentUserServiceMock.Setup(s => s.UserId).Returns(_userId); _clientRepoMock .Setup(r => r.GetByIdAndUserIdAsync(It.IsAny(), _userId, It.IsAny())) @@ -93,7 +87,7 @@ public async Task Handle_WhenClientNotFound_ShouldReturnNotFoundError() var command = new UpdateClientCommand( Guid.NewGuid(), "Maria Souza", "(11) 88888-8888", "maria@example.com", - "Av. Paulista", "1000", "São Paulo", "SP", "01311-100"); + "Av. Paulista", "1000", "São Paulo", "SP", "01311-100") { UserId = _userId }; var handler = CreateHandler(); var result = await handler.Handle(command, CancellationToken.None); @@ -103,20 +97,4 @@ public async Task Handle_WhenClientNotFound_ShouldReturnNotFoundError() result.FirstError.Description.ShouldBe(ResourceErrorMessages.CLIENTE_NAO_ENCONTRADO); _unitOfWorkMock.Verify(u => u.SaveChangesAsync(It.IsAny()), Times.Never); } - - [Fact] - public async Task Handle_WhenUserIdIsInvalid_ShouldReturnUnauthorizedError() - { - _currentUserServiceMock.Setup(s => s.UserId).Returns(Guid.Empty); - - var command = new UpdateClientCommand( - Guid.NewGuid(), "Maria Souza", "(11) 88888-8888", "maria@example.com", - "Av. Paulista", "1000", "São Paulo", "SP", "01311-100"); - - var handler = CreateHandler(); - var result = await handler.Handle(command, CancellationToken.None); - - result.IsError.ShouldBeTrue(); - result.FirstError.Type.ShouldBe(ErrorType.Unauthorized); - } } From 1c6e0e41abc4a1d4922ebeac7d86f82c003ee5ae Mon Sep 17 00:00:00 2001 From: Evandro Costa Date: Mon, 30 Mar 2026 22:00:12 -0300 Subject: [PATCH 15/25] test(clients): adapt DeleteClientCommandHandlerTests to IAuthenticatedRequest --- .../DeleteClientCommandHandlerTests.cs | 22 +++---------------- 1 file changed, 3 insertions(+), 19 deletions(-) diff --git a/tests/Voltiq.Application.Tests/Features/Clients/Commands/DeleteClientCommandHandlerTests.cs b/tests/Voltiq.Application.Tests/Features/Clients/Commands/DeleteClientCommandHandlerTests.cs index 71e65b7..1c9660a 100644 --- a/tests/Voltiq.Application.Tests/Features/Clients/Commands/DeleteClientCommandHandlerTests.cs +++ b/tests/Voltiq.Application.Tests/Features/Clients/Commands/DeleteClientCommandHandlerTests.cs @@ -1,7 +1,6 @@ using ErrorOr; using Moq; using Shouldly; -using Voltiq.Application.Common.Interfaces; using Voltiq.Application.Features.Clients.Commands.DeleteClient; using Voltiq.Domain.Entities; using Voltiq.Domain.Interfaces; @@ -15,12 +14,11 @@ public class DeleteClientCommandHandlerTests { private readonly Mock _clientRepoMock = new(); private readonly Mock _unitOfWorkMock = new(); - private readonly Mock _currentUserServiceMock = new(); private readonly Guid _userId = Guid.NewGuid(); private DeleteClientCommandHandler CreateHandler() => - new(_clientRepoMock.Object, _unitOfWorkMock.Object, _currentUserServiceMock.Object); + new(_clientRepoMock.Object, _unitOfWorkMock.Object); private static Client MakeClient(Guid userId) { @@ -33,13 +31,12 @@ private static Client MakeClient(Guid userId) public async Task Handle_WhenClientExists_ShouldDeleteAndReturnDeleted() { var client = MakeClient(_userId); - _currentUserServiceMock.Setup(s => s.UserId).Returns(_userId); _clientRepoMock .Setup(r => r.GetByIdAndUserIdAsync(client.Id, _userId, It.IsAny())) .ReturnsAsync(client); var handler = CreateHandler(); - var result = await handler.Handle(new DeleteClientCommand(client.Id), CancellationToken.None); + var result = await handler.Handle(new DeleteClientCommand(client.Id) { UserId = _userId }, CancellationToken.None); result.IsError.ShouldBeFalse(); _clientRepoMock.Verify(r => r.Remove(client), Times.Once); @@ -49,29 +46,16 @@ public async Task Handle_WhenClientExists_ShouldDeleteAndReturnDeleted() [Fact] public async Task Handle_WhenClientNotFound_ShouldReturnNotFoundError() { - _currentUserServiceMock.Setup(s => s.UserId).Returns(_userId); _clientRepoMock .Setup(r => r.GetByIdAndUserIdAsync(It.IsAny(), _userId, It.IsAny())) .ReturnsAsync((Client?)null); var handler = CreateHandler(); - var result = await handler.Handle(new DeleteClientCommand(Guid.NewGuid()), CancellationToken.None); + var result = await handler.Handle(new DeleteClientCommand(Guid.NewGuid()) { UserId = _userId }, CancellationToken.None); result.IsError.ShouldBeTrue(); result.FirstError.Type.ShouldBe(ErrorType.NotFound); result.FirstError.Description.ShouldBe(ResourceErrorMessages.CLIENTE_NAO_ENCONTRADO); _unitOfWorkMock.Verify(u => u.SaveChangesAsync(It.IsAny()), Times.Never); } - - [Fact] - public async Task Handle_WhenUserIdIsInvalid_ShouldReturnUnauthorizedError() - { - _currentUserServiceMock.Setup(s => s.UserId).Returns(Guid.Empty); - - var handler = CreateHandler(); - var result = await handler.Handle(new DeleteClientCommand(Guid.NewGuid()), CancellationToken.None); - - result.IsError.ShouldBeTrue(); - result.FirstError.Type.ShouldBe(ErrorType.Unauthorized); - } } From 2c5c8d04573a39a045fbca5a6c4e50ba2a640738 Mon Sep 17 00:00:00 2001 From: Evandro Costa Date: Mon, 30 Mar 2026 22:00:42 -0300 Subject: [PATCH 16/25] test(users): adapt GetCurrentUserQueryHandlerTests to IAuthenticatedRequest --- .../Users/GetCurrentUserQueryHandlerTests.cs | 25 +++---------------- 1 file changed, 3 insertions(+), 22 deletions(-) diff --git a/tests/Voltiq.Application.Tests/Features/Users/GetCurrentUserQueryHandlerTests.cs b/tests/Voltiq.Application.Tests/Features/Users/GetCurrentUserQueryHandlerTests.cs index b832efc..625338f 100644 --- a/tests/Voltiq.Application.Tests/Features/Users/GetCurrentUserQueryHandlerTests.cs +++ b/tests/Voltiq.Application.Tests/Features/Users/GetCurrentUserQueryHandlerTests.cs @@ -1,7 +1,6 @@ using ErrorOr; using Moq; using Shouldly; -using Voltiq.Application.Common.Interfaces; using Voltiq.Application.Features.Users.Queries.GetCurrentUser; using Voltiq.Domain.Entities; using Voltiq.Domain.Interfaces.Repositories; @@ -12,11 +11,10 @@ namespace Voltiq.Application.Tests.Features.Users; public class GetCurrentUserQueryHandlerTests { - private readonly Mock _currentUserServiceMock = new(); private readonly Mock> _userRepoMock = new(); private GetCurrentUserQueryHandler CreateHandler() => - new(_currentUserServiceMock.Object, _userRepoMock.Object); + new(_userRepoMock.Object); private static User MakeUser() { @@ -30,45 +28,28 @@ public async Task Handle_WithValidToken_ShouldReturnCurrentUser() { var user = MakeUser(); - _currentUserServiceMock.Setup(s => s.UserId).Returns(user.Id); _userRepoMock .Setup(r => r.GetByIdAsync(user.Id, It.IsAny())) .ReturnsAsync(user); var handler = CreateHandler(); - var result = await handler.Handle(new GetCurrentUserQuery(), CancellationToken.None); + var result = await handler.Handle(new GetCurrentUserQuery { UserId = user.Id }, CancellationToken.None); result.IsError.ShouldBeFalse(); result.Value.Name.ShouldBe("João Silva"); result.Value.Email.ShouldBe("joao@example.com"); } - [Fact] - public async Task Handle_WhenUserIdIsEmpty_ShouldReturnNotFoundError() - { - _currentUserServiceMock.Setup(s => s.UserId).Returns(Guid.Empty); - - var handler = CreateHandler(); - var result = await handler.Handle(new GetCurrentUserQuery(), CancellationToken.None); - - result.IsError.ShouldBeTrue(); - result.FirstError.Type.ShouldBe(ErrorType.NotFound); - result.FirstError.Description.ShouldBe( - string.Format(ResourceErrorMessages.ENTIDADE_NAO_ENCONTRADA, nameof(User), Guid.Empty)); - _userRepoMock.Verify(r => r.GetByIdAsync(It.IsAny(), It.IsAny()), Times.Never); - } - [Fact] public async Task Handle_WhenUserNotFoundInDb_ShouldReturnNotFoundError() { var id = Guid.NewGuid(); - _currentUserServiceMock.Setup(s => s.UserId).Returns(id); _userRepoMock .Setup(r => r.GetByIdAsync(id, It.IsAny())) .ReturnsAsync((User?)null); var handler = CreateHandler(); - var result = await handler.Handle(new GetCurrentUserQuery(), CancellationToken.None); + var result = await handler.Handle(new GetCurrentUserQuery { UserId = id }, CancellationToken.None); result.IsError.ShouldBeTrue(); result.FirstError.Type.ShouldBe(ErrorType.NotFound); From c199a42deb537cbf675999651d3f90aef2f871e8 Mon Sep 17 00:00:00 2001 From: Evandro Costa Date: Mon, 30 Mar 2026 22:29:51 -0300 Subject: [PATCH 17/25] refactor(validation): streamline error handling and remove unused methods --- .../Common/Behaviors/AuthorizationBehavior.cs | 19 +---------- .../Common/Behaviors/ValidationBehavior.cs | 32 ++++--------------- 2 files changed, 7 insertions(+), 44 deletions(-) diff --git a/src/Voltiq.Application/Common/Behaviors/AuthorizationBehavior.cs b/src/Voltiq.Application/Common/Behaviors/AuthorizationBehavior.cs index 7342053..3678947 100644 --- a/src/Voltiq.Application/Common/Behaviors/AuthorizationBehavior.cs +++ b/src/Voltiq.Application/Common/Behaviors/AuthorizationBehavior.cs @@ -21,27 +21,10 @@ public async Task Handle( if (userId == Guid.Empty) { var error = Error.Unauthorized(description: ResourceErrorMessages.TITULO_NAO_AUTORIZADO); - return CreateUnauthorized(error); + return (dynamic)error; } request.UserId = userId; return await next(cancellationToken); } - - private static TResponse CreateUnauthorized(Error error) - { - var responseType = typeof(TResponse); - - if (responseType.IsGenericType && responseType.GetGenericTypeDefinition() == typeof(ErrorOr<>)) - { - var typeArg = responseType.GetGenericArguments()[0]; - var implicitOp = typeof(ErrorOr<>) - .MakeGenericType(typeArg) - .GetMethod("op_Implicit", BindingFlags.Static | BindingFlags.Public, [typeof(Error)])!; - - return (TResponse)implicitOp.Invoke(null, [error])!; - } - - throw new InvalidOperationException($"Unexpected TResponse type: {responseType}"); - } } diff --git a/src/Voltiq.Application/Common/Behaviors/ValidationBehavior.cs b/src/Voltiq.Application/Common/Behaviors/ValidationBehavior.cs index 0aba4a0..a687a10 100644 --- a/src/Voltiq.Application/Common/Behaviors/ValidationBehavior.cs +++ b/src/Voltiq.Application/Common/Behaviors/ValidationBehavior.cs @@ -1,11 +1,11 @@ -using System.Reflection; using ErrorOr; using FluentValidation; using MediatR; namespace Voltiq.Application.Common.Behaviors; -public sealed class ValidationBehavior(IEnumerable> validators) +public sealed class ValidationBehavior( + IEnumerable> validators) : IPipelineBehavior where TRequest : IRequest where TResponse : IErrorOr @@ -20,36 +20,16 @@ public async Task Handle( var context = new ValidationContext(request); - var failures = validators + var errors = validators .Select(v => v.Validate(context)) .SelectMany(r => r.Errors) .Where(f => f is not null) + .Select(f => Error.Validation(f.PropertyName, f.ErrorMessage)) .ToList(); - if (failures.Count == 0) + if (errors.Count == 0) return await next(cancellationToken); - var errors = failures - .Select(f => Error.Validation(code: f.PropertyName, description: f.ErrorMessage)) - .ToList(); - - return CreateFailure(errors); - } - - private static TResponse CreateFailure(List errors) - { - var responseType = typeof(TResponse); - - if (responseType.IsGenericType && responseType.GetGenericTypeDefinition() == typeof(ErrorOr<>)) - { - var typeArg = responseType.GetGenericArguments()[0]; - var implicitOp = typeof(ErrorOr<>) - .MakeGenericType(typeArg) - .GetMethod("op_Implicit", BindingFlags.Static | BindingFlags.Public, [typeof(List)])!; - - return (TResponse)implicitOp.Invoke(null, [errors])!; - } - - throw new InvalidOperationException($"Unexpected TResponse type: {responseType}"); + return (dynamic)errors; } } From dd1d5cef5cdf8be9013ef38f27954d7b2ce92fb2 Mon Sep 17 00:00:00 2001 From: Evandro Costa Date: Mon, 30 Mar 2026 22:30:03 -0300 Subject: [PATCH 18/25] fix(localization): correct invalid email/password error message --- .../Resources/ResourceErrorMessages.Designer.cs | 2 +- src/Voltiq.Exceptions/Resources/ResourceErrorMessages.resx | 7 ++++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/Voltiq.Exceptions/Resources/ResourceErrorMessages.Designer.cs b/src/Voltiq.Exceptions/Resources/ResourceErrorMessages.Designer.cs index c0b0863..cfdd401 100644 --- a/src/Voltiq.Exceptions/Resources/ResourceErrorMessages.Designer.cs +++ b/src/Voltiq.Exceptions/Resources/ResourceErrorMessages.Designer.cs @@ -231,7 +231,7 @@ public static string HASH_SENHA_OBRIGATORIO { } /// - /// Looks up a localized string similar to E-mail ou senha inválidos.. + /// Looks up a localized string similar to E-mail e/ou senha inválidos.. /// public static string LOGIN_CREDENCIAIS_INVALIDAS { get { diff --git a/src/Voltiq.Exceptions/Resources/ResourceErrorMessages.resx b/src/Voltiq.Exceptions/Resources/ResourceErrorMessages.resx index f126a44..3276843 100644 --- a/src/Voltiq.Exceptions/Resources/ResourceErrorMessages.resx +++ b/src/Voltiq.Exceptions/Resources/ResourceErrorMessages.resx @@ -1,6 +1,7 @@ - @@ -205,8 +206,8 @@ - E-mail ou senha inválidos. - + E-mail e/ou senha inválidos. + Refresh token inválido. From 68af860509a7f02c2488cfa9c8b27158f6ece67c Mon Sep 17 00:00:00 2001 From: Evandro Costa Date: Mon, 30 Mar 2026 22:30:12 -0300 Subject: [PATCH 19/25] refactor(Program): configure JSON options for camelCase property naming --- src/Voltiq.API/Program.cs | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/src/Voltiq.API/Program.cs b/src/Voltiq.API/Program.cs index 720e329..cef0d74 100644 --- a/src/Voltiq.API/Program.cs +++ b/src/Voltiq.API/Program.cs @@ -1,3 +1,4 @@ +using System.Text.Json; using Asp.Versioning; using Microsoft.OpenApi; using Serilog; @@ -17,7 +18,13 @@ builder.Services.AddProblemDetails(); builder.Services.AddExceptionHandler(); -builder.Services.AddControllers(); +builder.Services.AddControllers() + .AddJsonOptions(options => + { + options.JsonSerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.CamelCase; + options.JsonSerializerOptions.DictionaryKeyPolicy = JsonNamingPolicy.CamelCase; + }); + builder.Services.AddRouting(options => options.LowercaseUrls = true); builder.Services.AddApiVersioning(options => @@ -49,11 +56,11 @@ """ Voltiq é uma aplicação para gerir orçamentos, clientes e materiais para profissionais autônomos, principalmente eletricistas, oferecendo uma maneira - prática e eficiente de gerir seus clientes e orçamentos. + prática e eficiente de gerir seus serviços. """, Contact = new OpenApiContact { - Name = "Team Voltiq", + Name = "Time Voltiq", Email = "suporte@voltiq.com.br" } }; @@ -112,8 +119,10 @@ prática e eficiente de gerir seus clientes e orçamentos. app.UseHttpsRedirection(); app.UseCors(); + app.UseAuthentication(); app.UseAuthorization(); + app.MapControllers().RequireAuthorization(); await app.RunAsync(); From 2865f8fe13a600cbc7f50525f706d09d6380443c Mon Sep 17 00:00:00 2001 From: Evandro Costa Date: Tue, 31 Mar 2026 16:53:05 -0300 Subject: [PATCH 20/25] refactor(domain): replace generic IRepository with typed role interfaces per entity --- ...etRepository.cs => IBudgetReadOnlyRepository.cs} | 9 ++------- .../Budget/IBudgetUpdateOnlyRepository.cs | 8 ++++++++ .../Budget/IBudgetWriteOnlyRepository.cs | 6 ++++++ ...ntRepository.cs => IClientReadOnlyRepository.cs} | 13 ++++--------- .../Client/IClientUpdateOnlyRepository.cs | 7 +++++++ .../Client/IClientWriteOnlyRepository.cs | 6 ++++++ .../Repositories/IRefreshTokenRepository.cs | 9 --------- .../Interfaces/Repositories/IRepository.cs | 11 ----------- ...Repository.cs => IMaterialReadOnlyRepository.cs} | 7 ++----- .../Material/IMaterialUpdateOnlyRepository.cs | 7 +++++++ .../Material/IMaterialWriteOnlyRepository.cs | 6 ++++++ .../RefreshToken/IRefreshTokenReadOnlyRepository.cs | 6 ++++++ .../IRefreshTokenWriteOnlyRepository.cs | 6 ++++++ ...UserRepository.cs => IUserReadOnlyRepository.cs} | 9 ++++----- .../Repositories/User/IUserWriteOnlyRepository.cs | 6 ++++++ 15 files changed, 70 insertions(+), 46 deletions(-) rename src/Voltiq.Domain/Interfaces/Repositories/Budget/{IBudgetRepository.cs => IBudgetReadOnlyRepository.cs} (82%) create mode 100644 src/Voltiq.Domain/Interfaces/Repositories/Budget/IBudgetUpdateOnlyRepository.cs create mode 100644 src/Voltiq.Domain/Interfaces/Repositories/Budget/IBudgetWriteOnlyRepository.cs rename src/Voltiq.Domain/Interfaces/Repositories/Client/{IClientRepository.cs => IClientReadOnlyRepository.cs} (50%) create mode 100644 src/Voltiq.Domain/Interfaces/Repositories/Client/IClientUpdateOnlyRepository.cs create mode 100644 src/Voltiq.Domain/Interfaces/Repositories/Client/IClientWriteOnlyRepository.cs delete mode 100644 src/Voltiq.Domain/Interfaces/Repositories/IRefreshTokenRepository.cs delete mode 100644 src/Voltiq.Domain/Interfaces/Repositories/IRepository.cs rename src/Voltiq.Domain/Interfaces/Repositories/Material/{IMaterialRepository.cs => IMaterialReadOnlyRepository.cs} (75%) create mode 100644 src/Voltiq.Domain/Interfaces/Repositories/Material/IMaterialUpdateOnlyRepository.cs create mode 100644 src/Voltiq.Domain/Interfaces/Repositories/Material/IMaterialWriteOnlyRepository.cs create mode 100644 src/Voltiq.Domain/Interfaces/Repositories/RefreshToken/IRefreshTokenReadOnlyRepository.cs create mode 100644 src/Voltiq.Domain/Interfaces/Repositories/RefreshToken/IRefreshTokenWriteOnlyRepository.cs rename src/Voltiq.Domain/Interfaces/Repositories/User/{IUserRepository.cs => IUserReadOnlyRepository.cs} (50%) create mode 100644 src/Voltiq.Domain/Interfaces/Repositories/User/IUserWriteOnlyRepository.cs diff --git a/src/Voltiq.Domain/Interfaces/Repositories/Budget/IBudgetRepository.cs b/src/Voltiq.Domain/Interfaces/Repositories/Budget/IBudgetReadOnlyRepository.cs similarity index 82% rename from src/Voltiq.Domain/Interfaces/Repositories/Budget/IBudgetRepository.cs rename to src/Voltiq.Domain/Interfaces/Repositories/Budget/IBudgetReadOnlyRepository.cs index 05fa04d..2f10736 100644 --- a/src/Voltiq.Domain/Interfaces/Repositories/Budget/IBudgetRepository.cs +++ b/src/Voltiq.Domain/Interfaces/Repositories/Budget/IBudgetReadOnlyRepository.cs @@ -1,16 +1,11 @@ -using Voltiq.Domain.Interfaces.Repositories; - namespace Voltiq.Domain.Interfaces.Repositories.Budget; -public interface IBudgetRepository : IRepository +public interface IBudgetReadOnlyRepository { + Task GetByIdAsync(Guid id, CancellationToken cancellationToken = default); Task> GetByUserIdAsync(Guid userId, CancellationToken cancellationToken = default); - Task> GetByClientIdAsync(Guid clientId, CancellationToken cancellationToken = default); - Task GetByIdAndUserIdAsync(Guid id, Guid userId, CancellationToken cancellationToken = default); - Task GetByIdWithItemsAsync(Guid id, CancellationToken cancellationToken = default); - Task GetByIdWithItemsAndUserIdAsync(Guid id, Guid userId, CancellationToken cancellationToken = default); } diff --git a/src/Voltiq.Domain/Interfaces/Repositories/Budget/IBudgetUpdateOnlyRepository.cs b/src/Voltiq.Domain/Interfaces/Repositories/Budget/IBudgetUpdateOnlyRepository.cs new file mode 100644 index 0000000..ff74026 --- /dev/null +++ b/src/Voltiq.Domain/Interfaces/Repositories/Budget/IBudgetUpdateOnlyRepository.cs @@ -0,0 +1,8 @@ +namespace Voltiq.Domain.Interfaces.Repositories.Budget; + +public interface IBudgetUpdateOnlyRepository +{ + Task GetByIdAndUserIdAsync(Guid id, Guid userId, CancellationToken cancellationToken = default); + Task GetByIdWithItemsAndUserIdAsync(Guid id, Guid userId, CancellationToken cancellationToken = default); + void Remove(Entities.Budget entity); +} diff --git a/src/Voltiq.Domain/Interfaces/Repositories/Budget/IBudgetWriteOnlyRepository.cs b/src/Voltiq.Domain/Interfaces/Repositories/Budget/IBudgetWriteOnlyRepository.cs new file mode 100644 index 0000000..6328b20 --- /dev/null +++ b/src/Voltiq.Domain/Interfaces/Repositories/Budget/IBudgetWriteOnlyRepository.cs @@ -0,0 +1,6 @@ +namespace Voltiq.Domain.Interfaces.Repositories.Budget; + +public interface IBudgetWriteOnlyRepository +{ + Task AddAsync(Entities.Budget entity, CancellationToken cancellationToken = default); +} diff --git a/src/Voltiq.Domain/Interfaces/Repositories/Client/IClientRepository.cs b/src/Voltiq.Domain/Interfaces/Repositories/Client/IClientReadOnlyRepository.cs similarity index 50% rename from src/Voltiq.Domain/Interfaces/Repositories/Client/IClientRepository.cs rename to src/Voltiq.Domain/Interfaces/Repositories/Client/IClientReadOnlyRepository.cs index ff22d1e..3bd2210 100644 --- a/src/Voltiq.Domain/Interfaces/Repositories/Client/IClientRepository.cs +++ b/src/Voltiq.Domain/Interfaces/Repositories/Client/IClientReadOnlyRepository.cs @@ -2,14 +2,9 @@ namespace Voltiq.Domain.Interfaces.Repositories.Client; -public interface IClientRepository : IRepository +public interface IClientReadOnlyRepository { - Task> GetByUserIdAsync(Guid userId, - CancellationToken cancellationToken = default); - - Task GetByIdAndUserIdAsync(Guid id, Guid userId, - CancellationToken cancellationToken = default); - - Task ExistsWithEmailForUserAsync(Email email, Guid userId, Guid? excludeId = null, - CancellationToken cancellationToken = default); + Task> GetByUserIdAsync(Guid userId, CancellationToken cancellationToken = default); + Task GetByIdAndUserIdAsync(Guid id, Guid userId, CancellationToken cancellationToken = default); + Task ExistsWithEmailForUserAsync(Email email, Guid userId, Guid? excludeId = null, CancellationToken cancellationToken = default); } diff --git a/src/Voltiq.Domain/Interfaces/Repositories/Client/IClientUpdateOnlyRepository.cs b/src/Voltiq.Domain/Interfaces/Repositories/Client/IClientUpdateOnlyRepository.cs new file mode 100644 index 0000000..08e8db5 --- /dev/null +++ b/src/Voltiq.Domain/Interfaces/Repositories/Client/IClientUpdateOnlyRepository.cs @@ -0,0 +1,7 @@ +namespace Voltiq.Domain.Interfaces.Repositories.Client; + +public interface IClientUpdateOnlyRepository +{ + Task GetByIdAndUserIdAsync(Guid id, Guid userId, CancellationToken cancellationToken = default); + void Remove(Entities.Client entity); +} diff --git a/src/Voltiq.Domain/Interfaces/Repositories/Client/IClientWriteOnlyRepository.cs b/src/Voltiq.Domain/Interfaces/Repositories/Client/IClientWriteOnlyRepository.cs new file mode 100644 index 0000000..6056b3c --- /dev/null +++ b/src/Voltiq.Domain/Interfaces/Repositories/Client/IClientWriteOnlyRepository.cs @@ -0,0 +1,6 @@ +namespace Voltiq.Domain.Interfaces.Repositories.Client; + +public interface IClientWriteOnlyRepository +{ + Task AddAsync(Entities.Client entity, CancellationToken cancellationToken = default); +} diff --git a/src/Voltiq.Domain/Interfaces/Repositories/IRefreshTokenRepository.cs b/src/Voltiq.Domain/Interfaces/Repositories/IRefreshTokenRepository.cs deleted file mode 100644 index c45440a..0000000 --- a/src/Voltiq.Domain/Interfaces/Repositories/IRefreshTokenRepository.cs +++ /dev/null @@ -1,9 +0,0 @@ -using Voltiq.Domain.Entities; - -namespace Voltiq.Domain.Interfaces.Repositories; - -public interface IRefreshTokenRepository -{ - Task GetByTokenAsync(string token, CancellationToken cancellationToken = default); - Task AddAsync(RefreshToken refreshToken, CancellationToken cancellationToken = default); -} diff --git a/src/Voltiq.Domain/Interfaces/Repositories/IRepository.cs b/src/Voltiq.Domain/Interfaces/Repositories/IRepository.cs deleted file mode 100644 index a99a64d..0000000 --- a/src/Voltiq.Domain/Interfaces/Repositories/IRepository.cs +++ /dev/null @@ -1,11 +0,0 @@ -using Voltiq.Domain.Entities; - -namespace Voltiq.Domain.Interfaces.Repositories; - -public interface IRepository where T : BaseEntity -{ - Task GetByIdAsync(Guid id, CancellationToken cancellationToken = default); - Task AddAsync(T entity, CancellationToken cancellationToken = default); - void Update(T entity); - void Remove(T entity); -} diff --git a/src/Voltiq.Domain/Interfaces/Repositories/Material/IMaterialRepository.cs b/src/Voltiq.Domain/Interfaces/Repositories/Material/IMaterialReadOnlyRepository.cs similarity index 75% rename from src/Voltiq.Domain/Interfaces/Repositories/Material/IMaterialRepository.cs rename to src/Voltiq.Domain/Interfaces/Repositories/Material/IMaterialReadOnlyRepository.cs index 1f7444d..33784ff 100644 --- a/src/Voltiq.Domain/Interfaces/Repositories/Material/IMaterialRepository.cs +++ b/src/Voltiq.Domain/Interfaces/Repositories/Material/IMaterialReadOnlyRepository.cs @@ -1,12 +1,9 @@ -using Voltiq.Domain.Interfaces.Repositories; - namespace Voltiq.Domain.Interfaces.Repositories.Material; -public interface IMaterialRepository : IRepository +public interface IMaterialReadOnlyRepository { + Task GetByIdAsync(Guid id, CancellationToken cancellationToken = default); Task> GetByUserIdAsync(Guid userId, CancellationToken cancellationToken = default); - Task GetByIdAndUserIdAsync(Guid id, Guid userId, CancellationToken cancellationToken = default); - Task> GetActiveByUserIdAsync(Guid userId, CancellationToken cancellationToken = default); } diff --git a/src/Voltiq.Domain/Interfaces/Repositories/Material/IMaterialUpdateOnlyRepository.cs b/src/Voltiq.Domain/Interfaces/Repositories/Material/IMaterialUpdateOnlyRepository.cs new file mode 100644 index 0000000..a49585d --- /dev/null +++ b/src/Voltiq.Domain/Interfaces/Repositories/Material/IMaterialUpdateOnlyRepository.cs @@ -0,0 +1,7 @@ +namespace Voltiq.Domain.Interfaces.Repositories.Material; + +public interface IMaterialUpdateOnlyRepository +{ + Task GetByIdAndUserIdAsync(Guid id, Guid userId, CancellationToken cancellationToken = default); + void Remove(Entities.Material entity); +} diff --git a/src/Voltiq.Domain/Interfaces/Repositories/Material/IMaterialWriteOnlyRepository.cs b/src/Voltiq.Domain/Interfaces/Repositories/Material/IMaterialWriteOnlyRepository.cs new file mode 100644 index 0000000..f928237 --- /dev/null +++ b/src/Voltiq.Domain/Interfaces/Repositories/Material/IMaterialWriteOnlyRepository.cs @@ -0,0 +1,6 @@ +namespace Voltiq.Domain.Interfaces.Repositories.Material; + +public interface IMaterialWriteOnlyRepository +{ + Task AddAsync(Entities.Material entity, CancellationToken cancellationToken = default); +} diff --git a/src/Voltiq.Domain/Interfaces/Repositories/RefreshToken/IRefreshTokenReadOnlyRepository.cs b/src/Voltiq.Domain/Interfaces/Repositories/RefreshToken/IRefreshTokenReadOnlyRepository.cs new file mode 100644 index 0000000..898083f --- /dev/null +++ b/src/Voltiq.Domain/Interfaces/Repositories/RefreshToken/IRefreshTokenReadOnlyRepository.cs @@ -0,0 +1,6 @@ +namespace Voltiq.Domain.Interfaces.Repositories.RefreshToken; + +public interface IRefreshTokenReadOnlyRepository +{ + Task GetByTokenAsync(string token, CancellationToken cancellationToken = default); +} diff --git a/src/Voltiq.Domain/Interfaces/Repositories/RefreshToken/IRefreshTokenWriteOnlyRepository.cs b/src/Voltiq.Domain/Interfaces/Repositories/RefreshToken/IRefreshTokenWriteOnlyRepository.cs new file mode 100644 index 0000000..9da9410 --- /dev/null +++ b/src/Voltiq.Domain/Interfaces/Repositories/RefreshToken/IRefreshTokenWriteOnlyRepository.cs @@ -0,0 +1,6 @@ +namespace Voltiq.Domain.Interfaces.Repositories.RefreshToken; + +public interface IRefreshTokenWriteOnlyRepository +{ + Task AddAsync(Entities.RefreshToken entity, CancellationToken cancellationToken = default); +} diff --git a/src/Voltiq.Domain/Interfaces/Repositories/User/IUserRepository.cs b/src/Voltiq.Domain/Interfaces/Repositories/User/IUserReadOnlyRepository.cs similarity index 50% rename from src/Voltiq.Domain/Interfaces/Repositories/User/IUserRepository.cs rename to src/Voltiq.Domain/Interfaces/Repositories/User/IUserReadOnlyRepository.cs index 74ac472..7970af8 100644 --- a/src/Voltiq.Domain/Interfaces/Repositories/User/IUserRepository.cs +++ b/src/Voltiq.Domain/Interfaces/Repositories/User/IUserReadOnlyRepository.cs @@ -2,10 +2,9 @@ namespace Voltiq.Domain.Interfaces.Repositories.User; -public interface IUserRepository : IRepository +public interface IUserReadOnlyRepository { - Task ExistsUserAsync(Document document, Email email, CancellationToken ct = - default); - - Task GetByEmailAsync(Email email, CancellationToken ct = default); + Task GetByIdAsync(Guid id, CancellationToken cancellationToken = default); + Task ExistsUserAsync(Document document, Email email, CancellationToken cancellationToken = default); + Task GetByEmailAsync(Email email, CancellationToken cancellationToken = default); } diff --git a/src/Voltiq.Domain/Interfaces/Repositories/User/IUserWriteOnlyRepository.cs b/src/Voltiq.Domain/Interfaces/Repositories/User/IUserWriteOnlyRepository.cs new file mode 100644 index 0000000..08d4a07 --- /dev/null +++ b/src/Voltiq.Domain/Interfaces/Repositories/User/IUserWriteOnlyRepository.cs @@ -0,0 +1,6 @@ +namespace Voltiq.Domain.Interfaces.Repositories.User; + +public interface IUserWriteOnlyRepository +{ + Task AddAsync(Entities.User entity, CancellationToken cancellationToken = default); +} From 1004684db98e458b106eb165cebe29849d1a36ed Mon Sep 17 00:00:00 2001 From: Evandro Costa Date: Tue, 31 Mar 2026 16:53:11 -0300 Subject: [PATCH 21/25] refactor(infrastructure): replace Repository base class with entity-specific implementations --- .../DependencyInjection.cs | 31 ++++++++++--- .../Repositories/Budget/BudgetRepository.cs | 44 ++++++++++++++----- .../Repositories/Client/ClientRepository.cs | 32 ++++++++------ .../Material/MaterialRepository.cs | 28 +++++++++--- .../Persistence/Repositories/Repository.cs | 32 -------------- .../TokenRepository/RefreshTokenRepository.cs | 13 +++--- .../Repositories/User/UserRepository.cs | 28 ++++++------ 7 files changed, 121 insertions(+), 87 deletions(-) delete mode 100644 src/Voltiq.Infrastructure/Persistence/Repositories/Repository.cs diff --git a/src/Voltiq.Infrastructure/DependencyInjection.cs b/src/Voltiq.Infrastructure/DependencyInjection.cs index 6cedb30..84f5077 100644 --- a/src/Voltiq.Infrastructure/DependencyInjection.cs +++ b/src/Voltiq.Infrastructure/DependencyInjection.cs @@ -7,10 +7,10 @@ using Microsoft.IdentityModel.Tokens; using Voltiq.Application.Common.Interfaces; using Voltiq.Domain.Interfaces; -using Voltiq.Domain.Interfaces.Repositories; using Voltiq.Domain.Interfaces.Repositories.Budget; using Voltiq.Domain.Interfaces.Repositories.Client; using Voltiq.Domain.Interfaces.Repositories.Material; +using Voltiq.Domain.Interfaces.Repositories.RefreshToken; using Voltiq.Domain.Interfaces.Repositories.User; using Voltiq.Exceptions.Resources; using Voltiq.Infrastructure.Auth; @@ -52,12 +52,29 @@ private static void AddAuthServices(IServiceCollection services) private static void AddRepositories(IServiceCollection services) { services.AddScoped(); - services.AddScoped(typeof(IRepository<>), typeof(Repository<>)); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); + + services.AddScoped(); + services.AddScoped(sp => sp.GetRequiredService()); + services.AddScoped(sp => sp.GetRequiredService()); + + services.AddScoped(); + services.AddScoped(sp => sp.GetRequiredService()); + services.AddScoped(sp => sp.GetRequiredService()); + + services.AddScoped(); + services.AddScoped(sp => sp.GetRequiredService()); + services.AddScoped(sp => sp.GetRequiredService()); + services.AddScoped(sp => sp.GetRequiredService()); + + services.AddScoped(); + services.AddScoped(sp => sp.GetRequiredService()); + services.AddScoped(sp => sp.GetRequiredService()); + services.AddScoped(sp => sp.GetRequiredService()); + + services.AddScoped(); + services.AddScoped(sp => sp.GetRequiredService()); + services.AddScoped(sp => sp.GetRequiredService()); + services.AddScoped(sp => sp.GetRequiredService()); } private static void AddDatabase(IServiceCollection services, IConfiguration configuration) diff --git a/src/Voltiq.Infrastructure/Persistence/Repositories/Budget/BudgetRepository.cs b/src/Voltiq.Infrastructure/Persistence/Repositories/Budget/BudgetRepository.cs index 43b3b93..3e46501 100644 --- a/src/Voltiq.Infrastructure/Persistence/Repositories/Budget/BudgetRepository.cs +++ b/src/Voltiq.Infrastructure/Persistence/Repositories/Budget/BudgetRepository.cs @@ -4,37 +4,61 @@ namespace Voltiq.Infrastructure.Persistence.Repositories.Budget; public sealed class BudgetRepository(ApplicationDbContext context) - : Repository(context), IBudgetRepository + : IBudgetReadOnlyRepository, IBudgetWriteOnlyRepository, IBudgetUpdateOnlyRepository { + public async Task GetByIdAsync(Guid id, CancellationToken cancellationToken = default) + => await context.Budgets + .AsNoTracking() + .FirstOrDefaultAsync(b => b.Id == id, cancellationToken); + public async Task> GetByUserIdAsync( Guid userId, CancellationToken cancellationToken = default) - => await Context.Budgets + => await context.Budgets .AsNoTracking() .Where(b => b.UserId == userId) .ToListAsync(cancellationToken); public async Task> GetByClientIdAsync( Guid clientId, CancellationToken cancellationToken = default) - => await Context.Budgets + => await context.Budgets .AsNoTracking() .Where(b => b.ClientId == clientId) .ToListAsync(cancellationToken); - public async Task GetByIdAndUserIdAsync( - Guid id, Guid userId, CancellationToken cancellationToken = default) - => await Context.Budgets + async Task IBudgetReadOnlyRepository.GetByIdAndUserIdAsync( + Guid id, Guid userId, CancellationToken cancellationToken) + => await context.Budgets .AsNoTracking() .FirstOrDefaultAsync(b => b.Id == id && b.UserId == userId, cancellationToken); + async Task IBudgetUpdateOnlyRepository.GetByIdAndUserIdAsync( + Guid id, Guid userId, CancellationToken cancellationToken) + => await context.Budgets + .FirstOrDefaultAsync(b => b.Id == id && b.UserId == userId, cancellationToken); + public async Task GetByIdWithItemsAsync( Guid id, CancellationToken cancellationToken = default) - => await Context.Budgets + => await context.Budgets .Include(b => b.Items) + .AsNoTracking() .FirstOrDefaultAsync(b => b.Id == id, cancellationToken); - public async Task GetByIdWithItemsAndUserIdAsync( - Guid id, Guid userId, CancellationToken cancellationToken = default) - => await Context.Budgets + async Task IBudgetReadOnlyRepository.GetByIdWithItemsAndUserIdAsync( + Guid id, Guid userId, CancellationToken cancellationToken) + => await context.Budgets + .Include(b => b.Items) + .AsNoTracking() + .FirstOrDefaultAsync(b => b.Id == id && b.UserId == userId, cancellationToken); + + async Task IBudgetUpdateOnlyRepository.GetByIdWithItemsAndUserIdAsync( + Guid id, Guid userId, CancellationToken cancellationToken) + => await context.Budgets .Include(b => b.Items) .FirstOrDefaultAsync(b => b.Id == id && b.UserId == userId, cancellationToken); + + public async Task AddAsync(Domain.Entities.Budget entity, CancellationToken cancellationToken = default) + => await context.Budgets.AddAsync(entity, cancellationToken); + + public void Remove(Domain.Entities.Budget entity) + => context.Budgets.Remove(entity); } diff --git a/src/Voltiq.Infrastructure/Persistence/Repositories/Client/ClientRepository.cs b/src/Voltiq.Infrastructure/Persistence/Repositories/Client/ClientRepository.cs index dbf27ca..3dfe426 100644 --- a/src/Voltiq.Infrastructure/Persistence/Repositories/Client/ClientRepository.cs +++ b/src/Voltiq.Infrastructure/Persistence/Repositories/Client/ClientRepository.cs @@ -5,34 +5,38 @@ namespace Voltiq.Infrastructure.Persistence.Repositories.Client; public sealed class ClientRepository(ApplicationDbContext context) - : Repository(context), IClientRepository + : IClientReadOnlyRepository, IClientWriteOnlyRepository, IClientUpdateOnlyRepository { public async Task> GetByUserIdAsync( Guid userId, CancellationToken cancellationToken = default) - { - return await Context.Clients + => await context.Clients .AsNoTracking() .Where(c => c.UserId == userId) .ToListAsync(cancellationToken); - } - public async Task GetByIdAndUserIdAsync( - Guid id, Guid userId, CancellationToken cancellationToken = default) - { - return await Context.Clients + async Task IClientReadOnlyRepository.GetByIdAndUserIdAsync( + Guid id, Guid userId, CancellationToken cancellationToken) + => await context.Clients .AsNoTracking() .FirstOrDefaultAsync(c => c.Id == id && c.UserId == userId, cancellationToken); - } + + async Task IClientUpdateOnlyRepository.GetByIdAndUserIdAsync( + Guid id, Guid userId, CancellationToken cancellationToken) + => await context.Clients + .FirstOrDefaultAsync(c => c.Id == id && c.UserId == userId, cancellationToken); public async Task ExistsWithEmailForUserAsync( - Email email, Guid userId, Guid? excludeId = null, CancellationToken cancellationToken = - default) - { - return await Context.Clients + Email email, Guid userId, Guid? excludeId = null, CancellationToken cancellationToken = default) + => await context.Clients .AsNoTracking() .AnyAsync(c => c.UserId == userId && c.Email == email && (excludeId == null || c.Id != excludeId), cancellationToken); - } + + public async Task AddAsync(Domain.Entities.Client entity, CancellationToken cancellationToken = default) + => await context.Clients.AddAsync(entity, cancellationToken); + + public void Remove(Domain.Entities.Client entity) + => context.Clients.Remove(entity); } diff --git a/src/Voltiq.Infrastructure/Persistence/Repositories/Material/MaterialRepository.cs b/src/Voltiq.Infrastructure/Persistence/Repositories/Material/MaterialRepository.cs index 6a319a8..50c06a2 100644 --- a/src/Voltiq.Infrastructure/Persistence/Repositories/Material/MaterialRepository.cs +++ b/src/Voltiq.Infrastructure/Persistence/Repositories/Material/MaterialRepository.cs @@ -4,25 +4,41 @@ namespace Voltiq.Infrastructure.Persistence.Repositories.Material; public sealed class MaterialRepository(ApplicationDbContext context) - : Repository(context), IMaterialRepository + : IMaterialReadOnlyRepository, IMaterialWriteOnlyRepository, IMaterialUpdateOnlyRepository { + public async Task GetByIdAsync(Guid id, CancellationToken cancellationToken = default) + => await context.Materials + .AsNoTracking() + .FirstOrDefaultAsync(m => m.Id == id, cancellationToken); + public async Task> GetByUserIdAsync( Guid userId, CancellationToken cancellationToken = default) - => await Context.Materials + => await context.Materials .AsNoTracking() .Where(m => m.UserId == userId) .ToListAsync(cancellationToken); - public async Task GetByIdAndUserIdAsync( - Guid id, Guid userId, CancellationToken cancellationToken = default) - => await Context.Materials + async Task IMaterialReadOnlyRepository.GetByIdAndUserIdAsync( + Guid id, Guid userId, CancellationToken cancellationToken) + => await context.Materials .AsNoTracking() .FirstOrDefaultAsync(m => m.Id == id && m.UserId == userId, cancellationToken); + async Task IMaterialUpdateOnlyRepository.GetByIdAndUserIdAsync( + Guid id, Guid userId, CancellationToken cancellationToken) + => await context.Materials + .FirstOrDefaultAsync(m => m.Id == id && m.UserId == userId, cancellationToken); + public async Task> GetActiveByUserIdAsync( Guid userId, CancellationToken cancellationToken = default) - => await Context.Materials + => await context.Materials .AsNoTracking() .Where(m => m.UserId == userId && m.IsActive) .ToListAsync(cancellationToken); + + public async Task AddAsync(Domain.Entities.Material entity, CancellationToken cancellationToken = default) + => await context.Materials.AddAsync(entity, cancellationToken); + + public void Remove(Domain.Entities.Material entity) + => context.Materials.Remove(entity); } diff --git a/src/Voltiq.Infrastructure/Persistence/Repositories/Repository.cs b/src/Voltiq.Infrastructure/Persistence/Repositories/Repository.cs deleted file mode 100644 index 1b32308..0000000 --- a/src/Voltiq.Infrastructure/Persistence/Repositories/Repository.cs +++ /dev/null @@ -1,32 +0,0 @@ -using Microsoft.EntityFrameworkCore; -using Voltiq.Domain.Entities; -using Voltiq.Domain.Interfaces.Repositories; - -namespace Voltiq.Infrastructure.Persistence.Repositories; - -public class Repository(ApplicationDbContext context) : IRepository - where T : BaseEntity -{ - private readonly DbSet _dbSet = context.Set(); - protected ApplicationDbContext Context { get; } = context; - - public async Task GetByIdAsync(Guid id, CancellationToken cancellationToken = default) - { - return await _dbSet.FirstOrDefaultAsync(c => c.Id == id, cancellationToken); - } - - public async Task AddAsync(T entity, CancellationToken cancellationToken = default) - { - await _dbSet.AddAsync(entity, cancellationToken); - } - - public void Update(T entity) - { - _dbSet.Update(entity); - } - - public void Remove(T entity) - { - _dbSet.Remove(entity); - } -} diff --git a/src/Voltiq.Infrastructure/Persistence/Repositories/TokenRepository/RefreshTokenRepository.cs b/src/Voltiq.Infrastructure/Persistence/Repositories/TokenRepository/RefreshTokenRepository.cs index aae1a32..4735a08 100644 --- a/src/Voltiq.Infrastructure/Persistence/Repositories/TokenRepository/RefreshTokenRepository.cs +++ b/src/Voltiq.Infrastructure/Persistence/Repositories/TokenRepository/RefreshTokenRepository.cs @@ -1,13 +1,16 @@ using Microsoft.EntityFrameworkCore; using Voltiq.Domain.Entities; -using Voltiq.Domain.Interfaces.Repositories; +using Voltiq.Domain.Interfaces.Repositories.RefreshToken; namespace Voltiq.Infrastructure.Persistence.Repositories.TokenRepository; -public class RefreshTokenRepository(ApplicationDbContext context) - : Repository(context), IRefreshTokenRepository +public sealed class RefreshTokenRepository(ApplicationDbContext context) + : IRefreshTokenReadOnlyRepository, IRefreshTokenWriteOnlyRepository { - public async Task GetByTokenAsync(string token, CancellationToken cancellationToken = default) => - await Context.RefreshTokens + public async Task GetByTokenAsync(string token, CancellationToken cancellationToken = default) + => await context.RefreshTokens .FirstOrDefaultAsync(r => r.Token == token, cancellationToken); + + public async Task AddAsync(RefreshToken entity, CancellationToken cancellationToken = default) + => await context.RefreshTokens.AddAsync(entity, cancellationToken); } diff --git a/src/Voltiq.Infrastructure/Persistence/Repositories/User/UserRepository.cs b/src/Voltiq.Infrastructure/Persistence/Repositories/User/UserRepository.cs index d9f5dbf..c75f53b 100644 --- a/src/Voltiq.Infrastructure/Persistence/Repositories/User/UserRepository.cs +++ b/src/Voltiq.Infrastructure/Persistence/Repositories/User/UserRepository.cs @@ -4,21 +4,23 @@ namespace Voltiq.Infrastructure.Persistence.Repositories.User; -public class UserRepository(ApplicationDbContext context) - : Repository(context), IUserRepository +public sealed class UserRepository(ApplicationDbContext context) + : IUserReadOnlyRepository, IUserWriteOnlyRepository { + public async Task GetByIdAsync(Guid id, CancellationToken cancellationToken = default) + => await context.Users.AsNoTracking() + .FirstOrDefaultAsync(u => u.Id == id, cancellationToken); + public async Task ExistsUserAsync(Document document, Email email, - CancellationToken ct = default) - { - return await Context.Users.AsNoTracking().AnyAsync(user => - user.Document == document || user.Email == email, - cancellationToken: ct); - } + CancellationToken cancellationToken = default) + => await context.Users.AsNoTracking() + .AnyAsync(u => u.Document == document || u.Email == email, cancellationToken); public async Task GetByEmailAsync(Email email, - CancellationToken ct = default) - { - return await Context.Users.AsNoTracking() - .FirstOrDefaultAsync(user => user.Email == email, cancellationToken: ct); - } + CancellationToken cancellationToken = default) + => await context.Users.AsNoTracking() + .FirstOrDefaultAsync(u => u.Email == email, cancellationToken); + + public async Task AddAsync(Domain.Entities.User entity, CancellationToken cancellationToken = default) + => await context.Users.AddAsync(entity, cancellationToken); } From cadcf447ee18175bc2d0425442d1da1518474a23 Mon Sep 17 00:00:00 2001 From: Evandro Costa Date: Tue, 31 Mar 2026 16:53:17 -0300 Subject: [PATCH 22/25] refactor(application): inject typed repository interfaces into command and query handlers --- .../Common/Behaviors/AuthorizationBehavior.cs | 1 - .../Auth/Commands/Login/LoginCommandHandler.cs | 6 +++--- .../Commands/Refresh/RefreshTokenCommandHandler.cs | 11 ++++++----- .../DeleteClient/DeleteClientCommandHandler.cs | 2 +- .../RegisterClient/RegisterClientCommandHandler.cs | 7 ++++--- .../UpdateClient/UpdateClientCommandHandler.cs | 7 ++++--- .../GetClientById/GetClientByIdQueryHandler.cs | 2 +- .../Queries/GetClients/GetClientsQueryHandler.cs | 2 +- .../RegisterUser/RegisterUserCommandHandler.cs | 11 ++++++----- .../GetCurrentUser/GetCurrentUserQueryHandler.cs | 4 ++-- 10 files changed, 28 insertions(+), 25 deletions(-) diff --git a/src/Voltiq.Application/Common/Behaviors/AuthorizationBehavior.cs b/src/Voltiq.Application/Common/Behaviors/AuthorizationBehavior.cs index 3678947..26eaf70 100644 --- a/src/Voltiq.Application/Common/Behaviors/AuthorizationBehavior.cs +++ b/src/Voltiq.Application/Common/Behaviors/AuthorizationBehavior.cs @@ -1,4 +1,3 @@ -using System.Reflection; using ErrorOr; using MediatR; using Voltiq.Application.Common.Interfaces; diff --git a/src/Voltiq.Application/Features/Auth/Commands/Login/LoginCommandHandler.cs b/src/Voltiq.Application/Features/Auth/Commands/Login/LoginCommandHandler.cs index a8d7745..8827c0e 100644 --- a/src/Voltiq.Application/Features/Auth/Commands/Login/LoginCommandHandler.cs +++ b/src/Voltiq.Application/Features/Auth/Commands/Login/LoginCommandHandler.cs @@ -3,7 +3,7 @@ using Voltiq.Application.Common.Interfaces; using Voltiq.Domain.Entities; using Voltiq.Domain.Interfaces; -using Voltiq.Domain.Interfaces.Repositories; +using Voltiq.Domain.Interfaces.Repositories.RefreshToken; using Voltiq.Domain.Interfaces.Repositories.User; using Voltiq.Domain.ValueObjects; using Voltiq.Exceptions.Resources; @@ -11,10 +11,10 @@ namespace Voltiq.Application.Features.Auth.Commands.Login; public sealed class LoginCommandHandler( - IUserRepository userRepository, + IUserReadOnlyRepository userRepository, IPasswordHasher passwordHasher, ITokenService tokenService, - IRefreshTokenRepository refreshTokenRepository, + IRefreshTokenWriteOnlyRepository refreshTokenRepository, IUnitOfWork unitOfWork) : IRequestHandler> { diff --git a/src/Voltiq.Application/Features/Auth/Commands/Refresh/RefreshTokenCommandHandler.cs b/src/Voltiq.Application/Features/Auth/Commands/Refresh/RefreshTokenCommandHandler.cs index 004e38b..9bc1680 100644 --- a/src/Voltiq.Application/Features/Auth/Commands/Refresh/RefreshTokenCommandHandler.cs +++ b/src/Voltiq.Application/Features/Auth/Commands/Refresh/RefreshTokenCommandHandler.cs @@ -3,22 +3,23 @@ using Voltiq.Application.Common.Interfaces; using Voltiq.Domain.Entities; using Voltiq.Domain.Interfaces; -using Voltiq.Domain.Interfaces.Repositories; +using Voltiq.Domain.Interfaces.Repositories.RefreshToken; using Voltiq.Domain.Interfaces.Repositories.User; using Voltiq.Exceptions.Resources; namespace Voltiq.Application.Features.Auth.Commands.Refresh; public sealed class RefreshTokenCommandHandler( - IRefreshTokenRepository refreshTokenRepository, - IUserRepository userRepository, + IRefreshTokenReadOnlyRepository refreshTokenReadOnlyRepository, + IRefreshTokenWriteOnlyRepository refreshTokenWriteOnlyRepository, + IUserReadOnlyRepository userRepository, ITokenService tokenService, IUnitOfWork unitOfWork) : IRequestHandler> { public async Task> Handle(RefreshTokenCommand request, CancellationToken cancellationToken) { - var refreshToken = await refreshTokenRepository.GetByTokenAsync(request.RefreshToken, cancellationToken); + var refreshToken = await refreshTokenReadOnlyRepository.GetByTokenAsync(request.RefreshToken, cancellationToken); if (refreshToken is null) return Error.Unauthorized(description: ResourceErrorMessages.REFRESH_TOKEN_NAO_ENCONTRADO); @@ -40,7 +41,7 @@ public async Task> Handle(RefreshTokenCommand request, Can var newRawRefreshToken = tokenService.GenerateRefreshToken(); var newRefreshToken = RefreshToken.Create(newRawRefreshToken, user.Id, expiresInDays: 7); - await refreshTokenRepository.AddAsync(newRefreshToken, cancellationToken); + await refreshTokenWriteOnlyRepository.AddAsync(newRefreshToken, cancellationToken); await unitOfWork.SaveChangesAsync(cancellationToken); return new AuthResponse(newAccessToken, newRawRefreshToken); diff --git a/src/Voltiq.Application/Features/Clients/Commands/DeleteClient/DeleteClientCommandHandler.cs b/src/Voltiq.Application/Features/Clients/Commands/DeleteClient/DeleteClientCommandHandler.cs index 5032df1..a211580 100644 --- a/src/Voltiq.Application/Features/Clients/Commands/DeleteClient/DeleteClientCommandHandler.cs +++ b/src/Voltiq.Application/Features/Clients/Commands/DeleteClient/DeleteClientCommandHandler.cs @@ -7,7 +7,7 @@ namespace Voltiq.Application.Features.Clients.Commands.DeleteClient; public sealed class DeleteClientCommandHandler( - IClientRepository clientRepository, + IClientUpdateOnlyRepository clientRepository, IUnitOfWork unitOfWork) : IRequestHandler> { diff --git a/src/Voltiq.Application/Features/Clients/Commands/RegisterClient/RegisterClientCommandHandler.cs b/src/Voltiq.Application/Features/Clients/Commands/RegisterClient/RegisterClientCommandHandler.cs index 291f646..9f477bb 100644 --- a/src/Voltiq.Application/Features/Clients/Commands/RegisterClient/RegisterClientCommandHandler.cs +++ b/src/Voltiq.Application/Features/Clients/Commands/RegisterClient/RegisterClientCommandHandler.cs @@ -10,7 +10,8 @@ namespace Voltiq.Application.Features.Clients.Commands.RegisterClient; public sealed class RegisterClientCommandHandler( - IClientRepository clientRepository, + IClientReadOnlyRepository clientReadOnlyRepository, + IClientWriteOnlyRepository clientWriteOnlyRepository, IUnitOfWork unitOfWork) : IRequestHandler> { @@ -19,7 +20,7 @@ public async Task> Handle(RegisterClientCommand request, { var email = Email.Create(request.Email).Value; - var emailExists = await clientRepository.ExistsWithEmailForUserAsync( + var emailExists = await clientReadOnlyRepository.ExistsWithEmailForUserAsync( email, request.UserId, cancellationToken: cancellationToken); if (emailExists) @@ -29,7 +30,7 @@ public async Task> Handle(RegisterClientCommand request, request.ZipCode); var client = Client.Register(request.UserId, request.Name, request.Phone, email, address); - await clientRepository.AddAsync(client, cancellationToken); + await clientWriteOnlyRepository.AddAsync(client, cancellationToken); await unitOfWork.SaveChangesAsync(cancellationToken); return client.ToResponse(); diff --git a/src/Voltiq.Application/Features/Clients/Commands/UpdateClient/UpdateClientCommandHandler.cs b/src/Voltiq.Application/Features/Clients/Commands/UpdateClient/UpdateClientCommandHandler.cs index 733a12a..ed46539 100644 --- a/src/Voltiq.Application/Features/Clients/Commands/UpdateClient/UpdateClientCommandHandler.cs +++ b/src/Voltiq.Application/Features/Clients/Commands/UpdateClient/UpdateClientCommandHandler.cs @@ -8,7 +8,8 @@ namespace Voltiq.Application.Features.Clients.Commands.UpdateClient; public sealed class UpdateClientCommandHandler( - IClientRepository clientRepository, + IClientReadOnlyRepository clientReadOnlyRepository, + IClientUpdateOnlyRepository clientUpdateOnlyRepository, IUnitOfWork unitOfWork) : IRequestHandler> { @@ -16,14 +17,14 @@ public async Task> Handle(UpdateClientCommand request, CancellationToken cancellationToken) { var client = - await clientRepository.GetByIdAndUserIdAsync(request.Id, request.UserId, cancellationToken); + await clientUpdateOnlyRepository.GetByIdAndUserIdAsync(request.Id, request.UserId, cancellationToken); if (client is null) return Error.NotFound(description: ResourceErrorMessages.CLIENTE_NAO_ENCONTRADO); var email = Email.Create(request.Email).Value; - var emailExists = await clientRepository.ExistsWithEmailForUserAsync( + var emailExists = await clientReadOnlyRepository.ExistsWithEmailForUserAsync( email, request.UserId, request.Id, cancellationToken); if (emailExists) diff --git a/src/Voltiq.Application/Features/Clients/Queries/GetClientById/GetClientByIdQueryHandler.cs b/src/Voltiq.Application/Features/Clients/Queries/GetClientById/GetClientByIdQueryHandler.cs index b852e59..c204cd9 100644 --- a/src/Voltiq.Application/Features/Clients/Queries/GetClientById/GetClientByIdQueryHandler.cs +++ b/src/Voltiq.Application/Features/Clients/Queries/GetClientById/GetClientByIdQueryHandler.cs @@ -6,7 +6,7 @@ namespace Voltiq.Application.Features.Clients.Queries.GetClientById; -public sealed class GetClientByIdQueryHandler(IClientRepository clientRepository) +public sealed class GetClientByIdQueryHandler(IClientReadOnlyRepository clientRepository) : IRequestHandler> { public async Task> Handle(GetClientByIdQuery request, CancellationToken cancellationToken) diff --git a/src/Voltiq.Application/Features/Clients/Queries/GetClients/GetClientsQueryHandler.cs b/src/Voltiq.Application/Features/Clients/Queries/GetClients/GetClientsQueryHandler.cs index 1475885..110f630 100644 --- a/src/Voltiq.Application/Features/Clients/Queries/GetClients/GetClientsQueryHandler.cs +++ b/src/Voltiq.Application/Features/Clients/Queries/GetClients/GetClientsQueryHandler.cs @@ -5,7 +5,7 @@ namespace Voltiq.Application.Features.Clients.Queries.GetClients; -public sealed class GetClientsQueryHandler(IClientRepository clientRepository) +public sealed class GetClientsQueryHandler(IClientReadOnlyRepository clientRepository) : IRequestHandler>> { public async Task>> Handle(GetClientsQuery request, CancellationToken cancellationToken) diff --git a/src/Voltiq.Application/Features/Users/Commands/RegisterUser/RegisterUserCommandHandler.cs b/src/Voltiq.Application/Features/Users/Commands/RegisterUser/RegisterUserCommandHandler.cs index 352f94f..0be9034 100644 --- a/src/Voltiq.Application/Features/Users/Commands/RegisterUser/RegisterUserCommandHandler.cs +++ b/src/Voltiq.Application/Features/Users/Commands/RegisterUser/RegisterUserCommandHandler.cs @@ -4,7 +4,7 @@ using Voltiq.Application.Mappings.Users; using Voltiq.Domain.Entities; using Voltiq.Domain.Interfaces; -using Voltiq.Domain.Interfaces.Repositories; +using Voltiq.Domain.Interfaces.Repositories.RefreshToken; using Voltiq.Domain.Interfaces.Repositories.User; using Voltiq.Domain.ValueObjects; using Voltiq.Exceptions.Resources; @@ -12,11 +12,12 @@ namespace Voltiq.Application.Features.Users.Commands.RegisterUser; public sealed class RegisterUserCommandHandler( - IUserRepository userRepository, + IUserReadOnlyRepository userReadOnlyRepository, + IUserWriteOnlyRepository userWriteOnlyRepository, IUnitOfWork unitOfWork, IPasswordHasher passwordHasher, ITokenService tokenService, - IRefreshTokenRepository refreshTokenRepository) + IRefreshTokenWriteOnlyRepository refreshTokenRepository) : IRequestHandler> { public async Task> Handle(RegisterUserCommand request, CancellationToken cancellationToken) @@ -24,7 +25,7 @@ public async Task> Handle(RegisterUserCommand requ var email = Email.Create(request.Email).Value; var document = Document.Create(request.Document).Value; - var userAlreadyExists = await userRepository.ExistsUserAsync( + var userAlreadyExists = await userReadOnlyRepository.ExistsUserAsync( document, email, cancellationToken); if (userAlreadyExists) @@ -34,7 +35,7 @@ public async Task> Handle(RegisterUserCommand requ var user = User.Register(request.Name, email, document, passwordHash); - await userRepository.AddAsync(user, cancellationToken); + await userWriteOnlyRepository.AddAsync(user, cancellationToken); var accessToken = tokenService.GenerateAccessToken(user.Id.ToString(), user.Name, []); var rawRefreshToken = tokenService.GenerateRefreshToken(); diff --git a/src/Voltiq.Application/Features/Users/Queries/GetCurrentUser/GetCurrentUserQueryHandler.cs b/src/Voltiq.Application/Features/Users/Queries/GetCurrentUser/GetCurrentUserQueryHandler.cs index 55a22b7..5c39125 100644 --- a/src/Voltiq.Application/Features/Users/Queries/GetCurrentUser/GetCurrentUserQueryHandler.cs +++ b/src/Voltiq.Application/Features/Users/Queries/GetCurrentUser/GetCurrentUserQueryHandler.cs @@ -2,12 +2,12 @@ using MediatR; using Voltiq.Application.Mappings.Users; using Voltiq.Domain.Entities; -using Voltiq.Domain.Interfaces.Repositories; +using Voltiq.Domain.Interfaces.Repositories.User; using Voltiq.Exceptions.Resources; namespace Voltiq.Application.Features.Users.Queries.GetCurrentUser; -public sealed class GetCurrentUserQueryHandler(IRepository userRepository) +public sealed class GetCurrentUserQueryHandler(IUserReadOnlyRepository userRepository) : IRequestHandler> { public async Task> Handle(GetCurrentUserQuery request, CancellationToken cancellationToken) From 4dc07aaae4a05729692474d29733bc4d8309019b Mon Sep 17 00:00:00 2001 From: Evandro Costa Date: Tue, 31 Mar 2026 16:53:23 -0300 Subject: [PATCH 23/25] test(application): update mocks to use typed repository interfaces --- .../Features/Auth/LoginCommandHandlerTests.cs | 6 ++--- .../Auth/RefreshTokenCommandHandlerTests.cs | 23 ++++++++++--------- .../DeleteClientCommandHandlerTests.cs | 2 +- .../RegisterClientCommandHandlerTests.cs | 13 ++++++----- .../UpdateClientCommandHandlerTests.cs | 15 ++++++------ .../Queries/GetClientByIdQueryHandlerTests.cs | 2 +- .../Queries/GetClientsQueryHandlerTests.cs | 2 +- .../Users/GetCurrentUserQueryHandlerTests.cs | 4 ++-- .../Users/RegisterUserCommandHandlerTests.cs | 17 +++++++------- 9 files changed, 44 insertions(+), 40 deletions(-) diff --git a/tests/Voltiq.Application.Tests/Features/Auth/LoginCommandHandlerTests.cs b/tests/Voltiq.Application.Tests/Features/Auth/LoginCommandHandlerTests.cs index 51c8974..db119c3 100644 --- a/tests/Voltiq.Application.Tests/Features/Auth/LoginCommandHandlerTests.cs +++ b/tests/Voltiq.Application.Tests/Features/Auth/LoginCommandHandlerTests.cs @@ -5,7 +5,7 @@ using Voltiq.Application.Features.Auth.Commands.Login; using Voltiq.Domain.Entities; using Voltiq.Domain.Interfaces; -using Voltiq.Domain.Interfaces.Repositories; +using Voltiq.Domain.Interfaces.Repositories.RefreshToken; using Voltiq.Domain.Interfaces.Repositories.User; using Voltiq.Domain.ValueObjects; using Voltiq.Exceptions.Resources; @@ -14,10 +14,10 @@ namespace Voltiq.Application.Tests.Features.Auth; public class LoginCommandHandlerTests { - private readonly Mock _userRepoMock = new(); + private readonly Mock _userRepoMock = new(); private readonly Mock _passwordHasherMock = new(); private readonly Mock _tokenServiceMock = new(); - private readonly Mock _refreshTokenRepoMock = new(); + private readonly Mock _refreshTokenRepoMock = new(); private readonly Mock _unitOfWorkMock = new(); private LoginCommandHandler CreateHandler() => diff --git a/tests/Voltiq.Application.Tests/Features/Auth/RefreshTokenCommandHandlerTests.cs b/tests/Voltiq.Application.Tests/Features/Auth/RefreshTokenCommandHandlerTests.cs index f5ed7f9..74edcca 100644 --- a/tests/Voltiq.Application.Tests/Features/Auth/RefreshTokenCommandHandlerTests.cs +++ b/tests/Voltiq.Application.Tests/Features/Auth/RefreshTokenCommandHandlerTests.cs @@ -5,7 +5,7 @@ using Voltiq.Application.Features.Auth.Commands.Refresh; using Voltiq.Domain.Entities; using Voltiq.Domain.Interfaces; -using Voltiq.Domain.Interfaces.Repositories; +using Voltiq.Domain.Interfaces.Repositories.RefreshToken; using Voltiq.Domain.Interfaces.Repositories.User; using Voltiq.Domain.ValueObjects; using Voltiq.Exceptions.Resources; @@ -14,14 +14,15 @@ namespace Voltiq.Application.Tests.Features.Auth; public class RefreshTokenCommandHandlerTests { - private readonly Mock _refreshTokenRepoMock = new(); - private readonly Mock _userRepoMock = new(); + private readonly Mock _refreshTokenReadRepoMock = new(); + private readonly Mock _refreshTokenWriteRepoMock = new(); + private readonly Mock _userRepoMock = new(); private readonly Mock _tokenServiceMock = new(); private readonly Mock _unitOfWorkMock = new(); private RefreshTokenCommandHandler CreateHandler() => - new(_refreshTokenRepoMock.Object, _userRepoMock.Object, - _tokenServiceMock.Object, _unitOfWorkMock.Object); + new(_refreshTokenReadRepoMock.Object, _refreshTokenWriteRepoMock.Object, + _userRepoMock.Object, _tokenServiceMock.Object, _unitOfWorkMock.Object); private static User MakeUser() { @@ -33,7 +34,7 @@ private static User MakeUser() [Fact] public async Task Handle_WhenTokenNotFound_ReturnsUnauthorizedWithNotFoundMessage() { - _refreshTokenRepoMock + _refreshTokenReadRepoMock .Setup(r => r.GetByTokenAsync(It.IsAny(), It.IsAny())) .ReturnsAsync((RefreshToken?)null); @@ -49,7 +50,7 @@ public async Task Handle_WhenTokenExpired_ReturnsUnauthorizedWithExpiredMessage( { var expiredToken = RefreshToken.Create("expired-token", Guid.NewGuid(), expiresInDays: -1); - _refreshTokenRepoMock + _refreshTokenReadRepoMock .Setup(r => r.GetByTokenAsync(It.IsAny(), It.IsAny())) .ReturnsAsync(expiredToken); @@ -69,7 +70,7 @@ public async Task Handle_WhenTokenRevoked_ReturnsUnauthorizedWithInvalidMessage( var revokedToken = RefreshToken.Create("revoked-token", Guid.NewGuid(), expiresInDays: 7); revokedToken.Revoke(); - _refreshTokenRepoMock + _refreshTokenReadRepoMock .Setup(r => r.GetByTokenAsync(It.IsAny(), It.IsAny())) .ReturnsAsync(revokedToken); @@ -86,7 +87,7 @@ public async Task Handle_WhenUserNotFoundAfterValidToken_ReturnsUnauthorizedWith { var activeToken = RefreshToken.Create("active-token", Guid.NewGuid(), expiresInDays: 7); - _refreshTokenRepoMock + _refreshTokenReadRepoMock .Setup(r => r.GetByTokenAsync(It.IsAny(), It.IsAny())) .ReturnsAsync(activeToken); _userRepoMock @@ -107,7 +108,7 @@ public async Task Handle_WhenTokenValid_RevokesOldToken() var user = MakeUser(); var activeToken = RefreshToken.Create("active-token", user.Id, expiresInDays: 7); - _refreshTokenRepoMock + _refreshTokenReadRepoMock .Setup(r => r.GetByTokenAsync(It.IsAny(), It.IsAny())) .ReturnsAsync(activeToken); _userRepoMock @@ -132,7 +133,7 @@ public async Task Handle_WhenTokenValid_ReturnsNewAccessAndRefreshTokens() var user = MakeUser(); var activeToken = RefreshToken.Create("active-token", user.Id, expiresInDays: 7); - _refreshTokenRepoMock + _refreshTokenReadRepoMock .Setup(r => r.GetByTokenAsync(It.IsAny(), It.IsAny())) .ReturnsAsync(activeToken); _userRepoMock diff --git a/tests/Voltiq.Application.Tests/Features/Clients/Commands/DeleteClientCommandHandlerTests.cs b/tests/Voltiq.Application.Tests/Features/Clients/Commands/DeleteClientCommandHandlerTests.cs index 1c9660a..9b4d198 100644 --- a/tests/Voltiq.Application.Tests/Features/Clients/Commands/DeleteClientCommandHandlerTests.cs +++ b/tests/Voltiq.Application.Tests/Features/Clients/Commands/DeleteClientCommandHandlerTests.cs @@ -12,7 +12,7 @@ namespace Voltiq.Application.Tests.Features.Clients.Commands; public class DeleteClientCommandHandlerTests { - private readonly Mock _clientRepoMock = new(); + private readonly Mock _clientRepoMock = new(); private readonly Mock _unitOfWorkMock = new(); private readonly Guid _userId = Guid.NewGuid(); diff --git a/tests/Voltiq.Application.Tests/Features/Clients/Commands/RegisterClientCommandHandlerTests.cs b/tests/Voltiq.Application.Tests/Features/Clients/Commands/RegisterClientCommandHandlerTests.cs index b110aa0..550b848 100644 --- a/tests/Voltiq.Application.Tests/Features/Clients/Commands/RegisterClientCommandHandlerTests.cs +++ b/tests/Voltiq.Application.Tests/Features/Clients/Commands/RegisterClientCommandHandlerTests.cs @@ -12,14 +12,15 @@ namespace Voltiq.Application.Tests.Features.Clients.Commands; public class RegisterClientCommandHandlerTests { - private readonly Mock _clientRepoMock = new(); + private readonly Mock _clientReadRepoMock = new(); + private readonly Mock _clientWriteRepoMock = new(); private readonly Mock _unitOfWorkMock = new(); private readonly Guid _userId = Guid.NewGuid(); private RegisterClientCommandHandler CreateHandler() { - return new RegisterClientCommandHandler(_clientRepoMock.Object, _unitOfWorkMock.Object); + return new RegisterClientCommandHandler(_clientReadRepoMock.Object, _clientWriteRepoMock.Object, _unitOfWorkMock.Object); } private RegisterClientCommand ValidCommand() => @@ -30,7 +31,7 @@ private RegisterClientCommand ValidCommand() => [Fact] public async Task Handle_WithValidCommand_ShouldRegisterClientAndReturnResponse() { - _clientRepoMock + _clientReadRepoMock .Setup(r => r.ExistsWithEmailForUserAsync(It.IsAny(), _userId, null, It.IsAny())) .ReturnsAsync(false); @@ -45,7 +46,7 @@ public async Task Handle_WithValidCommand_ShouldRegisterClientAndReturnResponse( result.Value.Email.ShouldBe("joao@example.com"); result.Value.Street.ShouldBe("Rua das Flores"); result.Value.City.ShouldBe("São Paulo"); - _clientRepoMock.Verify(r => r.AddAsync(It.IsAny(), It.IsAny()), + _clientWriteRepoMock.Verify(r => r.AddAsync(It.IsAny(), It.IsAny()), Times.Once); _unitOfWorkMock.Verify(u => u.SaveChangesAsync(It.IsAny()), Times.Once); } @@ -53,7 +54,7 @@ public async Task Handle_WithValidCommand_ShouldRegisterClientAndReturnResponse( [Fact] public async Task Handle_WhenEmailAlreadyExistsForUser_ShouldReturnConflictError() { - _clientRepoMock + _clientReadRepoMock .Setup(r => r.ExistsWithEmailForUserAsync(It.IsAny(), _userId, null, It.IsAny())) .ReturnsAsync(true); @@ -64,7 +65,7 @@ public async Task Handle_WhenEmailAlreadyExistsForUser_ShouldReturnConflictError result.IsError.ShouldBeTrue(); result.FirstError.Type.ShouldBe(ErrorType.Conflict); result.FirstError.Description.ShouldBe(ResourceErrorMessages.CLIENTE_EMAIL_JA_CADASTRADO); - _clientRepoMock.Verify(r => r.AddAsync(It.IsAny(), It.IsAny()), + _clientWriteRepoMock.Verify(r => r.AddAsync(It.IsAny(), It.IsAny()), Times.Never); } } diff --git a/tests/Voltiq.Application.Tests/Features/Clients/Commands/UpdateClientCommandHandlerTests.cs b/tests/Voltiq.Application.Tests/Features/Clients/Commands/UpdateClientCommandHandlerTests.cs index fa8ecc2..fe035f4 100644 --- a/tests/Voltiq.Application.Tests/Features/Clients/Commands/UpdateClientCommandHandlerTests.cs +++ b/tests/Voltiq.Application.Tests/Features/Clients/Commands/UpdateClientCommandHandlerTests.cs @@ -12,14 +12,15 @@ namespace Voltiq.Application.Tests.Features.Clients.Commands; public class UpdateClientCommandHandlerTests { - private readonly Mock _clientRepoMock = new(); + private readonly Mock _clientReadRepoMock = new(); + private readonly Mock _clientUpdateRepoMock = new(); private readonly Mock _unitOfWorkMock = new(); private readonly Guid _userId = Guid.NewGuid(); private UpdateClientCommandHandler CreateHandler() { - return new UpdateClientCommandHandler(_clientRepoMock.Object, _unitOfWorkMock.Object); + return new UpdateClientCommandHandler(_clientReadRepoMock.Object, _clientUpdateRepoMock.Object, _unitOfWorkMock.Object); } private static Client MakeClient(Guid userId) @@ -33,10 +34,10 @@ private static Client MakeClient(Guid userId) public async Task Handle_WithValidCommand_ShouldUpdateClientAndReturnUpdated() { var client = MakeClient(_userId); - _clientRepoMock + _clientUpdateRepoMock .Setup(r => r.GetByIdAndUserIdAsync(client.Id, _userId, It.IsAny())) .ReturnsAsync(client); - _clientRepoMock + _clientReadRepoMock .Setup(r => r.ExistsWithEmailForUserAsync(It.IsAny(), _userId, client.Id, It .IsAny())) .ReturnsAsync(false); @@ -56,10 +57,10 @@ public async Task Handle_WithValidCommand_ShouldUpdateClientAndReturnUpdated() public async Task Handle_WhenEmailAlreadyExistsForAnotherClient_ShouldReturnConflictError() { var client = MakeClient(_userId); - _clientRepoMock + _clientUpdateRepoMock .Setup(r => r.GetByIdAndUserIdAsync(client.Id, _userId, It.IsAny())) .ReturnsAsync(client); - _clientRepoMock + _clientReadRepoMock .Setup(r => r.ExistsWithEmailForUserAsync(It.IsAny(), _userId, client.Id, It .IsAny())) .ReturnsAsync(true); @@ -80,7 +81,7 @@ public async Task Handle_WhenEmailAlreadyExistsForAnotherClient_ShouldReturnConf [Fact] public async Task Handle_WhenClientNotFound_ShouldReturnNotFoundError() { - _clientRepoMock + _clientUpdateRepoMock .Setup(r => r.GetByIdAndUserIdAsync(It.IsAny(), _userId, It.IsAny())) .ReturnsAsync((Client?)null); diff --git a/tests/Voltiq.Application.Tests/Features/Clients/Queries/GetClientByIdQueryHandlerTests.cs b/tests/Voltiq.Application.Tests/Features/Clients/Queries/GetClientByIdQueryHandlerTests.cs index ad8c3fe..f583a59 100644 --- a/tests/Voltiq.Application.Tests/Features/Clients/Queries/GetClientByIdQueryHandlerTests.cs +++ b/tests/Voltiq.Application.Tests/Features/Clients/Queries/GetClientByIdQueryHandlerTests.cs @@ -11,7 +11,7 @@ namespace Voltiq.Application.Tests.Features.Clients.Queries; public class GetClientByIdQueryHandlerTests { - private readonly Mock _clientRepoMock = new(); + private readonly Mock _clientRepoMock = new(); private readonly Guid _userId = Guid.NewGuid(); diff --git a/tests/Voltiq.Application.Tests/Features/Clients/Queries/GetClientsQueryHandlerTests.cs b/tests/Voltiq.Application.Tests/Features/Clients/Queries/GetClientsQueryHandlerTests.cs index 736339f..0bb656a 100644 --- a/tests/Voltiq.Application.Tests/Features/Clients/Queries/GetClientsQueryHandlerTests.cs +++ b/tests/Voltiq.Application.Tests/Features/Clients/Queries/GetClientsQueryHandlerTests.cs @@ -10,7 +10,7 @@ namespace Voltiq.Application.Tests.Features.Clients.Queries; public class GetClientsQueryHandlerTests { - private readonly Mock _clientRepoMock = new(); + private readonly Mock _clientRepoMock = new(); private readonly Guid _userId = Guid.NewGuid(); diff --git a/tests/Voltiq.Application.Tests/Features/Users/GetCurrentUserQueryHandlerTests.cs b/tests/Voltiq.Application.Tests/Features/Users/GetCurrentUserQueryHandlerTests.cs index 625338f..21b29df 100644 --- a/tests/Voltiq.Application.Tests/Features/Users/GetCurrentUserQueryHandlerTests.cs +++ b/tests/Voltiq.Application.Tests/Features/Users/GetCurrentUserQueryHandlerTests.cs @@ -3,7 +3,7 @@ using Shouldly; using Voltiq.Application.Features.Users.Queries.GetCurrentUser; using Voltiq.Domain.Entities; -using Voltiq.Domain.Interfaces.Repositories; +using Voltiq.Domain.Interfaces.Repositories.User; using Voltiq.Domain.ValueObjects; using Voltiq.Exceptions.Resources; @@ -11,7 +11,7 @@ namespace Voltiq.Application.Tests.Features.Users; public class GetCurrentUserQueryHandlerTests { - private readonly Mock> _userRepoMock = new(); + private readonly Mock _userRepoMock = new(); private GetCurrentUserQueryHandler CreateHandler() => new(_userRepoMock.Object); diff --git a/tests/Voltiq.Application.Tests/Features/Users/RegisterUserCommandHandlerTests.cs b/tests/Voltiq.Application.Tests/Features/Users/RegisterUserCommandHandlerTests.cs index e34ba76..5e3a892 100644 --- a/tests/Voltiq.Application.Tests/Features/Users/RegisterUserCommandHandlerTests.cs +++ b/tests/Voltiq.Application.Tests/Features/Users/RegisterUserCommandHandlerTests.cs @@ -5,7 +5,7 @@ using Voltiq.Application.Features.Users.Commands.RegisterUser; using Voltiq.Domain.Entities; using Voltiq.Domain.Interfaces; -using Voltiq.Domain.Interfaces.Repositories; +using Voltiq.Domain.Interfaces.Repositories.RefreshToken; using Voltiq.Domain.Interfaces.Repositories.User; using Voltiq.Domain.ValueObjects; using Voltiq.Exceptions.Resources; @@ -14,14 +14,15 @@ namespace Voltiq.Application.Tests.Features.Users; public class RegisterUserCommandHandlerTests { - private readonly Mock _userRepoMock = new(); + private readonly Mock _userReadRepoMock = new(); + private readonly Mock _userWriteRepoMock = new(); private readonly Mock _unitOfWorkMock = new(); private readonly Mock _passwordHasherMock = new(); private readonly Mock _tokenServiceMock = new(); - private readonly Mock _refreshTokenRepoMock = new(); + private readonly Mock _refreshTokenRepoMock = new(); private RegisterUserCommandHandler CreateHandler() => - new(_userRepoMock.Object, _unitOfWorkMock.Object, _passwordHasherMock.Object, _tokenServiceMock.Object, _refreshTokenRepoMock.Object); + new(_userReadRepoMock.Object, _userWriteRepoMock.Object, _unitOfWorkMock.Object, _passwordHasherMock.Object, _tokenServiceMock.Object, _refreshTokenRepoMock.Object); private static RegisterUserCommand ValidCommand() => new("João Silva", "joao@example.com", "529.982.247-25", "S3cur3P@ssw0rd!"); @@ -29,7 +30,7 @@ private static RegisterUserCommand ValidCommand() => [Fact] public async Task Handle_WithValidCommand_ShouldReturnSuccessWithUserIdAndTokens() { - _userRepoMock + _userReadRepoMock .Setup(r => r.ExistsUserAsync(It.IsAny(), It.IsAny(), It.IsAny())) .ReturnsAsync(false); @@ -52,7 +53,7 @@ public async Task Handle_WithValidCommand_ShouldReturnSuccessWithUserIdAndTokens result.Value.Id.ShouldNotBe(Guid.Empty); result.Value.AccessToken.ShouldBe("jwt.token.here"); result.Value.RefreshToken.ShouldBe("refresh.token.here"); - _userRepoMock.Verify(r => r.AddAsync(It.IsAny(), It.IsAny()), Times.Once); + _userWriteRepoMock.Verify(r => r.AddAsync(It.IsAny(), It.IsAny()), Times.Once); _refreshTokenRepoMock.Verify(r => r.AddAsync(It.IsAny(), It.IsAny()), Times.Once); _unitOfWorkMock.Verify(u => u.SaveChangesAsync(It.IsAny()), Times.Once); _tokenServiceMock.Verify( @@ -64,7 +65,7 @@ public async Task Handle_WithValidCommand_ShouldReturnSuccessWithUserIdAndTokens [Fact] public async Task Handle_WhenUserAlreadyExists_ShouldReturnConflictError() { - _userRepoMock + _userReadRepoMock .Setup(r => r.ExistsUserAsync(It.IsAny(), It.IsAny(), It.IsAny())) .ReturnsAsync(true); @@ -76,7 +77,7 @@ public async Task Handle_WhenUserAlreadyExists_ShouldReturnConflictError() result.IsError.ShouldBeTrue(); result.FirstError.Type.ShouldBe(ErrorType.Conflict); result.FirstError.Description.ShouldBe(ResourceErrorMessages.USUARIO_EMAIL_JA_CADASTRADO); - _userRepoMock.Verify(r => r.AddAsync(It.IsAny(), It.IsAny()), Times.Never); + _userWriteRepoMock.Verify(r => r.AddAsync(It.IsAny(), It.IsAny()), Times.Never); _refreshTokenRepoMock.Verify(r => r.AddAsync(It.IsAny(), It.IsAny()), Times.Never); _tokenServiceMock.Verify( t => t.GenerateAccessToken(It.IsAny(), It.IsAny(), It.IsAny>()), From 37cf47ad3567d706838e4c8651b2f4c1870d16b0 Mon Sep 17 00:00:00 2001 From: Evandro Costa Date: Tue, 31 Mar 2026 16:53:23 -0300 Subject: [PATCH 24/25] test(infrastructure): replace Repository helper with UserRepository in integration tests --- .../Persistence/BudgetRepositoryTests.cs | 16 ++++++++++------ .../Persistence/ClientRepositoryTests.cs | 14 +++++++++----- .../Persistence/MaterialRepositoryTests.cs | 12 ++++++++---- .../Persistence/SoftDeleteTests.cs | 12 ++++++++---- .../Persistence/UserRepositoryTests.cs | 10 ++++------ 5 files changed, 39 insertions(+), 25 deletions(-) diff --git a/tests/Voltiq.Infrastructure.Tests/Persistence/BudgetRepositoryTests.cs b/tests/Voltiq.Infrastructure.Tests/Persistence/BudgetRepositoryTests.cs index 2c48298..9c1220a 100644 --- a/tests/Voltiq.Infrastructure.Tests/Persistence/BudgetRepositoryTests.cs +++ b/tests/Voltiq.Infrastructure.Tests/Persistence/BudgetRepositoryTests.cs @@ -7,8 +7,10 @@ using Voltiq.Domain.ValueObjects; using Voltiq.Infrastructure.Persistence; using Voltiq.Infrastructure.Persistence.Repositories; +using Voltiq.Domain.Interfaces.Repositories.Budget; using Voltiq.Infrastructure.Persistence.Repositories.Budget; using Voltiq.Infrastructure.Persistence.Repositories.Client; +using Voltiq.Infrastructure.Persistence.Repositories.User; namespace Voltiq.Infrastructure.Tests.Persistence; @@ -18,10 +20,11 @@ public class BudgetRepositoryTests(PostgreSqlContainerFixture fixture) private static readonly Guid UserId = Guid.NewGuid(); private BudgetRepository _budgetRepository = null!; + private IBudgetReadOnlyRepository _budgetReadOnly = null!; private ClientRepository _clientRepository = null!; private ApplicationDbContext _dbContext = null!; private UnitOfWork _unitOfWork = null!; - private Repository _userRepository = null!; + private UserRepository _userRepository = null!; public async ValueTask InitializeAsync() { @@ -30,9 +33,10 @@ public async ValueTask InitializeAsync() await _dbContext.Database.MigrateAsync(); await DatabaseHelper.CleanAsync(_dbContext); - _userRepository = new Repository(_dbContext); + _userRepository = new UserRepository(_dbContext); _clientRepository = new ClientRepository(_dbContext); _budgetRepository = new BudgetRepository(_dbContext); + _budgetReadOnly = _budgetRepository; _unitOfWork = new UnitOfWork(_dbContext); } @@ -133,7 +137,7 @@ public async Task GetByIdAndUserIdAsync_ShouldReturnBudget_WhenBelongsToUser() await _budgetRepository.AddAsync(budget, TestContext.Current.CancellationToken); await _unitOfWork.SaveChangesAsync(TestContext.Current.CancellationToken); - var found = await _budgetRepository.GetByIdAndUserIdAsync(budget.Id, user.Id, + var found = await _budgetReadOnly.GetByIdAndUserIdAsync(budget.Id, user.Id, TestContext.Current.CancellationToken); found.ShouldNotBeNull(); @@ -152,7 +156,7 @@ public async Task GetByIdAndUserIdAsync_ShouldReturnNull_WhenBudgetBelongsToAnot await _budgetRepository.AddAsync(budget, TestContext.Current.CancellationToken); await _unitOfWork.SaveChangesAsync(TestContext.Current.CancellationToken); - var found = await _budgetRepository.GetByIdAndUserIdAsync(budget.Id, user2.Id, + var found = await _budgetReadOnly.GetByIdAndUserIdAsync(budget.Id, user2.Id, TestContext.Current.CancellationToken); found.ShouldBeNull(); @@ -170,7 +174,7 @@ public async Task GetByIdWithItemsAndUserIdAsync_ShouldReturnBudgetWithItems() await _budgetRepository.AddAsync(budget, TestContext.Current.CancellationToken); await _unitOfWork.SaveChangesAsync(TestContext.Current.CancellationToken); - var found = await _budgetRepository.GetByIdWithItemsAndUserIdAsync(budget.Id, user.Id, + var found = await _budgetReadOnly.GetByIdWithItemsAndUserIdAsync(budget.Id, user.Id, TestContext.Current.CancellationToken); found.ShouldNotBeNull(); @@ -189,7 +193,7 @@ public async Task GetByIdWithItemsAndUserIdAsync_ShouldReturnNull_WhenBelongsToA await _budgetRepository.AddAsync(budget, TestContext.Current.CancellationToken); await _unitOfWork.SaveChangesAsync(TestContext.Current.CancellationToken); - var found = await _budgetRepository.GetByIdWithItemsAndUserIdAsync(budget.Id, user2.Id, + var found = await _budgetReadOnly.GetByIdWithItemsAndUserIdAsync(budget.Id, user2.Id, TestContext.Current.CancellationToken); found.ShouldBeNull(); diff --git a/tests/Voltiq.Infrastructure.Tests/Persistence/ClientRepositoryTests.cs b/tests/Voltiq.Infrastructure.Tests/Persistence/ClientRepositoryTests.cs index fad605a..c097f6f 100644 --- a/tests/Voltiq.Infrastructure.Tests/Persistence/ClientRepositoryTests.cs +++ b/tests/Voltiq.Infrastructure.Tests/Persistence/ClientRepositoryTests.cs @@ -6,7 +6,9 @@ using Voltiq.Domain.ValueObjects; using Voltiq.Infrastructure.Persistence; using Voltiq.Infrastructure.Persistence.Repositories; +using Voltiq.Domain.Interfaces.Repositories.Client; using Voltiq.Infrastructure.Persistence.Repositories.Client; +using Voltiq.Infrastructure.Persistence.Repositories.User; namespace Voltiq.Infrastructure.Tests.Persistence; @@ -16,10 +18,11 @@ public class ClientRepositoryTests(PostgreSqlContainerFixture fixture) private static readonly Guid UserId = Guid.NewGuid(); private ClientRepository _clientRepository = null!; + private IClientReadOnlyRepository _clientReadOnly = null!; private ApplicationDbContext _dbContext = null!; private UnitOfWork _unitOfWork = null!; - private Repository _userRepository = null!; + private UserRepository _userRepository = null!; public async ValueTask InitializeAsync() { @@ -28,8 +31,9 @@ public async ValueTask InitializeAsync() await _dbContext.Database.MigrateAsync(); await DatabaseHelper.CleanAsync(_dbContext); - _userRepository = new Repository(_dbContext); + _userRepository = new UserRepository(_dbContext); _clientRepository = new ClientRepository(_dbContext); + _clientReadOnly = _clientRepository; _unitOfWork = new UnitOfWork(_dbContext); } @@ -71,7 +75,7 @@ public async Task AddAndGetById_ShouldPersistClient() await _unitOfWork.SaveChangesAsync(TestContext.Current.CancellationToken); var found = - await _clientRepository.GetByIdAsync(client.Id, TestContext.Current.CancellationToken); + await _clientReadOnly.GetByIdAndUserIdAsync(client.Id, user.Id, TestContext.Current.CancellationToken); found.ShouldNotBeNull(); found.Id.ShouldBe(client.Id); @@ -120,7 +124,7 @@ public async Task GetByIdAndUserIdAsync_ShouldReturnClient_WhenBelongsToUser() await _clientRepository.AddAsync(client, TestContext.Current.CancellationToken); await _unitOfWork.SaveChangesAsync(TestContext.Current.CancellationToken); - var found = await _clientRepository.GetByIdAndUserIdAsync(client.Id, user.Id, + var found = await _clientReadOnly.GetByIdAndUserIdAsync(client.Id, user.Id, TestContext.Current.CancellationToken); found.ShouldNotBeNull(); @@ -142,7 +146,7 @@ public async Task GetByIdAndUserIdAsync_ShouldReturnNull_WhenClientBelongsToAnot await _clientRepository.AddAsync(client, TestContext.Current.CancellationToken); await _unitOfWork.SaveChangesAsync(TestContext.Current.CancellationToken); - var found = await _clientRepository.GetByIdAndUserIdAsync(client.Id, user2.Id, + var found = await _clientReadOnly.GetByIdAndUserIdAsync(client.Id, user2.Id, TestContext.Current.CancellationToken); found.ShouldBeNull(); diff --git a/tests/Voltiq.Infrastructure.Tests/Persistence/MaterialRepositoryTests.cs b/tests/Voltiq.Infrastructure.Tests/Persistence/MaterialRepositoryTests.cs index c66efa1..00f131f 100644 --- a/tests/Voltiq.Infrastructure.Tests/Persistence/MaterialRepositoryTests.cs +++ b/tests/Voltiq.Infrastructure.Tests/Persistence/MaterialRepositoryTests.cs @@ -7,7 +7,9 @@ using Voltiq.Domain.ValueObjects; using Voltiq.Infrastructure.Persistence; using Voltiq.Infrastructure.Persistence.Repositories; +using Voltiq.Domain.Interfaces.Repositories.Material; using Voltiq.Infrastructure.Persistence.Repositories.Material; +using Voltiq.Infrastructure.Persistence.Repositories.User; namespace Voltiq.Infrastructure.Tests.Persistence; @@ -17,9 +19,10 @@ public class MaterialRepositoryTests(PostgreSqlContainerFixture fixture) private static readonly Guid UserId = Guid.NewGuid(); private MaterialRepository _materialRepository = null!; + private IMaterialReadOnlyRepository _materialReadOnly = null!; private ApplicationDbContext _dbContext = null!; private UnitOfWork _unitOfWork = null!; - private Repository _userRepository = null!; + private UserRepository _userRepository = null!; public async ValueTask InitializeAsync() { @@ -27,8 +30,9 @@ public async ValueTask InitializeAsync() await _dbContext.Database.MigrateAsync(); await DatabaseHelper.CleanAsync(_dbContext); - _userRepository = new Repository(_dbContext); + _userRepository = new UserRepository(_dbContext); _materialRepository = new MaterialRepository(_dbContext); + _materialReadOnly = _materialRepository; _unitOfWork = new UnitOfWork(_dbContext); } @@ -105,7 +109,7 @@ public async Task GetByIdAndUserIdAsync_ShouldReturnMaterial_WhenBelongsToUser() await _materialRepository.AddAsync(material, TestContext.Current.CancellationToken); await _unitOfWork.SaveChangesAsync(TestContext.Current.CancellationToken); - var found = await _materialRepository.GetByIdAndUserIdAsync(material.Id, user.Id, TestContext.Current.CancellationToken); + var found = await _materialReadOnly.GetByIdAndUserIdAsync(material.Id, user.Id, TestContext.Current.CancellationToken); found.ShouldNotBeNull(); found.Id.ShouldBe(material.Id); @@ -121,7 +125,7 @@ public async Task GetByIdAndUserIdAsync_ShouldReturnNull_WhenMaterialBelongsToAn await _materialRepository.AddAsync(material, TestContext.Current.CancellationToken); await _unitOfWork.SaveChangesAsync(TestContext.Current.CancellationToken); - var found = await _materialRepository.GetByIdAndUserIdAsync(material.Id, user2.Id, TestContext.Current.CancellationToken); + var found = await _materialReadOnly.GetByIdAndUserIdAsync(material.Id, user2.Id, TestContext.Current.CancellationToken); found.ShouldBeNull(); } diff --git a/tests/Voltiq.Infrastructure.Tests/Persistence/SoftDeleteTests.cs b/tests/Voltiq.Infrastructure.Tests/Persistence/SoftDeleteTests.cs index 127f3c9..a3d7528 100644 --- a/tests/Voltiq.Infrastructure.Tests/Persistence/SoftDeleteTests.cs +++ b/tests/Voltiq.Infrastructure.Tests/Persistence/SoftDeleteTests.cs @@ -6,7 +6,9 @@ using Voltiq.Domain.ValueObjects; using Voltiq.Infrastructure.Persistence; using Voltiq.Infrastructure.Persistence.Repositories; +using Voltiq.Domain.Interfaces.Repositories.Client; using Voltiq.Infrastructure.Persistence.Repositories.Client; +using Voltiq.Infrastructure.Persistence.Repositories.User; namespace Voltiq.Infrastructure.Tests.Persistence; @@ -16,9 +18,10 @@ public class SoftDeleteTests(PostgreSqlContainerFixture fixture) private static readonly Guid UserId = Guid.NewGuid(); private ClientRepository _clientRepository = null!; + private IClientReadOnlyRepository _clientReadOnly = null!; private ApplicationDbContext _dbContext = null!; private UnitOfWork _unitOfWork = null!; - private Repository _userRepository = null!; + private UserRepository _userRepository = null!; public async ValueTask InitializeAsync() { @@ -27,8 +30,9 @@ public async ValueTask InitializeAsync() await _dbContext.Database.MigrateAsync(); await DatabaseHelper.CleanAsync(_dbContext); - _userRepository = new Repository(_dbContext); + _userRepository = new UserRepository(_dbContext); _clientRepository = new ClientRepository(_dbContext); + _clientReadOnly = _clientRepository; _unitOfWork = new UnitOfWork(_dbContext); } @@ -100,13 +104,13 @@ public async Task Remove_ShouldSetIsDeletedAndDeletedAt() [Fact] public async Task GlobalQueryFilter_ShouldExcludeDeletedEntitiesFromNormalQueries() { - var (_, client) = await CreateUserAndClientAsync(); + var (user, client) = await CreateUserAndClientAsync(); _clientRepository.Remove(client); await _unitOfWork.SaveChangesAsync(TestContext.Current.CancellationToken); var found = - await _clientRepository.GetByIdAsync(client.Id, TestContext.Current.CancellationToken); + await _clientReadOnly.GetByIdAndUserIdAsync(client.Id, user.Id, TestContext.Current.CancellationToken); found.ShouldBeNull(); } diff --git a/tests/Voltiq.Infrastructure.Tests/Persistence/UserRepositoryTests.cs b/tests/Voltiq.Infrastructure.Tests/Persistence/UserRepositoryTests.cs index 691d9ea..fe78946 100644 --- a/tests/Voltiq.Infrastructure.Tests/Persistence/UserRepositoryTests.cs +++ b/tests/Voltiq.Infrastructure.Tests/Persistence/UserRepositoryTests.cs @@ -14,7 +14,6 @@ public class UserRepositoryTests(PostgreSqlContainerFixture fixture) : IClassFixture, IAsyncLifetime { private ApplicationDbContext _dbContext = null!; - private Repository _repository = null!; private UnitOfWork _unitOfWork = null!; private UserRepository _userRepository = null!; @@ -25,7 +24,6 @@ public async ValueTask InitializeAsync() await _dbContext.Database.MigrateAsync(); await DatabaseHelper.CleanAsync(_dbContext); - _repository = new Repository(_dbContext); _userRepository = new UserRepository(_dbContext); _unitOfWork = new UnitOfWork(_dbContext); } @@ -42,10 +40,10 @@ public async Task AddAndGetById_ShouldPersistUser() var document = Document.Create("529.982.247-25").Value; var user = User.Register("João Silva", email, document, "$argon2id$hash"); - await _repository.AddAsync(user, TestContext.Current.CancellationToken); + await _userRepository.AddAsync(user, TestContext.Current.CancellationToken); await _unitOfWork.SaveChangesAsync(TestContext.Current.CancellationToken); - var found = await _repository.GetByIdAsync(user.Id, TestContext.Current.CancellationToken); + var found = await _userRepository.GetByIdAsync(user.Id, TestContext.Current.CancellationToken); found.ShouldNotBeNull(); found.Id.ShouldBe(user.Id); @@ -60,7 +58,7 @@ public async Task ExistsUserAsync_ShouldReturnTrue_WhenEmailOrDocumentExists() var document = Document.Create("11222333000181").Value; var user = User.Register("Maria Santos", email, document, "$argon2id$hash"); - await _repository.AddAsync(user, TestContext.Current.CancellationToken); + await _userRepository.AddAsync(user, TestContext.Current.CancellationToken); await _unitOfWork.SaveChangesAsync(TestContext.Current.CancellationToken); var exists = @@ -77,7 +75,7 @@ public async Task GetByEmailAsync_ShouldReturnUser_WhenEmailExists() var document = Document.Create("153.509.460-56").Value; var user = User.Register("Carlos Souza", email, document, "$argon2id$hash"); - await _repository.AddAsync(user, TestContext.Current.CancellationToken); + await _userRepository.AddAsync(user, TestContext.Current.CancellationToken); await _unitOfWork.SaveChangesAsync(TestContext.Current.CancellationToken); var found = From 93e848f29d5177027e953e0ae60e1a5905bf4675 Mon Sep 17 00:00:00 2001 From: Evandro Costa Date: Tue, 31 Mar 2026 17:19:53 -0300 Subject: [PATCH 25/25] style: fix format erros with dotnet format --- .../Commands/RegisterClientCommandHandlerTests.cs | 3 ++- .../Clients/Commands/UpdateClientCommandHandlerTests.cs | 9 ++++++--- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/tests/Voltiq.Application.Tests/Features/Clients/Commands/RegisterClientCommandHandlerTests.cs b/tests/Voltiq.Application.Tests/Features/Clients/Commands/RegisterClientCommandHandlerTests.cs index 550b848..4159790 100644 --- a/tests/Voltiq.Application.Tests/Features/Clients/Commands/RegisterClientCommandHandlerTests.cs +++ b/tests/Voltiq.Application.Tests/Features/Clients/Commands/RegisterClientCommandHandlerTests.cs @@ -26,7 +26,8 @@ private RegisterClientCommandHandler CreateHandler() private RegisterClientCommand ValidCommand() => new("João Silva", "(11) 99999-9999", "joao@example.com", "Rua das Flores", "123", - "São Paulo", "SP", "01310-100") { UserId = _userId }; + "São Paulo", "SP", "01310-100") + { UserId = _userId }; [Fact] public async Task Handle_WithValidCommand_ShouldRegisterClientAndReturnResponse() diff --git a/tests/Voltiq.Application.Tests/Features/Clients/Commands/UpdateClientCommandHandlerTests.cs b/tests/Voltiq.Application.Tests/Features/Clients/Commands/UpdateClientCommandHandlerTests.cs index fe035f4..4587bc7 100644 --- a/tests/Voltiq.Application.Tests/Features/Clients/Commands/UpdateClientCommandHandlerTests.cs +++ b/tests/Voltiq.Application.Tests/Features/Clients/Commands/UpdateClientCommandHandlerTests.cs @@ -44,7 +44,8 @@ public async Task Handle_WithValidCommand_ShouldUpdateClientAndReturnUpdated() var command = new UpdateClientCommand( client.Id, "Maria Souza", "(11) 88888-8888", "maria@example.com", - "Av. Paulista", "1000", "São Paulo", "SP", "01311-100") { UserId = _userId }; + "Av. Paulista", "1000", "São Paulo", "SP", "01311-100") + { UserId = _userId }; var handler = CreateHandler(); var result = await handler.Handle(command, CancellationToken.None); @@ -67,7 +68,8 @@ public async Task Handle_WhenEmailAlreadyExistsForAnotherClient_ShouldReturnConf var command = new UpdateClientCommand( client.Id, "Maria Souza", "(11) 88888-8888", "outro@example.com", - "Av. Paulista", "1000", "São Paulo", "SP", "01311-100") { UserId = _userId }; + "Av. Paulista", "1000", "São Paulo", "SP", "01311-100") + { UserId = _userId }; var handler = CreateHandler(); var result = await handler.Handle(command, CancellationToken.None); @@ -88,7 +90,8 @@ public async Task Handle_WhenClientNotFound_ShouldReturnNotFoundError() var command = new UpdateClientCommand( Guid.NewGuid(), "Maria Souza", "(11) 88888-8888", "maria@example.com", - "Av. Paulista", "1000", "São Paulo", "SP", "01311-100") { UserId = _userId }; + "Av. Paulista", "1000", "São Paulo", "SP", "01311-100") + { UserId = _userId }; var handler = CreateHandler(); var result = await handler.Handle(command, CancellationToken.None);