From 1145a273d6f843494cdd2dc70f1ec485486566b9 Mon Sep 17 00:00:00 2001 From: xilapa Date: Thu, 2 Nov 2023 16:44:22 -0300 Subject: [PATCH] Remove validation filter --- .../CreateAlert/CreateAlertCommand.cs | 17 ++--- .../DeleteAlert/DeleteAlertCommand.cs | 16 ++--- .../UpdateAlert/UpdateAlertCommmand.cs | 25 ++++---- .../GetUserAlerts/GetUserAlertsQuery.cs | 14 ++--- .../Queries/SearchAlerts/SearchAlertQuery.cs | 37 ++++++----- src/Application/Common/BaseHandler.cs | 31 ++++++++++ .../Common/Commands/CommandResult.cs | 39 ------------ .../Common/Constants/ApplicationErrors.cs | 1 - .../Queries/{QueryResult.cs => DbQuery.cs} | 4 +- src/Application/Common/Queries/IQueries.cs | 10 +-- src/Application/Common/Results/Result.cs | 62 +++++++++++++++++++ src/Application/DependencyInjection.cs | 9 +++ src/Application/Interfaces/IDapperContext.cs | 1 - .../ConfirmEmail/ConfirmEmailCommand.cs | 19 +++--- .../ReactivateAccountCommand.cs | 19 +++--- .../RegisterUser/RegisterUserCommand.cs | 16 ++--- .../SendReactivateAccountEmailCommand.cs | 21 ++++--- .../Commands/UpdateUser/UpdateUserCommand.cs | 28 +++++---- src/Domain/Common/Errors/Error.cs | 15 +++++ src/Domain/Common/Errors/ErrorType.cs | 6 ++ src/Infra/Cache/RedisCache.cs | 3 - .../PostgresConnectionHealthCheck.cs | 3 - src/Infra/HealthChecks/RabbitMqHealthCheck.cs | 3 - src/Infra/Http/HttpRetryPolicies.cs | 2 - .../Configuration/UriValueConverter.cs | 3 +- src/Infra/Persistence/DatabaseMigrator.cs | 3 - src/Infra/Persistence/Queries.cs | 20 +++--- src/WebAPI/Controllers/AlertController.cs | 44 +++++++------ src/WebAPI/Controllers/UserController.cs | 53 +++++++++------- .../Extensions/CommandResultExtensions.cs | 42 ------------- src/WebAPI/Extensions/ErrorExtensions.cs | 16 +++++ src/WebAPI/Filters/CommandValidationFilter.cs | 39 ------------ .../ProcessNotificationTests.cs | 1 - .../BaseTestFixtureOptions.cs | 1 - .../Commands/ConfirmEmailCommandTests.cs | 22 +++---- .../Commands/DeleteAlertCommandTests.cs | 7 +-- .../Commands/GetUserAlertsCommandTests.cs | 3 +- .../Commands/ReactivateAccountCommandTests.cs | 24 +++---- .../SendReactivateAccountEmailCommandTests.cs | 13 +++- .../Commands/UpdateAlertCommandTests.cs | 24 ++++--- .../Commands/UpdateUserCommandTests.cs | 21 ++++--- test/UnitTests/Services/AuthServiceTests.cs | 1 - 42 files changed, 392 insertions(+), 346 deletions(-) create mode 100644 src/Application/Common/BaseHandler.cs delete mode 100644 src/Application/Common/Commands/CommandResult.cs rename src/Application/Common/Queries/{QueryResult.cs => DbQuery.cs} (66%) create mode 100644 src/Application/Common/Results/Result.cs create mode 100644 src/Domain/Common/Errors/Error.cs create mode 100644 src/Domain/Common/Errors/ErrorType.cs delete mode 100644 src/WebAPI/Extensions/CommandResultExtensions.cs create mode 100644 src/WebAPI/Extensions/ErrorExtensions.cs delete mode 100644 src/WebAPI/Filters/CommandValidationFilter.cs diff --git a/src/Application/Alerts/Commands/CreateAlert/CreateAlertCommand.cs b/src/Application/Alerts/Commands/CreateAlert/CreateAlertCommand.cs index 414c04bb..57a7d88f 100644 --- a/src/Application/Alerts/Commands/CreateAlert/CreateAlertCommand.cs +++ b/src/Application/Alerts/Commands/CreateAlert/CreateAlertCommand.cs @@ -1,4 +1,6 @@ -using Mediator; +using FluentValidation; +using SiteWatcher.Application.Common.Command; +using SiteWatcher.Application.Common.Results; using SiteWatcher.Application.Interfaces; using SiteWatcher.Common.Services; using SiteWatcher.Domain.Alerts; @@ -8,7 +10,7 @@ namespace SiteWatcher.Application.Alerts.Commands.CreateAlert; -public class CreateAlertCommand : ICommand +public sealed class CreateAlertCommand { public string Name { get; set; } = null!; public Frequencies Frequency { get; set; } @@ -34,24 +36,25 @@ public static implicit operator CreateAlertInput(CreateAlertCommand command) => command.RegexPattern); } -public class CreateAlertCommandHandler : ICommandHandler +public sealed class CreateAlertCommandHandler : BaseHandler> { private readonly ISession _session; private readonly ISiteWatcherContext _context; private readonly IIdHasher _idHasher; - public CreateAlertCommandHandler(ISession session, ISiteWatcherContext context, IIdHasher idHasher) + public CreateAlertCommandHandler(ISession session, ISiteWatcherContext context, IIdHasher idHasher, + IValidator validator) : base(validator) { _session = session; _context = context; _idHasher = idHasher; } - public async ValueTask Handle(CreateAlertCommand request, CancellationToken cancellationToken) + protected override async Task> HandleCommand(CreateAlertCommand command, CancellationToken ct) { - var alert = AlertFactory.Create(request, _session.UserId!.Value, _session.Now); + var alert = AlertFactory.Create(command, _session.UserId!.Value, _session.Now); _context.Alerts.Add(alert); - await _context.SaveChangesAsync(cancellationToken); + await _context.SaveChangesAsync(ct); return DetailedAlertView.FromAlert(alert, _idHasher); } } \ No newline at end of file diff --git a/src/Application/Alerts/Commands/DeleteAlert/DeleteAlertCommand.cs b/src/Application/Alerts/Commands/DeleteAlert/DeleteAlertCommand.cs index 3578b3ca..b3c95a00 100644 --- a/src/Application/Alerts/Commands/DeleteAlert/DeleteAlertCommand.cs +++ b/src/Application/Alerts/Commands/DeleteAlert/DeleteAlertCommand.cs @@ -1,21 +1,23 @@ using Mediator; using Microsoft.EntityFrameworkCore; -using SiteWatcher.Application.Common.Commands; +using SiteWatcher.Application.Common.Command; using SiteWatcher.Application.Common.Constants; +using SiteWatcher.Application.Common.Results; using SiteWatcher.Application.Interfaces; using SiteWatcher.Common.Services; using SiteWatcher.Domain.Alerts.Events; using SiteWatcher.Domain.Authentication; +using SiteWatcher.Domain.Common.Errors; using SiteWatcher.Domain.Common.ValueObjects; namespace SiteWatcher.Application.Alerts.Commands.DeleteAlert; -public class DeleteAlertCommand : ICommand +public class DeleteAlertCommand { public string? AlertId { get; set; } } -public class DeleteAlertCommandHandler : ICommandHandler +public class DeleteAlertCommandHandler : IApplicationHandler { private readonly ISiteWatcherContext _context; private readonly IIdHasher _idHasher; @@ -31,7 +33,7 @@ public DeleteAlertCommandHandler(ISiteWatcherContext context, IIdHasher idHasher _mediator = mediator; } - public async ValueTask Handle(DeleteAlertCommand request, CancellationToken cancellationToken) + public async Task Handle(DeleteAlertCommand request, CancellationToken cancellationToken) { if(request.AlertId == null) return ReturnError(); @@ -49,9 +51,9 @@ public async ValueTask Handle(DeleteAlertCommand request, Cancell if (deleted != 0) await _mediator.Publish(new AlertsChangedEvent(_session.UserId!.Value), CancellationToken.None); - return deleted != 0 ? CommandResult.Empty() : ReturnError(); + return deleted != 0 ? Result.Empty : ReturnError(); } - private static CommandResult ReturnError() => - CommandResult.FromError(ApplicationErrors.ValueIsInvalid(nameof(DeleteAlertCommand.AlertId))); + private static Error ReturnError() => + Error.Validation(ApplicationErrors.ValueIsInvalid(nameof(DeleteAlertCommand.AlertId))); } \ No newline at end of file diff --git a/src/Application/Alerts/Commands/UpdateAlert/UpdateAlertCommmand.cs b/src/Application/Alerts/Commands/UpdateAlert/UpdateAlertCommmand.cs index 6564d258..4fcde7a0 100644 --- a/src/Application/Alerts/Commands/UpdateAlert/UpdateAlertCommmand.cs +++ b/src/Application/Alerts/Commands/UpdateAlert/UpdateAlertCommmand.cs @@ -1,18 +1,20 @@ -using Mediator; -using SiteWatcher.Application.Common.Commands; +using FluentValidation; +using SiteWatcher.Application.Common.Command; using SiteWatcher.Application.Common.Constants; using SiteWatcher.Application.Common.Extensions; +using SiteWatcher.Application.Common.Results; using SiteWatcher.Application.Interfaces; using SiteWatcher.Common.Services; using SiteWatcher.Domain.Alerts.DTOs; using SiteWatcher.Domain.Alerts.Enums; using SiteWatcher.Domain.Authentication; using SiteWatcher.Domain.Common.DTOs; +using SiteWatcher.Domain.Common.Errors; using SiteWatcher.Domain.Common.ValueObjects; namespace SiteWatcher.Application.Alerts.Commands.UpdateAlert; -public class UpdateAlertCommmand : ICommand +public class UpdateAlertCommmand { public string AlertId { get; set; } = null!; public UpdateInfo? Name { get; set; } @@ -45,32 +47,33 @@ public UpdateAlertInput ToUpdateAlertInput(IIdHasher idHasher) } } -public class UpdateAlertCommandHandler : ICommandHandler +public class UpdateAlertCommandHandler : BaseHandler> { private readonly IIdHasher _idHasher; private readonly ISiteWatcherContext _context; private readonly ISession _session; - public UpdateAlertCommandHandler(IIdHasher idHasher, ISiteWatcherContext context, ISession session) + public UpdateAlertCommandHandler(IIdHasher idHasher, ISiteWatcherContext context, ISession session, + IValidator validator) : base(validator) { _idHasher = idHasher; _context = context; _session = session; } - public async ValueTask Handle(UpdateAlertCommmand request, CancellationToken cancellationToken) + protected override async Task> HandleCommand(UpdateAlertCommmand command, CancellationToken ct) { - var updateInfo = request.ToUpdateAlertInput(_idHasher); + var updateInfo = command.ToUpdateAlertInput(_idHasher); if (AlertId.Empty.Equals(updateInfo.AlertId) || updateInfo.AlertId.Value == 0) - return CommandResult.FromError(ApplicationErrors.ValueIsInvalid(nameof(UpdateAlertCommmand.AlertId))); + return Error.Validation(ApplicationErrors.ValueIsInvalid(nameof(UpdateAlertCommmand.AlertId))); - var alert = await _context.GetAlertForUpdateAsync(updateInfo.AlertId, _session.UserId!.Value, cancellationToken); - if (alert is null) return CommandResult.FromError(ApplicationErrors.ALERT_DO_NOT_EXIST); + var alert = await _context.GetAlertForUpdateAsync(updateInfo.AlertId, _session.UserId!.Value, ct); + if (alert is null) return Error.Validation(ApplicationErrors.ALERT_DO_NOT_EXIST); alert.Update(updateInfo, _session.Now); await _context.SaveChangesAsync(CancellationToken.None); - return CommandResult.FromValue(DetailedAlertView.FromAlert(alert, _idHasher)); + return DetailedAlertView.FromAlert(alert, _idHasher); } } \ No newline at end of file diff --git a/src/Application/Alerts/Queries/GetUserAlerts/GetUserAlertsQuery.cs b/src/Application/Alerts/Queries/GetUserAlerts/GetUserAlertsQuery.cs index d0af73b9..b504f4de 100644 --- a/src/Application/Alerts/Queries/GetUserAlerts/GetUserAlertsQuery.cs +++ b/src/Application/Alerts/Queries/GetUserAlerts/GetUserAlertsQuery.cs @@ -1,8 +1,8 @@ using Application.Alerts.Dtos; using Dapper; -using Mediator; -using SiteWatcher.Application.Common.Commands; +using SiteWatcher.Application.Common.Command; using SiteWatcher.Application.Common.Queries; +using SiteWatcher.Application.Common.Results; using SiteWatcher.Application.Interfaces; using SiteWatcher.Common.Services; using SiteWatcher.Domain.Alerts.DTOs; @@ -13,7 +13,7 @@ namespace SiteWatcher.Application.Alerts.Commands.GetUserAlerts; -public class GetUserAlertsQuery : IQuery, ICacheable +public class GetUserAlertsQuery : ICacheable { public string? LastAlertId { get; set; } public int Take { get; set; } = 10; @@ -27,7 +27,7 @@ public string GetKey(ISession session) => public TimeSpan Expiration => TimeSpan.FromMinutes(60); } -public class GetUserAlertsQueryHandler : IQueryHandler +public class GetUserAlertsQueryHandler : IApplicationHandler { private readonly IIdHasher _idHasher; private readonly ISession _session; @@ -42,10 +42,10 @@ public GetUserAlertsQueryHandler(IIdHasher idHasher, ISession session, IDapperCo _queries = queries; } - public async ValueTask Handle(GetUserAlertsQuery request, CancellationToken cancellationToken) + public async Task>> Handle(GetUserAlertsQuery request, CancellationToken cancellationToken) { if (request.Take == 0) - return CommandResult.Empty(); + return Result>.Empty; var take = request.Take > 50 ? 50 : request.Take; var lastAlertId = _idHasher.DecodeId(request.LastAlertId!); @@ -70,6 +70,6 @@ public async ValueTask Handle(GetUserAlertsQuery request, Cancell return result; }); - return CommandResult.FromValue(paginatedListAlerts); + return paginatedListAlerts; } } \ No newline at end of file diff --git a/src/Application/Alerts/Queries/SearchAlerts/SearchAlertQuery.cs b/src/Application/Alerts/Queries/SearchAlerts/SearchAlertQuery.cs index 4c6170e6..b1b7a6e6 100644 --- a/src/Application/Alerts/Queries/SearchAlerts/SearchAlertQuery.cs +++ b/src/Application/Alerts/Queries/SearchAlerts/SearchAlertQuery.cs @@ -1,7 +1,9 @@ using Application.Alerts.Dtos; using Dapper; -using Mediator; +using FluentValidation; +using SiteWatcher.Application.Common.Command; using SiteWatcher.Application.Common.Queries; +using SiteWatcher.Application.Common.Results; using SiteWatcher.Application.Interfaces; using SiteWatcher.Common.Services; using SiteWatcher.Domain.Alerts.DTOs; @@ -11,23 +13,25 @@ namespace SiteWatcher.Application.Alerts.Commands.SearchAlerts; -public class SearchAlertQuery : IQuery>, ICacheable +public class SearchAlertQuery : ICacheable { public string Term { get; set; } = null!; public TimeSpan Expiration => TimeSpan.FromMinutes(10); public string HashFieldName => $"Term:{Term}"; + public string GetKey(ISession session) => CacheKeys.UserAlertSearch(session.UserId!.Value); } -public class SearchAlertQueryHandler : IQueryHandler> +public class SearchAlertQueryHandler : BaseHandler>> { private readonly IDapperContext _context; private readonly IQueries _queries; private readonly ISession _session; private readonly IIdHasher _idHasher; - public SearchAlertQueryHandler(IDapperContext context, IQueries queries, ISession session, IIdHasher idHasher) + public SearchAlertQueryHandler(IDapperContext context, IQueries queries, ISession session, IIdHasher idHasher, + IValidator validator) : base(validator) { _context = context; _queries = queries; @@ -35,9 +39,10 @@ public SearchAlertQueryHandler(IDapperContext context, IQueries queries, ISessio _idHasher = idHasher; } - public async ValueTask> Handle(SearchAlertQuery request, CancellationToken cancellationToken) + protected override async Task>> HandleCommand(SearchAlertQuery command, + CancellationToken ct) { - var searchTerms = request + var searchTerms = command .Term.Split(' ') .Where(t => !string.IsNullOrEmpty(t)) .Select(t => t.ToLowerCaseWithoutDiacritics()).ToArray(); @@ -46,13 +51,17 @@ public async ValueTask> Handle(SearchAlertQuery req var simpleAlertViewDtos = await _context .UsingConnectionAsync(conn => - { - var cmd = new CommandDefinition( - query.Sql, - query.Parameters, - cancellationToken: cancellationToken); - return conn.QueryAsync(cmd); - }); - return simpleAlertViewDtos.Select(dto => dto.ToSimpleAlertView(_idHasher)); + { + var cmd = new CommandDefinition( + query.Sql, + query.Parameters, + cancellationToken: ct); + return conn.QueryAsync(cmd); + }); + + var simpleAlertViews = simpleAlertViewDtos + .Select(dto => dto.ToSimpleAlertView(_idHasher)); + + return new Result>(simpleAlertViews); } } \ No newline at end of file diff --git a/src/Application/Common/BaseHandler.cs b/src/Application/Common/BaseHandler.cs new file mode 100644 index 00000000..80c3c548 --- /dev/null +++ b/src/Application/Common/BaseHandler.cs @@ -0,0 +1,31 @@ +using FluentValidation; +using SiteWatcher.Application.Common.Results; +using SiteWatcher.Domain.Common.Errors; + +namespace SiteWatcher.Application.Common.Command; + +public abstract class BaseHandler : IApplicationHandler where R : IResult, new() +{ + private readonly IValidator _validator; + + protected BaseHandler(IValidator validator) + { + _validator = validator; + } + + public async Task Handle(T command, CancellationToken ct) + { + var res = _validator.Validate(command); + if (res.IsValid) + return await HandleCommand(command, ct); + + var errors = res.Errors.Select(err => err.ErrorMessage).ToArray(); + var errorResult = new R(); + errorResult.SetError(Error.Validation(errors)); + return errorResult; + } + + protected abstract Task HandleCommand(T command, CancellationToken ct); +} + +public interface IApplicationHandler{} \ No newline at end of file diff --git a/src/Application/Common/Commands/CommandResult.cs b/src/Application/Common/Commands/CommandResult.cs deleted file mode 100644 index 0d3dcaab..00000000 --- a/src/Application/Common/Commands/CommandResult.cs +++ /dev/null @@ -1,39 +0,0 @@ -namespace SiteWatcher.Application.Common.Commands; - -public abstract class CommandResult -{ - public static ValueResult FromValue(T value) => new (value); - public static ErrorResult FromError(string value) => new (value); - public static ErrorResult FromErrors(IEnumerable value) => new (value); - - private static readonly EmptyResult _empty = new (); - public static CommandResult Empty() => _empty; -} - -public sealed class ValueResult : CommandResult -{ - public ValueResult(T value) - { - Value = value; - } - - public T Value { get; init; } -} - -public sealed class EmptyResult : CommandResult -{ } - -public sealed class ErrorResult: CommandResult -{ - public ErrorResult(string error) - { - Errors = new [] {error}; - } - - public ErrorResult(IEnumerable errors) - { - Errors = errors; - } - - public IEnumerable Errors { get; } -} \ No newline at end of file diff --git a/src/Application/Common/Constants/ApplicationErrors.cs b/src/Application/Common/Constants/ApplicationErrors.cs index f4f1a1a7..355863a5 100644 --- a/src/Application/Common/Constants/ApplicationErrors.cs +++ b/src/Application/Common/Constants/ApplicationErrors.cs @@ -2,7 +2,6 @@ namespace SiteWatcher.Application.Common.Constants; public static class ApplicationErrors { - // TODO: Translate error messages at the fronteend public static readonly string INTERNAL_ERROR = "An error has occurred."; public static readonly string NAME_MUST_HAVE_ONLY_LETTERS = "Username must only have letters."; public static readonly string USER_DO_NOT_EXIST = "User does not exist."; diff --git a/src/Application/Common/Queries/QueryResult.cs b/src/Application/Common/Queries/DbQuery.cs similarity index 66% rename from src/Application/Common/Queries/QueryResult.cs rename to src/Application/Common/Queries/DbQuery.cs index 6806f920..fa9ee7f7 100644 --- a/src/Application/Common/Queries/QueryResult.cs +++ b/src/Application/Common/Queries/DbQuery.cs @@ -1,8 +1,8 @@ namespace SiteWatcher.Application.Common.Queries; -public sealed class QueryResult +public sealed class DbQuery { - public QueryResult(string sql, Dictionary parameters) + public DbQuery(string sql, Dictionary parameters) { Sql = sql; Parameters = parameters; diff --git a/src/Application/Common/Queries/IQueries.cs b/src/Application/Common/Queries/IQueries.cs index c0da4e97..cfa76b75 100644 --- a/src/Application/Common/Queries/IQueries.cs +++ b/src/Application/Common/Queries/IQueries.cs @@ -3,9 +3,9 @@ namespace SiteWatcher.Application.Common.Queries; public interface IQueries { - QueryResult GetUserByGoogleId(string googleId); - QueryResult GetUserById(UserId userId); - QueryResult GetSimpleAlertViewListByUserId(UserId userId, AlertId? lastAlertId, int take); - QueryResult GetAlertDetails(UserId userId, AlertId alertId); - QueryResult SearchSimpleAlerts(UserId userId, string[] searchTerms, int take); + DbQuery GetUserByGoogleId(string googleId); + DbQuery GetUserById(UserId userId); + DbQuery GetSimpleAlertViewListByUserId(UserId userId, AlertId? lastAlertId, int take); + DbQuery GetAlertDetails(UserId userId, AlertId alertId); + DbQuery SearchSimpleAlerts(UserId userId, string[] searchTerms, int take); } \ No newline at end of file diff --git a/src/Application/Common/Results/Result.cs b/src/Application/Common/Results/Result.cs new file mode 100644 index 00000000..f74005dc --- /dev/null +++ b/src/Application/Common/Results/Result.cs @@ -0,0 +1,62 @@ +using SiteWatcher.Domain.Common.Errors; + +namespace SiteWatcher.Application.Common.Results; + +public interface IResult +{ + Error? Error { get; } + void SetError(Error error); +} + +public sealed class Result : IResult +{ + public Result() + { + } + + public Result(Error error) + { + Error = error; + } + + public Error? Error { get; private set; } + + public void SetError(Error error) + { + Error = error; + } + + public static Result Empty { get; } = new(); + + public static implicit operator Result(Error error) => new(error); +} + +public sealed class Result : IResult where T : class +{ + public Result() + { + } + + public Result(T value) + { + Value = value; + } + + public Result(Error error) + { + Error = error; + } + + public Error? Error { get; private set; } + public T? Value { get; } + + public void SetError(Error error) + { + Error = error; + } + + public static Result Empty { get; } = new(); + + public static implicit operator Result(T value) => new(value); + public static implicit operator Result(Error error) => new(error); +} \ No newline at end of file diff --git a/src/Application/DependencyInjection.cs b/src/Application/DependencyInjection.cs index 1fdcb50f..14177633 100644 --- a/src/Application/DependencyInjection.cs +++ b/src/Application/DependencyInjection.cs @@ -1,6 +1,7 @@ using MassTransit; using Microsoft.Extensions.DependencyInjection; using Scrutor; +using SiteWatcher.Application.Common.Command; namespace SiteWatcher.Application; @@ -13,6 +14,14 @@ public static IServiceCollection AddApplication(this IServiceCollection services opts.Namespace = "SiteWatcher.Application.Mediator"; opts.ServiceLifetime = ServiceLifetime.Scoped; }); + + services.Scan(scan => + { + scan.FromAssemblyOf() + .AddClasses(c => c.AssignableTo()) + .AsSelf(); + }); + return services; } diff --git a/src/Application/Interfaces/IDapperContext.cs b/src/Application/Interfaces/IDapperContext.cs index 98403e38..e6bd7421 100644 --- a/src/Application/Interfaces/IDapperContext.cs +++ b/src/Application/Interfaces/IDapperContext.cs @@ -1,5 +1,4 @@ using System.Data; -using Dapper; namespace SiteWatcher.Application.Interfaces; diff --git a/src/Application/Users/Commands/ConfirmEmail/ConfirmEmailCommand.cs b/src/Application/Users/Commands/ConfirmEmail/ConfirmEmailCommand.cs index 4cf92307..e6a7e91c 100644 --- a/src/Application/Users/Commands/ConfirmEmail/ConfirmEmailCommand.cs +++ b/src/Application/Users/Commands/ConfirmEmail/ConfirmEmailCommand.cs @@ -1,19 +1,20 @@ -using Mediator; -using Microsoft.EntityFrameworkCore; -using SiteWatcher.Application.Common.Commands; +using Microsoft.EntityFrameworkCore; +using SiteWatcher.Application.Common.Command; using SiteWatcher.Application.Common.Constants; +using SiteWatcher.Application.Common.Results; using SiteWatcher.Application.Interfaces; using SiteWatcher.Domain.Authentication; using SiteWatcher.Domain.Authentication.Services; +using SiteWatcher.Domain.Common.Errors; namespace SiteWatcher.Application.Users.Commands.ConfirmEmail; -public class ConfirmEmailCommand : ICommand +public class ConfirmEmailCommand { public string? Token { get; set; } } -public class ConfirmEmailCommandHandler : ICommandHandler +public class ConfirmEmailCommandHandler : IApplicationHandler { private readonly IAuthService _authservice; private readonly ISiteWatcherContext _context; @@ -26,7 +27,7 @@ public ConfirmEmailCommandHandler(IAuthService authservice, ISiteWatcherContext _session = session; } - public async ValueTask Handle(ConfirmEmailCommand request, CancellationToken cancellationToken) + public async Task Handle(ConfirmEmailCommand request, CancellationToken cancellationToken) { if (request.Token == null) return ReturnError(); @@ -45,9 +46,9 @@ public async ValueTask Handle(ConfirmEmailCommand request, Cancel return ReturnError(); await _context.SaveChangesAsync(CancellationToken.None); - return CommandResult.Empty(); + return Result.Empty; } - private static CommandResult ReturnError() => - CommandResult.FromError(ApplicationErrors.ValueIsInvalid(nameof(ConfirmEmailCommand.Token))); + private static Error ReturnError() => + Error.Validation(ApplicationErrors.ValueIsInvalid(nameof(ConfirmEmailCommand.Token))); } \ No newline at end of file diff --git a/src/Application/Users/Commands/ReactivateAccount/ReactivateAccountCommand.cs b/src/Application/Users/Commands/ReactivateAccount/ReactivateAccountCommand.cs index 3090ddd4..30b395ad 100644 --- a/src/Application/Users/Commands/ReactivateAccount/ReactivateAccountCommand.cs +++ b/src/Application/Users/Commands/ReactivateAccount/ReactivateAccountCommand.cs @@ -1,19 +1,20 @@ -using Mediator; -using Microsoft.EntityFrameworkCore; -using SiteWatcher.Application.Common.Commands; +using Microsoft.EntityFrameworkCore; +using SiteWatcher.Application.Common.Command; using SiteWatcher.Application.Common.Constants; +using SiteWatcher.Application.Common.Results; using SiteWatcher.Application.Interfaces; using SiteWatcher.Domain.Authentication; using SiteWatcher.Domain.Authentication.Services; +using SiteWatcher.Domain.Common.Errors; namespace SiteWatcher.Application.Users.Commands.ReactivateAccount; -public class ReactivateAccountCommand : ICommand +public class ReactivateAccountCommand { public string? Token { get; set; } } -public class ReactivateAccountCommandHandler : ICommandHandler +public class ReactivateAccountCommandHandler : IApplicationHandler { private readonly IAuthService _authService; private readonly ISiteWatcherContext _context; @@ -26,7 +27,7 @@ public ReactivateAccountCommandHandler(IAuthService authService, ISiteWatcherCon _session = session; } - public async ValueTask Handle(ReactivateAccountCommand request, CancellationToken cancellationToken) + public async ValueTask Handle(ReactivateAccountCommand request, CancellationToken cancellationToken) { if(request.Token == null) return ReturnError(); @@ -44,9 +45,9 @@ public async ValueTask Handle(ReactivateAccountCommand request, C return ReturnError(); await _context.SaveChangesAsync(CancellationToken.None); - return CommandResult.Empty(); + return Result.Empty; } - private static CommandResult ReturnError() => - CommandResult.FromError(ApplicationErrors.ValueIsInvalid(nameof(ReactivateAccountCommand.Token))); + private static Error ReturnError() => + Error.Validation(ApplicationErrors.ValueIsInvalid(nameof(ReactivateAccountCommand.Token))); } \ No newline at end of file diff --git a/src/Application/Users/Commands/RegisterUser/RegisterUserCommand.cs b/src/Application/Users/Commands/RegisterUser/RegisterUserCommand.cs index f8171837..155890b2 100644 --- a/src/Application/Users/Commands/RegisterUser/RegisterUserCommand.cs +++ b/src/Application/Users/Commands/RegisterUser/RegisterUserCommand.cs @@ -1,4 +1,6 @@ -using Mediator; +using FluentValidation; +using SiteWatcher.Application.Common.Command; +using SiteWatcher.Application.Common.Results; using SiteWatcher.Application.Interfaces; using SiteWatcher.Domain.Authentication; using SiteWatcher.Domain.Authentication.Services; @@ -9,7 +11,7 @@ namespace SiteWatcher.Application.Users.Commands.RegisterUser; -public class RegisterUserCommand : ICommand +public class RegisterUserCommand { public string? Name { get; set; } public string? Email { get; set; } @@ -21,29 +23,29 @@ public class RegisterUserCommand : ICommand public RegisterUserInput ToInputModel() => new (Name!, Email!, Language, Theme, GoogleId, AuthEmail); } -public class RegisterUserCommandHandler : ICommandHandler +public class RegisterUserCommandHandler : BaseHandler> { private readonly ISiteWatcherContext _context; private readonly IAuthService _authService; private readonly ISession _session; public RegisterUserCommandHandler(ISiteWatcherContext context, IAuthService authService, - ISession session) + ISession session, IValidator validator) : base(validator) { _context = context; _authService = authService; _session = session; } - public async ValueTask Handle(RegisterUserCommand request, CancellationToken cancellationToken) + protected override async Task> HandleCommand(RegisterUserCommand command, CancellationToken ct) { - var user = User.FromInputModel(request.ToInputModel(), _session.Now); + var user = User.FromInputModel(command.ToInputModel(), _session.Now); // TODO: remove this exception, not rely on database for a business rule try { _context.Users.Add(user); - await _context.SaveChangesAsync(cancellationToken); + await _context.SaveChangesAsync(ct); } catch (UniqueViolationException) { diff --git a/src/Application/Users/Commands/SendReactivateAccountEmail/SendReactivateAccountEmailCommand.cs b/src/Application/Users/Commands/SendReactivateAccountEmail/SendReactivateAccountEmailCommand.cs index 6d620887..edd2a338 100644 --- a/src/Application/Users/Commands/SendReactivateAccountEmail/SendReactivateAccountEmailCommand.cs +++ b/src/Application/Users/Commands/SendReactivateAccountEmail/SendReactivateAccountEmailCommand.cs @@ -1,36 +1,39 @@ -using Mediator; +using FluentValidation; using Microsoft.EntityFrameworkCore; +using SiteWatcher.Application.Common.Command; +using SiteWatcher.Application.Common.Results; using SiteWatcher.Application.Interfaces; using SiteWatcher.Domain.Authentication; using SiteWatcher.Domain.Common.ValueObjects; namespace SiteWatcher.Application.Users.Commands.ActivateAccount; -public class SendReactivateAccountEmailCommand : ICommand +public class SendReactivateAccountEmailCommand { public UserId UserId { get; set; } } -public class SendReactivateAccountEmailCommandHandler : ICommandHandler +public class SendReactivateAccountEmailCommandHandler : BaseHandler { private readonly ISiteWatcherContext _context; private readonly ISession _session; - public SendReactivateAccountEmailCommandHandler(ISiteWatcherContext context, ISession session) + public SendReactivateAccountEmailCommandHandler(ISiteWatcherContext context, ISession session, + IValidator validator) : base(validator) { _context = context; _session = session; } - public async ValueTask Handle(SendReactivateAccountEmailCommand request, CancellationToken cancellationToken) + protected override async Task HandleCommand(SendReactivateAccountEmailCommand command, CancellationToken ct) { var user = await _context.Users - .FirstOrDefaultAsync(u => u.Id == request.UserId && !u.Active, cancellationToken); - if(user is null) - return Unit.Value; + .FirstOrDefaultAsync(u => u.Id == command.UserId && !u.Active, ct); + if (user is null) + return Result.Empty; user.GenerateReactivationToken(_session.Now); await _context.SaveChangesAsync(CancellationToken.None); - return Unit.Value; + return Result.Empty; } } \ No newline at end of file diff --git a/src/Application/Users/Commands/UpdateUser/UpdateUserCommand.cs b/src/Application/Users/Commands/UpdateUser/UpdateUserCommand.cs index b4711213..bb797f57 100644 --- a/src/Application/Users/Commands/UpdateUser/UpdateUserCommand.cs +++ b/src/Application/Users/Commands/UpdateUser/UpdateUserCommand.cs @@ -1,49 +1,51 @@ -using Mediator; +using FluentValidation; using Microsoft.EntityFrameworkCore; -using SiteWatcher.Application.Common.Commands; +using SiteWatcher.Application.Common.Command; using SiteWatcher.Application.Common.Constants; +using SiteWatcher.Application.Common.Results; using SiteWatcher.Application.Interfaces; using SiteWatcher.Domain.Authentication; -using SiteWatcher.Domain.Common.Constants; +using SiteWatcher.Domain.Common.Errors; using SiteWatcher.Domain.Common.Services; using SiteWatcher.Domain.Users.DTOs; using SiteWatcher.Domain.Users.Enums; namespace SiteWatcher.Application.Users.Commands.UpdateUser; -public class UpdateUserCommand : ICommand +public class UpdateUserCommand { public string? Name { get; set; } public string? Email { get; set; } public Language Language { get; set; } public Theme Theme { get; set; } - public UpdateUserInput ToInputModel() => new (Name!, Email!, Language, Theme); + public UpdateUserInput ToInputModel() => new(Name!, Email!, Language, Theme); } -public class UpdateUserCommandHandler : ICommandHandler +public class UpdateUserCommandHandler : BaseHandler> { private readonly ISiteWatcherContext _context; private readonly ISession _session; private readonly ICache _cache; - public UpdateUserCommandHandler(ISiteWatcherContext context, ISession session, ICache cache) + public UpdateUserCommandHandler(ISiteWatcherContext context, ISession session, ICache cache, + IValidator validator) : base(validator) { _context = context; _session = session; _cache = cache; } - public async ValueTask Handle(UpdateUserCommand request, CancellationToken cancellationToken) + protected override async Task> HandleCommand(UpdateUserCommand command, CancellationToken ct) { var user = await _context.Users - .FirstOrDefaultAsync(u => u.Id == _session.UserId && u.Active, cancellationToken); + .FirstOrDefaultAsync(u => u.Id == _session.UserId && u.Active, ct); if (user is null) - return CommandResult.FromError(ApplicationErrors.USER_DO_NOT_EXIST); + return Error.Validation(ApplicationErrors.USER_DO_NOT_EXIST); - user.Update(request.ToInputModel(), _session.Now); - await _context.SaveChangesAsync(cancellationToken); + user.Update(command.ToInputModel(), _session.Now); + await _context.SaveChangesAsync(ct); - return CommandResult.FromValue(new UpdateUserResult(new UserViewModel(user), !user.EmailConfirmed)); + return new UpdateUserResult(new UserViewModel(user), !user.EmailConfirmed); } } \ No newline at end of file diff --git a/src/Domain/Common/Errors/Error.cs b/src/Domain/Common/Errors/Error.cs new file mode 100644 index 00000000..9d120159 --- /dev/null +++ b/src/Domain/Common/Errors/Error.cs @@ -0,0 +1,15 @@ +namespace SiteWatcher.Domain.Common.Errors; + +public sealed class Error +{ + public Error(ErrorType type, params string[] messages) + { + Type = type; + Messages = messages; + } + + public ErrorType Type { get; } + public string[] Messages { get; } + + public static Error Validation(params string[] messages) => new Error(ErrorType.Validation, messages); +} \ No newline at end of file diff --git a/src/Domain/Common/Errors/ErrorType.cs b/src/Domain/Common/Errors/ErrorType.cs new file mode 100644 index 00000000..fb7cf5da --- /dev/null +++ b/src/Domain/Common/Errors/ErrorType.cs @@ -0,0 +1,6 @@ +namespace SiteWatcher.Domain.Common.Errors; + +public enum ErrorType +{ + Validation +} \ No newline at end of file diff --git a/src/Infra/Cache/RedisCache.cs b/src/Infra/Cache/RedisCache.cs index e6d79367..6a459b59 100644 --- a/src/Infra/Cache/RedisCache.cs +++ b/src/Infra/Cache/RedisCache.cs @@ -1,7 +1,4 @@ -using System; using System.Text.Json; -using System.Threading.Tasks; -using SiteWatcher.Application.Interfaces; using SiteWatcher.Domain.Common.Services; using StackExchange.Redis; diff --git a/src/Infra/HealthChecks/PostgresConnectionHealthCheck.cs b/src/Infra/HealthChecks/PostgresConnectionHealthCheck.cs index b2c3a6af..7c974def 100644 --- a/src/Infra/HealthChecks/PostgresConnectionHealthCheck.cs +++ b/src/Infra/HealthChecks/PostgresConnectionHealthCheck.cs @@ -1,6 +1,3 @@ -using System; -using System.Threading; -using System.Threading.Tasks; using Microsoft.Extensions.Diagnostics.HealthChecks; using Npgsql; namespace SiteWatcher.Infra.HealthChecks; diff --git a/src/Infra/HealthChecks/RabbitMqHealthCheck.cs b/src/Infra/HealthChecks/RabbitMqHealthCheck.cs index 8a4cb8a2..93dde9a8 100644 --- a/src/Infra/HealthChecks/RabbitMqHealthCheck.cs +++ b/src/Infra/HealthChecks/RabbitMqHealthCheck.cs @@ -1,9 +1,6 @@ using Microsoft.Extensions.Diagnostics.HealthChecks; using RabbitMQ.Client; using SiteWatcher.Infra.Messaging; -using System; -using System.Threading; -using System.Threading.Tasks; namespace SiteWatcher.Infra.HealthChecks; diff --git a/src/Infra/Http/HttpRetryPolicies.cs b/src/Infra/Http/HttpRetryPolicies.cs index c204ad88..f1e8e79e 100644 --- a/src/Infra/Http/HttpRetryPolicies.cs +++ b/src/Infra/Http/HttpRetryPolicies.cs @@ -1,8 +1,6 @@ using System.Net; using Microsoft.Extensions.Logging; using Polly; -using System; -using System.Net.Http; namespace SiteWatcher.Infra.Http; diff --git a/src/Infra/Persistence/Configuration/UriValueConverter.cs b/src/Infra/Persistence/Configuration/UriValueConverter.cs index becf95f0..e676b559 100644 --- a/src/Infra/Persistence/Configuration/UriValueConverter.cs +++ b/src/Infra/Persistence/Configuration/UriValueConverter.cs @@ -1,5 +1,4 @@ -using System; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; namespace SiteWatcher.Infra.Persistence.Configuration; diff --git a/src/Infra/Persistence/DatabaseMigrator.cs b/src/Infra/Persistence/DatabaseMigrator.cs index 52b6e8a6..e171cac6 100644 --- a/src/Infra/Persistence/DatabaseMigrator.cs +++ b/src/Infra/Persistence/DatabaseMigrator.cs @@ -1,7 +1,4 @@ -using System; using System.Diagnostics; -using System.Linq; -using System.Threading.Tasks; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; diff --git a/src/Infra/Persistence/Queries.cs b/src/Infra/Persistence/Queries.cs index 9b0e2f1c..430ab6b0 100644 --- a/src/Infra/Persistence/Queries.cs +++ b/src/Infra/Persistence/Queries.cs @@ -93,19 +93,19 @@ INNER JOIN ""sw"".""Rules"" r #endregion - public QueryResult GetUserByGoogleId(string googleId) + public DbQuery GetUserByGoogleId(string googleId) { var parameters = new Dictionary { { "googleId", googleId } }; - return new QueryResult(GetUserByGoogleIdQuery, parameters); + return new DbQuery(GetUserByGoogleIdQuery, parameters); } - public QueryResult GetUserById(UserId userId) + public DbQuery GetUserById(UserId userId) { var parameters = new Dictionary { { "userId", userId.Value } }; - return new QueryResult(GetUserByIdQuery, parameters); + return new DbQuery(GetUserByIdQuery, parameters); } - public QueryResult GetSimpleAlertViewListByUserId(UserId userId, AlertId? lastAlertId, int take) + public DbQuery GetSimpleAlertViewListByUserId(UserId userId, AlertId? lastAlertId, int take) { var parameters = new Dictionary { @@ -113,20 +113,20 @@ public QueryResult GetSimpleAlertViewListByUserId(UserId userId, AlertId? lastAl { "lastAlertId", lastAlertId?.Value ?? 0 }, { "take", take } }; - return new QueryResult(GetSimpleAlertViewListByUserIdQuery, parameters); + return new DbQuery(GetSimpleAlertViewListByUserIdQuery, parameters); } - public QueryResult GetAlertDetails(UserId userId, AlertId alertId) + public DbQuery GetAlertDetails(UserId userId, AlertId alertId) { var parameters = new Dictionary { { "userId", userId.Value }, { "alertId", alertId.Value } }; - return new QueryResult(GetAlertDetailsQuery, parameters); + return new DbQuery(GetAlertDetailsQuery, parameters); } - public QueryResult SearchSimpleAlerts(UserId userId, string[] searchTerms, int take) + public DbQuery SearchSimpleAlerts(UserId userId, string[] searchTerms, int take) { var query = _searchSimpleAlertsQueryCache.GetOrAdd(searchTerms.Length, GenerateSearchSimpleAlertsQuery); var parameters = new Dictionary {{"userId", userId}}; @@ -136,7 +136,7 @@ public QueryResult SearchSimpleAlerts(UserId userId, string[] searchTerms, int t parameters.Add($"searchTermWildCards{i}", $"%{searchTerms[i]}%"); parameters.Add($"searchTerm{i}", searchTerms[i]); } - return new QueryResult(query, parameters); + return new DbQuery(query, parameters); } private static string GenerateSearchSimpleAlertsQuery(int searchTermsLenght) diff --git a/src/WebAPI/Controllers/AlertController.cs b/src/WebAPI/Controllers/AlertController.cs index 738cbb22..2a4e4c31 100644 --- a/src/WebAPI/Controllers/AlertController.cs +++ b/src/WebAPI/Controllers/AlertController.cs @@ -7,10 +7,7 @@ using SiteWatcher.Application.Alerts.Commands.GetUserAlerts; using SiteWatcher.Application.Alerts.Commands.SearchAlerts; using SiteWatcher.Application.Alerts.Commands.UpdateAlert; -using SiteWatcher.Domain.Alerts.DTOs; -using SiteWatcher.Domain.Common.DTOs; using SiteWatcher.WebAPI.Extensions; -using SiteWatcher.WebAPI.Filters; using SiteWatcher.WebAPI.Filters.Cache; namespace SiteWatcher.WebAPI.Controllers; @@ -28,16 +25,20 @@ public AlertController(IMediator mediator) } [HttpPost] - [CommandValidationFilter] - public async Task CreateAlert(CreateAlertCommand request, CancellationToken ct) => - Created(string.Empty, await _mediator.Send(request, ct)); + public async Task CreateAlert([FromServices] CreateAlertCommandHandler handlerHandler, + CreateAlertCommand request, CancellationToken ct) + { + var res = await handlerHandler.Handle(request, ct); + return res.Error != null ? res.Error.ToActionResult() : Created(string.Empty, res.Value); + } [HttpGet] [CacheFilter] - public async Task GetUserAlerts([FromQuery] GetUserAlertsQuery request, CancellationToken ct) + public async Task GetUserAlerts([FromServices] GetUserAlertsQueryHandler handler, + [FromQuery] GetUserAlertsQuery request, CancellationToken ct) { - var commandResult = await _mediator.Send(request, ct); - return commandResult.ToActionResult>(); + var res = await handler.Handle(request, ct); + return res.Error != null ? res.Error.ToActionResult() : Ok(res.Value); } [HttpGet("{AlertId}/details")] @@ -46,24 +47,27 @@ public async Task GetAlertDetails([FromRoute] GetAlertDetailsQuer Ok(await _mediator.Send(request, ct)); [HttpDelete("{AlertId}")] - public async Task DeleteAlert([FromRoute] DeleteAlertCommand request, - CancellationToken cancellationToken) + public async Task DeleteAlert([FromServices] DeleteAlertCommandHandler handler, + [FromRoute] DeleteAlertCommand request, CancellationToken ct) { - var commandResult = await _mediator.Send(request, cancellationToken); - return commandResult.ToActionResult(); + var res = await handler.Handle(request, ct); + return res.Error != null ? res.Error.ToActionResult() : NoContent(); } [HttpPut] - [CommandValidationFilter] - public async Task UpdateAlert([FromBody] UpdateAlertCommmand request, CancellationToken ct) + public async Task UpdateAlert([FromServices] UpdateAlertCommandHandler commandHandler, + [FromBody] UpdateAlertCommmand request, CancellationToken ct) { - var commandResult = await _mediator.Send(request, ct); - return commandResult.ToActionResult(); + var res = await commandHandler.Handle(request, ct); + return res.Error != null ? res.Error.ToActionResult() : Ok(res.Value); } [HttpGet("search")] [CacheFilter] - [CommandValidationFilter] - public async Task SearchAlerts([FromQuery] SearchAlertQuery request, CancellationToken ct) => - Ok(await _mediator.Send(request, ct)); + public async Task SearchAlerts([FromServices] SearchAlertQueryHandler handler, + [FromQuery] SearchAlertQuery request, CancellationToken ct) + { + var res = await handler.Handle(request, ct); + return res.Error != null ? res.Error.ToActionResult() : Ok(res.Value); + } } \ No newline at end of file diff --git a/src/WebAPI/Controllers/UserController.cs b/src/WebAPI/Controllers/UserController.cs index e044216b..1e0f659f 100644 --- a/src/WebAPI/Controllers/UserController.cs +++ b/src/WebAPI/Controllers/UserController.cs @@ -11,7 +11,6 @@ using SiteWatcher.Application.Users.Commands.RegisterUser; using SiteWatcher.Application.Users.Commands.SendEmailConfirmation; using SiteWatcher.Application.Users.Commands.UpdateUser; -using SiteWatcher.WebAPI.Filters; using SiteWatcher.Domain.Common.Constants; using SiteWatcher.Infra.Authorization.Constants; using SiteWatcher.WebAPI.Extensions; @@ -41,19 +40,25 @@ public async Task GetUserInfo([FromRoute] GetUserInfoQuery reques } [HttpPost] - [CommandValidationFilter] [Route("register")] [Authorize(Policy = Policies.ValidRegisterData)] - public async Task Register(RegisterUserCommand request) + public async Task Register([FromServices] RegisterUserCommandHandler handler, + RegisterUserCommand request, CancellationToken ct) { - request.GoogleId = HttpContext.User.Claims.FirstOrDefault(c => c.Type == AuthenticationDefaults.ClaimTypes.GoogleId)?.Value; - request.AuthEmail = HttpContext.User.Claims.FirstOrDefault(c => c.Type == AuthenticationDefaults.ClaimTypes.Email)?.Value; - RegisterUserResult commandResult = await _mediator.Send(request); - return commandResult switch + request.GoogleId = HttpContext.User.Claims + .FirstOrDefault(c => c.Type == AuthenticationDefaults.ClaimTypes.GoogleId)?.Value; + request.AuthEmail = HttpContext.User.Claims + .FirstOrDefault(c => c.Type == AuthenticationDefaults.ClaimTypes.Email)?.Value; + + var res = await handler.Handle(request, ct); + + if (res.Error != null) return res.Error.ToActionResult(); + + return res.Value switch { AlreadyExists => Conflict(), Registered registered => Created(string.Empty, registered), - _ => throw new ArgumentOutOfRangeException(nameof(commandResult)) + _ => throw new ArgumentOutOfRangeException(nameof(res.Value)) }; } @@ -64,19 +69,20 @@ public async Task ResendConfirmationEmail() => [AllowAnonymous] [HttpPut("confirm-email")] - public async Task ConfirmEmail(ConfirmEmailCommand request) + public async Task ConfirmEmail([FromServices] ConfirmEmailCommandHandler handler, + ConfirmEmailCommand request, CancellationToken ct) { - var commandResult = await _mediator.Send(request); - return commandResult.ToActionResult(); + var res = await handler.Handle(request, ct); + return res.Error != null ? res.Error.ToActionResult() : NoContent(); } [Authorize] [HttpPut] - [CommandValidationFilter] - public async Task UpdateUser(UpdateUserCommand request) + public async Task UpdateUser([FromServices] UpdateUserCommandHandler handler, + UpdateUserCommand request, CancellationToken ct) { - var commandResult = await _mediator.Send(request); - return commandResult.ToActionResult(); + var res = await handler.Handle(request, ct); + return res.Error != null ? res.Error.ToActionResult() : Ok(res.Value); } [Authorize] @@ -85,17 +91,22 @@ public async Task DeactivateAccount() => await _mediator.Send(new DeactivateAccountCommand()); [AllowAnonymous] - [CommandValidationFilter] [HttpPut("send-reactivate-account-email")] - public async Task SendReactivateAccountEmail(SendReactivateAccountEmailCommand request) => - await _mediator.Send(request); + public async Task SendReactivateAccountEmail( + [FromServices] SendReactivateAccountEmailCommandHandler handler, + SendReactivateAccountEmailCommand request, CancellationToken ct) + { + var res = await handler.Handle(request, ct); + return res.Error != null ? res.Error.ToActionResult() : Ok(); + } [AllowAnonymous] [HttpPut("reactivate-account")] - public async Task ReactivateAccount(ReactivateAccountCommand request) + public async Task ReactivateAccount([FromServices] ReactivateAccountCommandHandler handler, + ReactivateAccountCommand request, CancellationToken ct) { - var commandResult = await _mediator.Send(request); - return commandResult.ToActionResult(); + var res = await handler.Handle(request, ct); + return res.Error != null ? res.Error.ToActionResult() : NoContent(); } [Authorize] diff --git a/src/WebAPI/Extensions/CommandResultExtensions.cs b/src/WebAPI/Extensions/CommandResultExtensions.cs deleted file mode 100644 index 8322449f..00000000 --- a/src/WebAPI/Extensions/CommandResultExtensions.cs +++ /dev/null @@ -1,42 +0,0 @@ -using Microsoft.AspNetCore.Mvc; -using SiteWatcher.Application.Common.Commands; -using EmptyResult = SiteWatcher.Application.Common.Commands.EmptyResult; - -namespace SiteWatcher.WebAPI.Extensions; - -public static class CommandResultExtensions -{ - private const string ResultIsNotEmpty = "The result has a value, use instead ToActionResult"; - - /// - /// Handle command results with value. - /// - /// Command result to be handled - /// Result handler, the default value is OkObjectResult - /// Type of the CommandValueResult - /// Action result - public static IActionResult ToActionResult(this CommandResult commandResult, Func? valueResultHandler = null) => - commandResult switch - { - EmptyResult => new OkObjectResult(null), - ErrorResult errorResult => new BadRequestObjectResult(errorResult.Errors), - ValueResult valueResult => valueResultHandler is null ? - new OkObjectResult(valueResult.Value) : valueResultHandler(valueResult.Value), - _ => throw new ArgumentOutOfRangeException(nameof(commandResult), commandResult, null) - }; - - /// - /// Handle empty command results. - /// - /// - /// Action result - /// The result has a value - public static IActionResult ToActionResult(this CommandResult commandResult) => - commandResult switch - { - EmptyResult => new OkObjectResult(null), - ErrorResult errorResult => new BadRequestObjectResult(errorResult.Errors), - { } => throw new InvalidOperationException(ResultIsNotEmpty), - null => throw new ArgumentNullException(nameof(commandResult)) - }; -} \ No newline at end of file diff --git a/src/WebAPI/Extensions/ErrorExtensions.cs b/src/WebAPI/Extensions/ErrorExtensions.cs new file mode 100644 index 00000000..1a4ebf44 --- /dev/null +++ b/src/WebAPI/Extensions/ErrorExtensions.cs @@ -0,0 +1,16 @@ +using Microsoft.AspNetCore.Mvc; +using SiteWatcher.Domain.Common.Errors; + +namespace SiteWatcher.WebAPI.Extensions; + +public static class ErrorExtensions +{ + public static IActionResult ToActionResult(this Error error) + { + return error switch + { + { Type: ErrorType.Validation } => new BadRequestObjectResult(error.Messages), + _ => throw new ArgumentOutOfRangeException(nameof(error)) + }; + } +} \ No newline at end of file diff --git a/src/WebAPI/Filters/CommandValidationFilter.cs b/src/WebAPI/Filters/CommandValidationFilter.cs deleted file mode 100644 index 2be5c30e..00000000 --- a/src/WebAPI/Filters/CommandValidationFilter.cs +++ /dev/null @@ -1,39 +0,0 @@ -using System.Net; -using FluentValidation; -using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Mvc.Filters; - -namespace SiteWatcher.WebAPI.Filters; - -/// -/// Command must be named "command" and have an implementation to be validated. -/// -public class CommandValidationFilter : ActionFilterAttribute -{ - public override async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next) - { - var errs = await ValidateInputAsync(context); - if (errs.Length == 0) - { - await next(); - return; - } - - var result = new ObjectResult(errs) - { - StatusCode = (int) HttpStatusCode.BadRequest - }; - context.Result = result; - } - - private static async Task ValidateInputAsync(ActionExecutingContext context) - { - var command = context.ActionArguments["request"]; - var validatorType = typeof(IValidator<>).MakeGenericType(command!.GetType()); - - var validator = context.HttpContext.RequestServices.GetRequiredService(validatorType) as IValidator; - var result = await validator!.ValidateAsync(new ValidationContext(command)); - - return result.IsValid ? Array.Empty(): result.Errors.Select(err => err.ErrorMessage).ToArray(); - } -} \ No newline at end of file diff --git a/test/IntegrationTests/NotificationTests/ProcessNotificationTests.cs b/test/IntegrationTests/NotificationTests/ProcessNotificationTests.cs index 6fb55c53..57048c0c 100644 --- a/test/IntegrationTests/NotificationTests/ProcessNotificationTests.cs +++ b/test/IntegrationTests/NotificationTests/ProcessNotificationTests.cs @@ -4,7 +4,6 @@ using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using Moq; -using SiteWatcher.Application.Common.Messages; using SiteWatcher.Domain.Alerts; using SiteWatcher.Domain.Alerts.Entities.Triggerings; using SiteWatcher.Domain.Alerts.Enums; diff --git a/test/IntegrationTests/Setup/WebApplicationFactory/BaseTestFixtureOptions.cs b/test/IntegrationTests/Setup/WebApplicationFactory/BaseTestFixtureOptions.cs index aecb0554..6c613cff 100644 --- a/test/IntegrationTests/Setup/WebApplicationFactory/BaseTestFixtureOptions.cs +++ b/test/IntegrationTests/Setup/WebApplicationFactory/BaseTestFixtureOptions.cs @@ -1,5 +1,4 @@ using System.Collections.ObjectModel; -using IntegrationTests.Setup; using SiteWatcher.Infra.Persistence; namespace SiteWatcher.IntegrationTests.Setup.WebApplicationFactory; diff --git a/test/UnitTests/Commands/ConfirmEmailCommandTests.cs b/test/UnitTests/Commands/ConfirmEmailCommandTests.cs index 018d456f..d538261b 100644 --- a/test/UnitTests/Commands/ConfirmEmailCommandTests.cs +++ b/test/UnitTests/Commands/ConfirmEmailCommandTests.cs @@ -1,7 +1,6 @@ using FluentAssertions; using MockQueryable.Moq; using Moq; -using SiteWatcher.Application.Common.Commands; using SiteWatcher.Application.Common.Constants; using SiteWatcher.Application.Interfaces; using SiteWatcher.Application.Users.Commands.ConfirmEmail; @@ -16,14 +15,9 @@ namespace UnitTests.Commands; public sealed class ConfirmEmailCommandTests { - private readonly Mock _authServiceMock; + private readonly Mock _authServiceMock = new(); private SqliteContext _context = null!; - public ConfirmEmailCommandTests() - { - _authServiceMock = new Mock(); - } - [Fact] public async Task CantConfirmEmailIfUserDoesNotExists() { @@ -34,11 +28,11 @@ public async Task CantConfirmEmailIfUserDoesNotExists() var commandHandler = new ConfirmEmailCommandHandler(_authServiceMock.Object, _context, null!); // Act - var result = await commandHandler.Handle(new ConfirmEmailCommand(), CancellationToken.None) as ErrorResult; + var result = await commandHandler.Handle(new ConfirmEmailCommand(), CancellationToken.None); // Assert - result!.Errors.Count().Should().Be(1); - result.Errors.First() + result.Error!.Messages.Length.Should().Be(1); + result.Error.Messages[0] .Should().Be(ApplicationErrors.ValueIsInvalid(nameof(ConfirmEmailCommand.Token))); } @@ -61,14 +55,14 @@ public async Task UserCantConfirmEmailWithInvalidToken() var commandHandler = new ConfirmEmailCommandHandler(_authServiceMock.Object, contextMock.Object, sessionMock.Object); // Act - var result = await commandHandler.Handle(new ConfirmEmailCommand {Token = "INVALID_TOKEN"}, CancellationToken.None) as ErrorResult; + var result = await commandHandler.Handle(new ConfirmEmailCommand {Token = "INVALID_TOKEN"}, CancellationToken.None); // Assert - result!.Errors - .Count().Should().Be(1); + result.Error!.Messages + .Length.Should().Be(1); - result.Errors.First() + result.Error.Messages[0] .Should().Be(ApplicationErrors.ValueIsInvalid(nameof(ConfirmEmailCommand.Token))); user.EmailConfirmed.Should().BeFalse(); diff --git a/test/UnitTests/Commands/DeleteAlertCommandTests.cs b/test/UnitTests/Commands/DeleteAlertCommandTests.cs index adb75bd5..ae075676 100644 --- a/test/UnitTests/Commands/DeleteAlertCommandTests.cs +++ b/test/UnitTests/Commands/DeleteAlertCommandTests.cs @@ -2,7 +2,6 @@ using Microsoft.EntityFrameworkCore; using Moq; using SiteWatcher.Application.Alerts.Commands.DeleteAlert; -using SiteWatcher.Application.Common.Commands; using SiteWatcher.Application.Common.Constants; using SiteWatcher.Application.Interfaces; using SiteWatcher.Domain.Alerts; @@ -27,10 +26,10 @@ public async Task InvalidAlertIdDoesntDeleteAnything() var command = new DeleteAlertCommand {AlertId = "invalidId"}; // Act - var result = await handler.Handle(command, default) as ErrorResult; + var result = await handler.Handle(command, default); // Assert - result!.Errors.Count().Should().Be(1); - result.Errors.First().Should().Be(ApplicationErrors.ValueIsInvalid(nameof(DeleteAlertCommand.AlertId))); + result.Error!.Messages.Length.Should().Be(1); + result.Error.Messages[0].Should().Be(ApplicationErrors.ValueIsInvalid(nameof(DeleteAlertCommand.AlertId))); } } \ No newline at end of file diff --git a/test/UnitTests/Commands/GetUserAlertsCommandTests.cs b/test/UnitTests/Commands/GetUserAlertsCommandTests.cs index f105eabe..5bacaf1f 100644 --- a/test/UnitTests/Commands/GetUserAlertsCommandTests.cs +++ b/test/UnitTests/Commands/GetUserAlertsCommandTests.cs @@ -2,7 +2,6 @@ using FluentAssertions; using Moq; using SiteWatcher.Application.Alerts.Commands.GetUserAlerts; -using SiteWatcher.Application.Common.Commands; using SiteWatcher.Application.Interfaces; namespace UnitTests.Commands; @@ -21,7 +20,7 @@ public async Task RepositoryIsNotCalledWithTakeEqualsToZero() var result = await handler.Handle(command, default); // Assert - result.Should().BeAssignableTo(); + result.Value.Should().BeNull(); dapperContextMock .Verify(r => r.UsingConnectionAsync(It.IsAny>>()), diff --git a/test/UnitTests/Commands/ReactivateAccountCommandTests.cs b/test/UnitTests/Commands/ReactivateAccountCommandTests.cs index 9ef05071..1c2c723f 100644 --- a/test/UnitTests/Commands/ReactivateAccountCommandTests.cs +++ b/test/UnitTests/Commands/ReactivateAccountCommandTests.cs @@ -1,7 +1,6 @@ using FluentAssertions; using MockQueryable.Moq; using Moq; -using SiteWatcher.Application.Common.Commands; using SiteWatcher.Application.Common.Constants; using SiteWatcher.Application.Interfaces; using SiteWatcher.Application.Users.Commands.ReactivateAccount; @@ -15,12 +14,7 @@ namespace UnitTests.Commands; public sealed class ReactivateAccountCommandTests { - private readonly Mock _authServiceMock; - - public ReactivateAccountCommandTests() - { - _authServiceMock = new Mock(); - } + private readonly Mock _authServiceMock = new(); [Fact] public async Task CantReactiveAccountIfUserDoesNotExists() @@ -38,13 +32,13 @@ public async Task CantReactiveAccountIfUserDoesNotExists() var command = new ReactivateAccountCommand {Token = "token"}; // Act - var result = await commandHandler.Handle(command, CancellationToken.None) as ErrorResult; + var result = await commandHandler.Handle(command, CancellationToken.None); // Assert - result!.Errors - .Count().Should().Be(1); + result.Error!.Messages + .Length.Should().Be(1); - result.Errors.First() + result.Error.Messages[0] .Should().Be(ApplicationErrors.ValueIsInvalid(nameof(ReactivateAccountCommand.Token))); } @@ -69,13 +63,13 @@ public async Task UserCantReactivateAccountlWithInvalidToken() var command = new ReactivateAccountCommand {Token = "INVALID_TOKEN"}; // Act - var result = await commandHandler.Handle(command, CancellationToken.None) as ErrorResult; + var result = await commandHandler.Handle(command, CancellationToken.None); // Assert - result!.Errors - .Count().Should().Be(1); + result.Error!.Messages + .Length.Should().Be(1); - result.Errors.First() + result.Error.Messages[0] .Should().Be(ApplicationErrors.ValueIsInvalid(nameof(ReactivateAccountCommand.Token))); user.Active.Should().BeFalse(); diff --git a/test/UnitTests/Commands/SendReactivateAccountEmailCommandTests.cs b/test/UnitTests/Commands/SendReactivateAccountEmailCommandTests.cs index 645b58ec..93388ffc 100644 --- a/test/UnitTests/Commands/SendReactivateAccountEmailCommandTests.cs +++ b/test/UnitTests/Commands/SendReactivateAccountEmailCommandTests.cs @@ -1,4 +1,6 @@ -using MockQueryable.Moq; +using FluentValidation; +using FluentValidation.Results; +using MockQueryable.Moq; using Moq; using SiteWatcher.Application.Interfaces; using SiteWatcher.Application.Users.Commands.ActivateAccount; @@ -13,9 +15,16 @@ public async Task CantSentReactivateAccountEmailForNonExistingUser() { // Arrange var dbsetMock = Array.Empty().AsQueryable().BuildMockDbSet(); + var contextMock = new Mock(); contextMock.Setup(c => c.Users).Returns(dbsetMock.Object); - var commandHandler = new SendReactivateAccountEmailCommandHandler(contextMock.Object, null!); + + var validatorMock = new Mock>(); + validatorMock.Setup(v => v.Validate(It.IsAny())) + .Returns(new ValidationResult()); + + var commandHandler = + new SendReactivateAccountEmailCommandHandler(contextMock.Object, null!, validatorMock.Object); // Act await commandHandler.Handle(new SendReactivateAccountEmailCommand(), default); diff --git a/test/UnitTests/Commands/UpdateAlertCommandTests.cs b/test/UnitTests/Commands/UpdateAlertCommandTests.cs index 22f2b6f1..8c0e0e9c 100644 --- a/test/UnitTests/Commands/UpdateAlertCommandTests.cs +++ b/test/UnitTests/Commands/UpdateAlertCommandTests.cs @@ -1,8 +1,9 @@ using FluentAssertions; +using FluentValidation; +using FluentValidation.Results; using MockQueryable.Moq; using Moq; using SiteWatcher.Application.Alerts.Commands.UpdateAlert; -using SiteWatcher.Application.Common.Commands; using SiteWatcher.Application.Common.Constants; using SiteWatcher.Application.Interfaces; using SiteWatcher.Common.Services; @@ -12,15 +13,17 @@ namespace UnitTests.Commands; - - public sealed class UpdateAlertCommandTests { private readonly Mock _hasherMock; + private readonly Mock> _validatorMock; public UpdateAlertCommandTests() { _hasherMock = new Mock(); + _validatorMock = new Mock>(); + _validatorMock.Setup(v => v.Validate(It.IsAny())) + .Returns(new ValidationResult()); } public static TheoryData InvalidIds => new() @@ -37,14 +40,15 @@ public async Task AlertIsNotUpdatedWithInvalidId(int alertId) _hasherMock.Setup(h => h.DecodeId(It.IsAny())) .Returns(alertId); - var handler = new UpdateAlertCommandHandler(_hasherMock.Object,null!, null!); + var handler = new UpdateAlertCommandHandler(_hasherMock.Object, null!, null!, _validatorMock.Object); var command = new UpdateAlertCommmand(); // Act - var result = await handler.Handle(command, CancellationToken.None) as ErrorResult; + var result = await handler.Handle(command, CancellationToken.None); // Assert - result!.Errors + result.Error!.Messages.Length.Should().Be(1); + result.Error.Messages[0] .Should() .BeEquivalentTo(ApplicationErrors.ValueIsInvalid(nameof(UpdateAlertCommmand.AlertId))); } @@ -63,14 +67,16 @@ public async Task NonexistentAlertDoesntCallSaveChanges() var sessionMock = new Mock(); sessionMock.Setup(s => s.UserId).Returns(UserId.Empty); - var handler = new UpdateAlertCommandHandler(_hasherMock.Object, contextMock.Object, sessionMock.Object); + var handler = new UpdateAlertCommandHandler(_hasherMock.Object, contextMock.Object, sessionMock.Object, + _validatorMock.Object); var command = new UpdateAlertCommmand(); // Act - var result = await handler.Handle(command, CancellationToken.None) as ErrorResult; + var result = await handler.Handle(command, CancellationToken.None); // Assert - result!.Errors + result.Error!.Messages.Length.Should().Be(1); + result.Error.Messages[0] .Should() .BeEquivalentTo(ApplicationErrors.ALERT_DO_NOT_EXIST); diff --git a/test/UnitTests/Commands/UpdateUserCommandTests.cs b/test/UnitTests/Commands/UpdateUserCommandTests.cs index d9b98f77..6756e552 100644 --- a/test/UnitTests/Commands/UpdateUserCommandTests.cs +++ b/test/UnitTests/Commands/UpdateUserCommandTests.cs @@ -1,7 +1,8 @@ using FluentAssertions; +using FluentValidation; +using FluentValidation.Results; using MockQueryable.Moq; using Moq; -using SiteWatcher.Application.Common.Commands; using SiteWatcher.Application.Common.Constants; using SiteWatcher.Application.Interfaces; using SiteWatcher.Application.Users.Commands.UpdateUser; @@ -22,17 +23,21 @@ public async Task CantUpdateNonExistingUser() contextMock.Setup(c => c.Users).Returns(dbSetMock.Object); var session = new Mock().Object; - var commandHandler = new UpdateUserCommandHandler(contextMock.Object, session, new FakeCache()); + var validatorMock = new Mock>(); + validatorMock.Setup(v => v.Validate(It.IsAny())) + .Returns(new ValidationResult()); + + var commandHandler = + new UpdateUserCommandHandler(contextMock.Object, session, new FakeCache(), validatorMock.Object); // Act - var result = await commandHandler.Handle(new UpdateUserCommand(), CancellationToken.None) as ErrorResult; + var result = await commandHandler.Handle(new UpdateUserCommand(), CancellationToken.None); // Assert - result!.Errors - .Count().Should().Be(1); - - result.Errors.First() - .Should().Be(ApplicationErrors.USER_DO_NOT_EXIST); + result.Error!.Messages.Length.Should().Be(1); + result.Error.Messages[0] + .Should() + .Be(ApplicationErrors.USER_DO_NOT_EXIST); } } \ No newline at end of file diff --git a/test/UnitTests/Services/AuthServiceTests.cs b/test/UnitTests/Services/AuthServiceTests.cs index 63a744ba..84f61969 100644 --- a/test/UnitTests/Services/AuthServiceTests.cs +++ b/test/UnitTests/Services/AuthServiceTests.cs @@ -2,7 +2,6 @@ using System.Runtime.CompilerServices; using Moq; using ReflectionMagic; -using SiteWatcher.Application.Interfaces; using SiteWatcher.Domain.Authentication; using SiteWatcher.Domain.Common.Constants; using SiteWatcher.Domain.Common.Services;