From b36aaff112cbf8e245dff11f74b8e64d80e32986 Mon Sep 17 00:00:00 2001 From: Alexander Konietzko Date: Fri, 17 Mar 2023 08:44:37 +0100 Subject: [PATCH 01/10] Add User auth --- .../CleanArchitecture.Api.csproj | 2 + .../Controllers/UserController.cs | 11 +++-- CleanArchitecture.Api/Program.cs | 36 +++++++++++++++ .../appsettings.Development.json | 8 ++++ CleanArchitecture.Api/appsettings.json | 5 ++ .../viewmodels/Users/CreateUserViewModel.cs | 3 +- .../viewmodels/Users/UserViewModel.cs | 5 +- CleanArchitecture.Domain/ApiUser.cs | 46 +++++++++++++++++++ .../CleanArchitecture.Domain.csproj | 2 + .../Users/CreateUser/CreateUserCommand.cs | 7 ++- .../CreateUser/CreateUserCommandHandler.cs | 8 +++- .../DeleteUser/DeleteUserCommandHandler.cs | 13 +++++- .../Users/UpdateUser/UpdateUserCommand.cs | 8 +++- .../UpdateUser/UpdateUserCommandHandler.cs | 23 ++++++++-- CleanArchitecture.Domain/Entities/User.cs | 35 ++++++++++++-- CleanArchitecture.Domain/Enums/UserRole.cs | 7 +++ .../Extensions/ServiceCollectionExtension.cs | 9 ++++ CleanArchitecture.Domain/Interfaces/IUser.cs | 12 +++++ 18 files changed, 221 insertions(+), 19 deletions(-) create mode 100644 CleanArchitecture.Domain/ApiUser.cs create mode 100644 CleanArchitecture.Domain/Enums/UserRole.cs create mode 100644 CleanArchitecture.Domain/Interfaces/IUser.cs diff --git a/CleanArchitecture.Api/CleanArchitecture.Api.csproj b/CleanArchitecture.Api/CleanArchitecture.Api.csproj index 66b0cf6..7b9dbb2 100644 --- a/CleanArchitecture.Api/CleanArchitecture.Api.csproj +++ b/CleanArchitecture.Api/CleanArchitecture.Api.csproj @@ -3,9 +3,11 @@ net7.0 enable + 64377c40-44d6-4989-9662-5d778f8b3b92 + diff --git a/CleanArchitecture.Api/Controllers/UserController.cs b/CleanArchitecture.Api/Controllers/UserController.cs index 5d6cfc6..ff9aa64 100644 --- a/CleanArchitecture.Api/Controllers/UserController.cs +++ b/CleanArchitecture.Api/Controllers/UserController.cs @@ -4,6 +4,7 @@ using CleanArchitecture.Application.Interfaces; using CleanArchitecture.Application.ViewModels.Users; using CleanArchitecture.Domain.Notifications; using MediatR; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; namespace CleanArchitecture.Api.Controllers; @@ -21,13 +22,15 @@ public class UserController : ApiController _userService = userService; } + [Authorize] [HttpGet] public async Task GetAllUsersAsync() { var users = await _userService.GetAllUsersAsync(); return Response(users); } - + + [Authorize] [HttpGet("{id}")] public async Task GetUserByIdAsync( [FromRoute] Guid id, @@ -43,14 +46,16 @@ public class UserController : ApiController var userId = await _userService.CreateUserAsync(viewModel); return Response(userId); } - + + [Authorize] [HttpDelete("{id}")] public async Task DeleteUserAsync([FromRoute] Guid id) { await _userService.DeleteUserAsync(id); return Response(id); } - + + [Authorize] [HttpPut] public async Task UpdateUserAsync([FromBody] UpdateUserViewModel viewModel) { diff --git a/CleanArchitecture.Api/Program.cs b/CleanArchitecture.Api/Program.cs index 4aee2bc..87dc4ee 100644 --- a/CleanArchitecture.Api/Program.cs +++ b/CleanArchitecture.Api/Program.cs @@ -1,12 +1,16 @@ +using System.IdentityModel.Tokens.Jwt; +using System.Text; using CleanArchitecture.Application.Extensions; using CleanArchitecture.Domain.Extensions; using CleanArchitecture.gRPC; using CleanArchitecture.Infrastructure.Database; using CleanArchitecture.Infrastructure.Extensions; +using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.Builder; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; +using Microsoft.IdentityModel.Tokens; var builder = WebApplication.CreateBuilder(args); @@ -24,11 +28,23 @@ builder.Services.AddDbContext(options => b => b.MigrationsAssembly("CleanArchitecture.Infrastructure")); }); +builder.Services.AddAuthentication( + options => + { + options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme; + }) + .AddJwtBearer( + jwtOptions => + { + jwtOptions.TokenValidationParameters = CreateTokenValidationParameters(); + }); + builder.Services.AddInfrastructure(); builder.Services.AddQueryHandlers(); builder.Services.AddServices(); builder.Services.AddCommandHandlers(); builder.Services.AddNotificationHandlers(); +builder.Services.AddApiUser(); builder.Services.AddMediatR(cfg => { @@ -42,6 +58,7 @@ app.UseSwaggerUI(); app.UseHttpsRedirection(); +app.UseAuthentication(); app.UseAuthorization(); app.MapControllers(); @@ -57,5 +74,24 @@ using (IServiceScope scope = app.Services.CreateScope()) app.Run(); +TokenValidationParameters CreateTokenValidationParameters() +{ + var result = new TokenValidationParameters + { + ValidateIssuer = true, + ValidateAudience = true, + ValidateLifetime = true, + ValidateIssuerSigningKey = true, + ValidIssuer = builder.Configuration["Auth:Issuer"], + ValidAudience = builder.Configuration["Auth:Audience"], + IssuerSigningKey = new SymmetricSecurityKey( + Encoding.UTF8.GetBytes( + builder.Configuration["Auth:Secret"]!)), + RequireSignedTokens = false + }; + + return result; +} + // Needed for integration tests webapplication factory public partial class Program { } \ No newline at end of file diff --git a/CleanArchitecture.Api/appsettings.Development.json b/CleanArchitecture.Api/appsettings.Development.json index 0c208ae..c183fbb 100644 --- a/CleanArchitecture.Api/appsettings.Development.json +++ b/CleanArchitecture.Api/appsettings.Development.json @@ -4,5 +4,13 @@ "Default": "Information", "Microsoft.AspNetCore": "Warning" } + }, + "ConnectionStrings": { + "DefaultConnection": "Server=localhost;Database=clean-architecture;Trusted_Connection=True;MultipleActiveResultSets=true;TrustServerCertificate=True" + }, + "Auth": { + "Issuer": "CleanArchitectureServer", + "Audience": "CleanArchitectureClient", + "Secret": "sD3v061gf8BxXgmxcHss" } } diff --git a/CleanArchitecture.Api/appsettings.json b/CleanArchitecture.Api/appsettings.json index 26b0156..c481317 100644 --- a/CleanArchitecture.Api/appsettings.json +++ b/CleanArchitecture.Api/appsettings.json @@ -8,5 +8,10 @@ "AllowedHosts": "*", "ConnectionStrings": { "DefaultConnection": "Server=localhost;Database=clean-architecture;Trusted_Connection=True;MultipleActiveResultSets=true;TrustServerCertificate=True" + }, + "Auth": { + "Issuer": "CleanArchitectureServer", + "Audience": "CleanArchitectureClient", + "Secret": "sD3v061gf8BxXgmxcHss" } } diff --git a/CleanArchitecture.Application/viewmodels/Users/CreateUserViewModel.cs b/CleanArchitecture.Application/viewmodels/Users/CreateUserViewModel.cs index c1a01d4..3efbb36 100644 --- a/CleanArchitecture.Application/viewmodels/Users/CreateUserViewModel.cs +++ b/CleanArchitecture.Application/viewmodels/Users/CreateUserViewModel.cs @@ -3,4 +3,5 @@ namespace CleanArchitecture.Application.ViewModels.Users; public sealed record CreateUserViewModel( string Email, string Surname, - string GivenName); \ No newline at end of file + string GivenName, + string Password); \ No newline at end of file diff --git a/CleanArchitecture.Application/viewmodels/Users/UserViewModel.cs b/CleanArchitecture.Application/viewmodels/Users/UserViewModel.cs index 9ecc82f..cc0ac67 100644 --- a/CleanArchitecture.Application/viewmodels/Users/UserViewModel.cs +++ b/CleanArchitecture.Application/viewmodels/Users/UserViewModel.cs @@ -1,5 +1,6 @@ using System; using CleanArchitecture.Domain.Entities; +using CleanArchitecture.Domain.Enums; namespace CleanArchitecture.Application.ViewModels.Users; @@ -9,6 +10,7 @@ public sealed class UserViewModel public string Email { get; set; } = string.Empty; public string GivenName { get; set; } = string.Empty; public string Surname { get; set; } = string.Empty; + public UserRole Role { get; set; } public static UserViewModel FromUser(User user) { @@ -17,7 +19,8 @@ public sealed class UserViewModel Id = user.Id, Email = user.Email, GivenName = user.GivenName, - Surname = user.Surname + Surname = user.Surname, + Role = user.Role }; } } diff --git a/CleanArchitecture.Domain/ApiUser.cs b/CleanArchitecture.Domain/ApiUser.cs new file mode 100644 index 0000000..705bd4e --- /dev/null +++ b/CleanArchitecture.Domain/ApiUser.cs @@ -0,0 +1,46 @@ +using System; +using System.Linq; +using System.Security.Claims; +using CleanArchitecture.Domain.Enums; +using CleanArchitecture.Domain.Interfaces; +using Microsoft.AspNetCore.Http; + +namespace CleanArchitecture.Domain; + +public sealed class ApiUser : IUser +{ + private readonly IHttpContextAccessor _httpContextAccessor; + + public ApiUser(IHttpContextAccessor httpContextAccessor) + { + _httpContextAccessor = httpContextAccessor; + } + + public Guid GetUserId() + { + var claim = _httpContextAccessor.HttpContext?.User.Claims + .FirstOrDefault(x => string.Equals(x.Type, ClaimTypes.NameIdentifier)); + + if (Guid.TryParse(claim?.Value, out var userId)) + { + return userId; + } + + throw new ArgumentException("Could not parse user id to guid"); + } + + public UserRole GetUserRole() + { + var claim = _httpContextAccessor.HttpContext?.User.Claims + .FirstOrDefault(x => string.Equals(x.Type, ClaimTypes.Role)); + + if (Enum.TryParse(claim?.Value, out UserRole userRole)) + { + return userRole; + } + + throw new ArgumentException("Could not parse user role"); + } + + public string Name => _httpContextAccessor.HttpContext?.User.Identity?.Name ?? string.Empty; +} diff --git a/CleanArchitecture.Domain/CleanArchitecture.Domain.csproj b/CleanArchitecture.Domain/CleanArchitecture.Domain.csproj index 8c0626a..496b495 100644 --- a/CleanArchitecture.Domain/CleanArchitecture.Domain.csproj +++ b/CleanArchitecture.Domain/CleanArchitecture.Domain.csproj @@ -6,8 +6,10 @@ + + diff --git a/CleanArchitecture.Domain/Commands/Users/CreateUser/CreateUserCommand.cs b/CleanArchitecture.Domain/Commands/Users/CreateUser/CreateUserCommand.cs index 6106289..7185a16 100644 --- a/CleanArchitecture.Domain/Commands/Users/CreateUser/CreateUserCommand.cs +++ b/CleanArchitecture.Domain/Commands/Users/CreateUser/CreateUserCommand.cs @@ -10,17 +10,20 @@ public sealed class CreateUserCommand : CommandBase public string Email { get; } public string Surname { get; } public string GivenName { get; } - + public string Password { get; } + public CreateUserCommand( Guid userId, string email, string surname, - string givenName) : base(userId) + string givenName, + string password) : base(userId) { UserId = userId; Email = email; Surname = surname; GivenName = givenName; + Password = password; } public override bool IsValid() diff --git a/CleanArchitecture.Domain/Commands/Users/CreateUser/CreateUserCommandHandler.cs b/CleanArchitecture.Domain/Commands/Users/CreateUser/CreateUserCommandHandler.cs index 0ebbd00..234364c 100644 --- a/CleanArchitecture.Domain/Commands/Users/CreateUser/CreateUserCommandHandler.cs +++ b/CleanArchitecture.Domain/Commands/Users/CreateUser/CreateUserCommandHandler.cs @@ -1,12 +1,14 @@ using System.Threading; using System.Threading.Tasks; using CleanArchitecture.Domain.Entities; +using CleanArchitecture.Domain.Enums; using CleanArchitecture.Domain.Errors; using CleanArchitecture.Domain.Events.User; using CleanArchitecture.Domain.Interfaces; using CleanArchitecture.Domain.Interfaces.Repositories; using CleanArchitecture.Domain.Notifications; using MediatR; +using BC = BCrypt.Net.BCrypt; namespace CleanArchitecture.Domain.Commands.Users.CreateUser; @@ -43,11 +45,15 @@ public sealed class CreateUserCommandHandler : CommandHandlerBase, return; } + var passwordHash = BC.HashPassword(request.Password); + var user = new User( request.UserId, request.Email, request.Surname, - request.GivenName); + request.GivenName, + passwordHash, + UserRole.User); _userRepository.Add(user); diff --git a/CleanArchitecture.Domain/Commands/Users/DeleteUser/DeleteUserCommandHandler.cs b/CleanArchitecture.Domain/Commands/Users/DeleteUser/DeleteUserCommandHandler.cs index edf9b0c..e912010 100644 --- a/CleanArchitecture.Domain/Commands/Users/DeleteUser/DeleteUserCommandHandler.cs +++ b/CleanArchitecture.Domain/Commands/Users/DeleteUser/DeleteUserCommandHandler.cs @@ -1,5 +1,6 @@ using System.Threading; using System.Threading.Tasks; +using CleanArchitecture.Domain.Enums; using CleanArchitecture.Domain.Errors; using CleanArchitecture.Domain.Events.User; using CleanArchitecture.Domain.Interfaces; @@ -13,14 +14,17 @@ public sealed class DeleteUserCommandHandler : CommandHandlerBase, IRequestHandler { private readonly IUserRepository _userRepository; - + private readonly IUser _user; + public DeleteUserCommandHandler( IMediatorHandler bus, IUnitOfWork unitOfWork, INotificationHandler notifications, - IUserRepository userRepository) : base(bus, unitOfWork, notifications) + IUserRepository userRepository, + IUser user) : base(bus, unitOfWork, notifications) { _userRepository = userRepository; + _user = user; } public async Task Handle(DeleteUserCommand request, CancellationToken cancellationToken) @@ -43,6 +47,11 @@ public sealed class DeleteUserCommandHandler : CommandHandlerBase, return; } + if (_user.GetUserId() != request.UserId || _user.GetUserRole() != UserRole.Admin) + { + return; + } + _userRepository.Remove(user); if (await CommitAsync()) diff --git a/CleanArchitecture.Domain/Commands/Users/UpdateUser/UpdateUserCommand.cs b/CleanArchitecture.Domain/Commands/Users/UpdateUser/UpdateUserCommand.cs index 44e3617..d561e47 100644 --- a/CleanArchitecture.Domain/Commands/Users/UpdateUser/UpdateUserCommand.cs +++ b/CleanArchitecture.Domain/Commands/Users/UpdateUser/UpdateUserCommand.cs @@ -1,4 +1,5 @@ using System; +using CleanArchitecture.Domain.Enums; namespace CleanArchitecture.Domain.Commands.Users.UpdateUser; @@ -10,17 +11,20 @@ public sealed class UpdateUserCommand : CommandBase public string Email { get; } public string Surname { get; } public string GivenName { get; } - + public UserRole Role { get; } + public UpdateUserCommand( Guid userId, string email, string surname, - string givenName) : base(userId) + string givenName, + UserRole role) : base(userId) { UserId = userId; Email = email; Surname = surname; GivenName = givenName; + Role = role; } public override bool IsValid() diff --git a/CleanArchitecture.Domain/Commands/Users/UpdateUser/UpdateUserCommandHandler.cs b/CleanArchitecture.Domain/Commands/Users/UpdateUser/UpdateUserCommandHandler.cs index a94c5b5..99c6820 100644 --- a/CleanArchitecture.Domain/Commands/Users/UpdateUser/UpdateUserCommandHandler.cs +++ b/CleanArchitecture.Domain/Commands/Users/UpdateUser/UpdateUserCommandHandler.cs @@ -1,5 +1,7 @@ using System.Threading; using System.Threading.Tasks; +using CleanArchitecture.Domain.Entities; +using CleanArchitecture.Domain.Enums; using CleanArchitecture.Domain.Errors; using CleanArchitecture.Domain.Events.User; using CleanArchitecture.Domain.Interfaces; @@ -13,14 +15,17 @@ public sealed class UpdateUserCommandHandler : CommandHandlerBase, IRequestHandler { private readonly IUserRepository _userRepository; - + private readonly IUser _user; + public UpdateUserCommandHandler( IMediatorHandler bus, IUnitOfWork unitOfWork, INotificationHandler notifications, - IUserRepository userRepository) : base(bus, unitOfWork, notifications) + IUserRepository userRepository, + IUser user) : base(bus, unitOfWork, notifications) { _userRepository = userRepository; + _user = user; } public async Task Handle(UpdateUserCommand request, CancellationToken cancellationToken) @@ -41,11 +46,21 @@ public sealed class UpdateUserCommandHandler : CommandHandlerBase, ErrorCodes.ObjectNotFound)); return; } - + + if (_user.GetUserId() != request.UserId || _user.GetUserRole() != UserRole.Admin) + { + return; + } + + if (_user.GetUserRole() == UserRole.Admin) + { + user.SetRole(request.Role); + } + user.SetEmail(request.Email); user.SetSurname(request.Surname); user.SetGivenName(request.GivenName); - + _userRepository.Update(user); if (await CommitAsync()) diff --git a/CleanArchitecture.Domain/Entities/User.cs b/CleanArchitecture.Domain/Entities/User.cs index e2c56e9..2d14858 100644 --- a/CleanArchitecture.Domain/Entities/User.cs +++ b/CleanArchitecture.Domain/Entities/User.cs @@ -1,5 +1,6 @@ using System; using System.Diagnostics.CodeAnalysis; +using CleanArchitecture.Domain.Enums; namespace CleanArchitecture.Domain.Entities; @@ -8,20 +9,26 @@ public class User : Entity public string Email { get; private set; } public string GivenName { get; private set; } public string Surname { get; private set; } + public string Password { get; private set; } + public UserRole Role { get; private set; } public string FullName => $"{Surname}, {GivenName}"; - + public User( Guid id, string email, string surname, - string givenName) : base(id) + string givenName, + string password, + UserRole role) : base(id) { Email = email; GivenName = givenName; Surname = surname; + Password = password; + Role = role; } - + [MemberNotNull(nameof(Email))] public void SetEmail(string email) { @@ -72,4 +79,26 @@ public class User : Entity Surname = surname; } + + [MemberNotNull(nameof(Password))] + public void SetPassword(string password) + { + if (password == null) + { + throw new ArgumentNullException(nameof(password)); + } + + if (password.Length > 100) + { + throw new ArgumentException( + "Password may not be longer than 100 characters"); + } + + Password = password; + } + + public void SetRole(UserRole role) + { + Role = role; + } } \ No newline at end of file diff --git a/CleanArchitecture.Domain/Enums/UserRole.cs b/CleanArchitecture.Domain/Enums/UserRole.cs new file mode 100644 index 0000000..95c9259 --- /dev/null +++ b/CleanArchitecture.Domain/Enums/UserRole.cs @@ -0,0 +1,7 @@ +namespace CleanArchitecture.Domain.Enums; + +public enum UserRole +{ + Admin, + User +} diff --git a/CleanArchitecture.Domain/Extensions/ServiceCollectionExtension.cs b/CleanArchitecture.Domain/Extensions/ServiceCollectionExtension.cs index 64cd05d..f827036 100644 --- a/CleanArchitecture.Domain/Extensions/ServiceCollectionExtension.cs +++ b/CleanArchitecture.Domain/Extensions/ServiceCollectionExtension.cs @@ -3,6 +3,7 @@ using CleanArchitecture.Domain.Commands.Users.DeleteUser; using CleanArchitecture.Domain.Commands.Users.UpdateUser; using CleanArchitecture.Domain.EventHandler; using CleanArchitecture.Domain.Events.User; +using CleanArchitecture.Domain.Interfaces; using MediatR; using Microsoft.Extensions.DependencyInjection; @@ -29,4 +30,12 @@ public static class ServiceCollectionExtension return services; } + + public static IServiceCollection AddApiUser(this IServiceCollection services) + { + // User + services.AddScoped(); + + return services; + } } \ No newline at end of file diff --git a/CleanArchitecture.Domain/Interfaces/IUser.cs b/CleanArchitecture.Domain/Interfaces/IUser.cs new file mode 100644 index 0000000..55e953c --- /dev/null +++ b/CleanArchitecture.Domain/Interfaces/IUser.cs @@ -0,0 +1,12 @@ +using System; +using CleanArchitecture.Domain.Enums; + +namespace CleanArchitecture.Domain.Interfaces; + +public interface IUser +{ + Guid GetUserId(); + UserRole GetUserRole(); + + string Name { get; } +} From 5214c7c4ed202819588e2d4833072eee8a771855 Mon Sep 17 00:00:00 2001 From: alex289 Date: Sun, 19 Mar 2023 18:26:45 +0100 Subject: [PATCH 02/10] Fix tests --- .../Queries/Users/GetAllUsersTestFixture.cs | 9 +++++-- .../Queries/Users/GetUserByIdTestFixture.cs | 9 +++++-- .../Services/UserService.cs | 6 +++-- .../CreateUserCommandHandlerTests.cs | 6 +++-- .../CreateUserCommandTestFixture.cs | 5 +++- .../CreateUserCommandValidationTests.cs | 6 +++-- .../DeleteUserCommandTestFixture.cs | 8 ++++-- .../UpdateUserCommandHandlerTests.cs | 7 ++++-- .../UpdateUserCommandTestFixture.cs | 8 ++++-- .../UpdateUserCommandValidationTests.cs | 7 ++++-- .../CommandHandlerFixtureBase.cs | 6 +++++ .../DeleteUser/DeleteUserCommandHandler.cs | 8 +++++- .../UpdateUser/UpdateUserCommandHandler.cs | 8 +++++- CleanArchitecture.Domain/Errors/ErrorCodes.cs | 1 + .../Controller/UserControllerTests.cs | 10 ++++++-- .../Fixtures/UserTestsFixture.cs | 25 ++++++++++++++++--- 16 files changed, 103 insertions(+), 26 deletions(-) diff --git a/CleanArchitecture.Application.Tests/Fixtures/Queries/Users/GetAllUsersTestFixture.cs b/CleanArchitecture.Application.Tests/Fixtures/Queries/Users/GetAllUsersTestFixture.cs index 9e22915..e53901c 100644 --- a/CleanArchitecture.Application.Tests/Fixtures/Queries/Users/GetAllUsersTestFixture.cs +++ b/CleanArchitecture.Application.Tests/Fixtures/Queries/Users/GetAllUsersTestFixture.cs @@ -2,6 +2,7 @@ using System; using System.Linq; using CleanArchitecture.Application.Queries.Users.GetAll; using CleanArchitecture.Domain.Entities; +using CleanArchitecture.Domain.Enums; using CleanArchitecture.Domain.Interfaces.Repositories; using MockQueryable.Moq; using Moq; @@ -28,7 +29,9 @@ public sealed class GetAllUsersTestFixture : QueryHandlerBaseFixture ExistingUserId, "max@mustermann.com", "Max", - "Mustermann")); + "Mustermann", + "Password", + UserRole.User)); var query = new[] { user.Object }.AsQueryable().BuildMock(); @@ -44,7 +47,9 @@ public sealed class GetAllUsersTestFixture : QueryHandlerBaseFixture ExistingUserId, "max@mustermann.com", "Max", - "Mustermann")); + "Mustermann", + "Password", + UserRole.User)); user.Object.Delete(); diff --git a/CleanArchitecture.Application.Tests/Fixtures/Queries/Users/GetUserByIdTestFixture.cs b/CleanArchitecture.Application.Tests/Fixtures/Queries/Users/GetUserByIdTestFixture.cs index 404f699..b7c0201 100644 --- a/CleanArchitecture.Application.Tests/Fixtures/Queries/Users/GetUserByIdTestFixture.cs +++ b/CleanArchitecture.Application.Tests/Fixtures/Queries/Users/GetUserByIdTestFixture.cs @@ -2,6 +2,7 @@ using System; using System.Linq; using CleanArchitecture.Application.Queries.Users.GetUserById; using CleanArchitecture.Domain.Entities; +using CleanArchitecture.Domain.Enums; using CleanArchitecture.Domain.Interfaces.Repositories; using MockQueryable.Moq; using Moq; @@ -28,7 +29,9 @@ public sealed class GetUserByIdTestFixture : QueryHandlerBaseFixture ExistingUserId, "max@mustermann.com", "Max", - "Mustermann")); + "Mustermann", + "Password", + UserRole.User)); var query = new[] { user.Object }.AsQueryable().BuildMock(); @@ -44,7 +47,9 @@ public sealed class GetUserByIdTestFixture : QueryHandlerBaseFixture ExistingUserId, "max@mustermann.com", "Max", - "Mustermann")); + "Mustermann", + "Password", + UserRole.User)); user.Object.Delete(); diff --git a/CleanArchitecture.Application/Services/UserService.cs b/CleanArchitecture.Application/Services/UserService.cs index dd91d4f..4204870 100644 --- a/CleanArchitecture.Application/Services/UserService.cs +++ b/CleanArchitecture.Application/Services/UserService.cs @@ -39,7 +39,8 @@ public sealed class UserService : IUserService userId, user.Email, user.Surname, - user.GivenName)); + user.GivenName, + user.Password)); return userId; } @@ -50,7 +51,8 @@ public sealed class UserService : IUserService user.Id, user.Email, user.Surname, - user.GivenName)); + user.GivenName, + user.Role)); } public async Task DeleteUserAsync(Guid userId) diff --git a/CleanArchitecture.Domain.Tests/CommandHandler/User/CreateUser/CreateUserCommandHandlerTests.cs b/CleanArchitecture.Domain.Tests/CommandHandler/User/CreateUser/CreateUserCommandHandlerTests.cs index 7d5109f..c4072be 100644 --- a/CleanArchitecture.Domain.Tests/CommandHandler/User/CreateUser/CreateUserCommandHandlerTests.cs +++ b/CleanArchitecture.Domain.Tests/CommandHandler/User/CreateUser/CreateUserCommandHandlerTests.cs @@ -19,7 +19,8 @@ public sealed class CreateUserCommandHandlerTests Guid.NewGuid(), "test@email.com", "Test", - "Email"); + "Email", + "SomePassword"); _fixture.CommandHandler.Handle(command, default).Wait(); @@ -38,7 +39,8 @@ public sealed class CreateUserCommandHandlerTests user.Id, "test@email.com", "Test", - "Email"); + "Email", + "SomePassword"); _fixture.CommandHandler.Handle(command, default).Wait(); diff --git a/CleanArchitecture.Domain.Tests/CommandHandler/User/CreateUser/CreateUserCommandTestFixture.cs b/CleanArchitecture.Domain.Tests/CommandHandler/User/CreateUser/CreateUserCommandTestFixture.cs index 2f30515..c7ba05a 100644 --- a/CleanArchitecture.Domain.Tests/CommandHandler/User/CreateUser/CreateUserCommandTestFixture.cs +++ b/CleanArchitecture.Domain.Tests/CommandHandler/User/CreateUser/CreateUserCommandTestFixture.cs @@ -1,5 +1,6 @@ using System; using CleanArchitecture.Domain.Commands.Users.CreateUser; +using CleanArchitecture.Domain.Enums; using CleanArchitecture.Domain.Interfaces.Repositories; using Moq; @@ -27,7 +28,9 @@ public sealed class CreateUserCommandTestFixture : CommandHandlerFixtureBase Guid.NewGuid(), "max@mustermann.com", "Max", - "Mustermann"); + "Mustermann", + "Password", + UserRole.User); UserRepository .Setup(x => x.GetByIdAsync(It.Is(y => y == user.Id))) diff --git a/CleanArchitecture.Domain.Tests/CommandHandler/User/CreateUser/CreateUserCommandValidationTests.cs b/CleanArchitecture.Domain.Tests/CommandHandler/User/CreateUser/CreateUserCommandValidationTests.cs index a6c54e4..2b92763 100644 --- a/CleanArchitecture.Domain.Tests/CommandHandler/User/CreateUser/CreateUserCommandValidationTests.cs +++ b/CleanArchitecture.Domain.Tests/CommandHandler/User/CreateUser/CreateUserCommandValidationTests.cs @@ -112,10 +112,12 @@ public sealed class CreateUserCommandValidationTests : Guid? userId = null, string? email = null, string? surName = null, - string? givenName = null) => + string? givenName = null, + string? password = null) => new ( userId ?? Guid.NewGuid(), email ?? "test@email.com", surName ?? "test", - givenName ?? "email"); + givenName ?? "email", + password ?? "some password"); } \ No newline at end of file diff --git a/CleanArchitecture.Domain.Tests/CommandHandler/User/DeleteUser/DeleteUserCommandTestFixture.cs b/CleanArchitecture.Domain.Tests/CommandHandler/User/DeleteUser/DeleteUserCommandTestFixture.cs index 19f7761..149480d 100644 --- a/CleanArchitecture.Domain.Tests/CommandHandler/User/DeleteUser/DeleteUserCommandTestFixture.cs +++ b/CleanArchitecture.Domain.Tests/CommandHandler/User/DeleteUser/DeleteUserCommandTestFixture.cs @@ -1,5 +1,6 @@ using System; using CleanArchitecture.Domain.Commands.Users.DeleteUser; +using CleanArchitecture.Domain.Enums; using CleanArchitecture.Domain.Interfaces.Repositories; using Moq; @@ -18,7 +19,8 @@ public sealed class DeleteUserCommandTestFixture : CommandHandlerFixtureBase Bus.Object, UnitOfWork.Object, NotificationHandler.Object, - UserRepository.Object); + UserRepository.Object, + User.Object); } public Entities.User SetupUser() @@ -27,7 +29,9 @@ public sealed class DeleteUserCommandTestFixture : CommandHandlerFixtureBase Guid.NewGuid(), "max@mustermann.com", "Max", - "Mustermann"); + "Mustermann", + "Password", + UserRole.User); UserRepository .Setup(x => x.GetByIdAsync(It.Is(y => y == user.Id))) diff --git a/CleanArchitecture.Domain.Tests/CommandHandler/User/UpdateUser/UpdateUserCommandHandlerTests.cs b/CleanArchitecture.Domain.Tests/CommandHandler/User/UpdateUser/UpdateUserCommandHandlerTests.cs index bdd53e6..3fcb757 100644 --- a/CleanArchitecture.Domain.Tests/CommandHandler/User/UpdateUser/UpdateUserCommandHandlerTests.cs +++ b/CleanArchitecture.Domain.Tests/CommandHandler/User/UpdateUser/UpdateUserCommandHandlerTests.cs @@ -1,6 +1,7 @@ using System; using System.Threading.Tasks; using CleanArchitecture.Domain.Commands.Users.UpdateUser; +using CleanArchitecture.Domain.Enums; using CleanArchitecture.Domain.Errors; using CleanArchitecture.Domain.Events.User; using Xunit; @@ -20,7 +21,8 @@ public sealed class UpdateUserCommandHandlerTests user.Id, "test@email.com", "Test", - "Email"); + "Email", + UserRole.User); await _fixture.CommandHandler.Handle(command, default); @@ -39,7 +41,8 @@ public sealed class UpdateUserCommandHandlerTests Guid.NewGuid(), "test@email.com", "Test", - "Email"); + "Email", + UserRole.User); await _fixture.CommandHandler.Handle(command, default); diff --git a/CleanArchitecture.Domain.Tests/CommandHandler/User/UpdateUser/UpdateUserCommandTestFixture.cs b/CleanArchitecture.Domain.Tests/CommandHandler/User/UpdateUser/UpdateUserCommandTestFixture.cs index 1b63128..bb4e581 100644 --- a/CleanArchitecture.Domain.Tests/CommandHandler/User/UpdateUser/UpdateUserCommandTestFixture.cs +++ b/CleanArchitecture.Domain.Tests/CommandHandler/User/UpdateUser/UpdateUserCommandTestFixture.cs @@ -1,5 +1,6 @@ using System; using CleanArchitecture.Domain.Commands.Users.UpdateUser; +using CleanArchitecture.Domain.Enums; using CleanArchitecture.Domain.Interfaces.Repositories; using Moq; @@ -18,7 +19,8 @@ public sealed class UpdateUserCommandTestFixture : CommandHandlerFixtureBase Bus.Object, UnitOfWork.Object, NotificationHandler.Object, - UserRepository.Object); + UserRepository.Object, + User.Object); } public Entities.User SetupUser() @@ -27,7 +29,9 @@ public sealed class UpdateUserCommandTestFixture : CommandHandlerFixtureBase Guid.NewGuid(), "max@mustermann.com", "Max", - "Mustermann"); + "Mustermann", + "Password", + UserRole.User); UserRepository .Setup(x => x.GetByIdAsync(It.Is(y => y == user.Id))) diff --git a/CleanArchitecture.Domain.Tests/CommandHandler/User/UpdateUser/UpdateUserCommandValidationTests.cs b/CleanArchitecture.Domain.Tests/CommandHandler/User/UpdateUser/UpdateUserCommandValidationTests.cs index e593959..0c80928 100644 --- a/CleanArchitecture.Domain.Tests/CommandHandler/User/UpdateUser/UpdateUserCommandValidationTests.cs +++ b/CleanArchitecture.Domain.Tests/CommandHandler/User/UpdateUser/UpdateUserCommandValidationTests.cs @@ -1,5 +1,6 @@ using System; using CleanArchitecture.Domain.Commands.Users.UpdateUser; +using CleanArchitecture.Domain.Enums; using CleanArchitecture.Domain.Errors; using Xunit; @@ -112,10 +113,12 @@ public sealed class UpdateUserCommandValidationTests : Guid? userId = null, string? email = null, string? surName = null, - string? givenName = null) => + string? givenName = null, + UserRole? role = null) => new ( userId ?? Guid.NewGuid(), email ?? "test@email.com", surName ?? "test", - givenName ?? "email"); + givenName ?? "email", + role ?? UserRole.User); } \ No newline at end of file diff --git a/CleanArchitecture.Domain.Tests/CommandHandlerFixtureBase.cs b/CleanArchitecture.Domain.Tests/CommandHandlerFixtureBase.cs index 01964a0..a8e464c 100644 --- a/CleanArchitecture.Domain.Tests/CommandHandlerFixtureBase.cs +++ b/CleanArchitecture.Domain.Tests/CommandHandlerFixtureBase.cs @@ -1,5 +1,6 @@ using System; using System.Linq.Expressions; +using CleanArchitecture.Domain.Enums; using CleanArchitecture.Domain.Interfaces; using CleanArchitecture.Domain.Notifications; using Moq; @@ -11,12 +12,17 @@ public class CommandHandlerFixtureBase protected Mock Bus { get; } protected Mock UnitOfWork { get; } protected Mock NotificationHandler { get; } + protected Mock User { get; } protected CommandHandlerFixtureBase() { Bus = new Mock(); UnitOfWork = new Mock(); NotificationHandler = new Mock(); + User = new Mock(); + + User.Setup(x => x.GetUserId()).Returns(Guid.NewGuid()); + User.Setup(x => x.GetUserRole()).Returns(UserRole.Admin); UnitOfWork.Setup(unit => unit.CommitAsync()).ReturnsAsync(true); } diff --git a/CleanArchitecture.Domain/Commands/Users/DeleteUser/DeleteUserCommandHandler.cs b/CleanArchitecture.Domain/Commands/Users/DeleteUser/DeleteUserCommandHandler.cs index e912010..f06cf41 100644 --- a/CleanArchitecture.Domain/Commands/Users/DeleteUser/DeleteUserCommandHandler.cs +++ b/CleanArchitecture.Domain/Commands/Users/DeleteUser/DeleteUserCommandHandler.cs @@ -47,8 +47,14 @@ public sealed class DeleteUserCommandHandler : CommandHandlerBase, return; } - if (_user.GetUserId() != request.UserId || _user.GetUserRole() != UserRole.Admin) + if (_user.GetUserId() != request.UserId && _user.GetUserRole() != UserRole.Admin) { + await NotifyAsync( + new DomainNotification( + request.MessageType, + $"No permission to delete user {request.UserId}", + ErrorCodes.Unauthorized)); + return; } diff --git a/CleanArchitecture.Domain/Commands/Users/UpdateUser/UpdateUserCommandHandler.cs b/CleanArchitecture.Domain/Commands/Users/UpdateUser/UpdateUserCommandHandler.cs index 99c6820..a5aff0e 100644 --- a/CleanArchitecture.Domain/Commands/Users/UpdateUser/UpdateUserCommandHandler.cs +++ b/CleanArchitecture.Domain/Commands/Users/UpdateUser/UpdateUserCommandHandler.cs @@ -47,8 +47,14 @@ public sealed class UpdateUserCommandHandler : CommandHandlerBase, return; } - if (_user.GetUserId() != request.UserId || _user.GetUserRole() != UserRole.Admin) + if (_user.GetUserId() != request.UserId && _user.GetUserRole() != UserRole.Admin) { + await NotifyAsync( + new DomainNotification( + request.MessageType, + $"No permission to update user {request.UserId}", + ErrorCodes.Unauthorized)); + return; } diff --git a/CleanArchitecture.Domain/Errors/ErrorCodes.cs b/CleanArchitecture.Domain/Errors/ErrorCodes.cs index 056c188..427c50d 100644 --- a/CleanArchitecture.Domain/Errors/ErrorCodes.cs +++ b/CleanArchitecture.Domain/Errors/ErrorCodes.cs @@ -4,4 +4,5 @@ public static class ErrorCodes { public const string CommitFailed = "COMMIT_FAILED"; public const string ObjectNotFound = "OBJECT_NOT_FOUND"; + public const string Unauthorized = "UNAUTHORIZED"; } \ No newline at end of file diff --git a/CleanArchitecture.IntegrationTests/Controller/UserControllerTests.cs b/CleanArchitecture.IntegrationTests/Controller/UserControllerTests.cs index fd8791b..20d92be 100644 --- a/CleanArchitecture.IntegrationTests/Controller/UserControllerTests.cs +++ b/CleanArchitecture.IntegrationTests/Controller/UserControllerTests.cs @@ -4,6 +4,7 @@ using System.Linq; using System.Net; using System.Threading.Tasks; using CleanArchitecture.Application.ViewModels.Users; +using CleanArchitecture.Domain.Enums; using CleanArchitecture.IntegrationTests.Extensions; using CleanArchitecture.IntegrationTests.Fixtures; using FluentAssertions; @@ -42,7 +43,11 @@ public sealed class UserControllerTests : IClassFixture [Fact, Priority(5)] public async Task Should_Create_User() { - var user = new CreateUserViewModel("test@email.com", "Test", "Email"); + var user = new CreateUserViewModel( + "test@email.com", + "Test", + "Email", + "Password"); var response = await _fixture.ServerClient.PostAsJsonAsync("user", user); @@ -81,7 +86,8 @@ public sealed class UserControllerTests : IClassFixture _fixture.CreatedUserId, "newtest@email.com", "NewTest", - "NewEmail"); + "NewEmail", + UserRole.User); var response = await _fixture.ServerClient.PutAsJsonAsync("user", user); diff --git a/CleanArchitecture.gRPC.Tests/Fixtures/UserTestsFixture.cs b/CleanArchitecture.gRPC.Tests/Fixtures/UserTestsFixture.cs index 63dd2bf..9cac355 100644 --- a/CleanArchitecture.gRPC.Tests/Fixtures/UserTestsFixture.cs +++ b/CleanArchitecture.gRPC.Tests/Fixtures/UserTestsFixture.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; using CleanArchitecture.Domain.Entities; +using CleanArchitecture.Domain.Enums; using CleanArchitecture.Domain.Interfaces.Repositories; using MockQueryable.Moq; using Moq; @@ -20,9 +21,27 @@ public sealed class UserTestsFixture { ExistingUsers = new List() { - new (Guid.NewGuid(), "test@test.de", "Test First Name", "Test Last Name"), - new (Guid.NewGuid(), "email@Email.de", "Email First Name", "Email Last Name"), - new (Guid.NewGuid(), "user@user.de", "User First Name", "User Last Name"), + new ( + Guid.NewGuid(), + "test@test.de", + "Test First Name", + "Test Last Name", + "Test Password", + UserRole.User), + new ( + Guid.NewGuid(), + "email@Email.de", + "Email First Name", + "Email Last Name", + "Email Password", + UserRole.Admin), + new ( + Guid.NewGuid(), + "user@user.de", + "User First Name", + "User Last Name", + "User Password", + UserRole.User), }; var queryable = ExistingUsers.AsQueryable().BuildMock(); From d89e44b8dfec2305c01c3e5ce36d4852a37afbb5 Mon Sep 17 00:00:00 2001 From: alex289 Date: Mon, 20 Mar 2023 21:33:56 +0100 Subject: [PATCH 03/10] Refactor the user endpoints --- .../Controllers/ApiController.cs | 5 +++ .../Controllers/UserController.cs | 8 +++++ CleanArchitecture.Api/Program.cs | 7 +++- .../Interfaces/IUserService.cs | 1 + .../Services/UserService.cs | 9 +++++- .../viewmodels/Users/UpdateUserViewModel.cs | 4 ++- .../ChangePassword/ChangePasswordCommand.cs | 6 ++++ .../CreateUser/CreateUserCommandHandler.cs | 12 +++++++ .../CreateUser/CreateUserCommandValidation.cs | 8 +++++ .../DeleteUser/DeleteUserCommandHandler.cs | 2 +- .../Users/LoginUser/LoginUserCommand.cs | 6 ++++ .../UpdateUser/UpdateUserCommandHandler.cs | 2 +- .../Errors/DomainErrorCodes.cs | 9 ++++++ CleanArchitecture.Domain/Errors/ErrorCodes.cs | 2 +- .../Extensions/Validation/CustomValidator.cs | 32 +++++++++++++++++++ .../Settings/TokenSettings.cs | 8 +++++ .../Configurations/UserConfiguration.cs | 16 ++++++++++ 17 files changed, 131 insertions(+), 6 deletions(-) create mode 100644 CleanArchitecture.Domain/Commands/Users/ChangePassword/ChangePasswordCommand.cs create mode 100644 CleanArchitecture.Domain/Commands/Users/LoginUser/LoginUserCommand.cs create mode 100644 CleanArchitecture.Domain/Extensions/Validation/CustomValidator.cs create mode 100644 CleanArchitecture.Domain/Settings/TokenSettings.cs diff --git a/CleanArchitecture.Api/Controllers/ApiController.cs b/CleanArchitecture.Api/Controllers/ApiController.cs index ddb8cd0..a191d22 100644 --- a/CleanArchitecture.Api/Controllers/ApiController.cs +++ b/CleanArchitecture.Api/Controllers/ApiController.cs @@ -62,6 +62,11 @@ public class ApiController : ControllerBase { return HttpStatusCode.NotFound; } + + if (_notifications.GetNotifications().Any(n => n.Code == ErrorCodes.InsufficientPermissions)) + { + return HttpStatusCode.Forbidden; + } return HttpStatusCode.BadRequest; } diff --git a/CleanArchitecture.Api/Controllers/UserController.cs b/CleanArchitecture.Api/Controllers/UserController.cs index ff9aa64..ba98cac 100644 --- a/CleanArchitecture.Api/Controllers/UserController.cs +++ b/CleanArchitecture.Api/Controllers/UserController.cs @@ -40,6 +40,14 @@ public class UserController : ApiController return Response(user); } + [Authorize] + [HttpGet("me")] + public async Task GetCurrentUserAsync() + { + var user = await _userService.GetCurrentUserAsync(); + return Response(user); + } + [HttpPost] public async Task CreateUserAsync([FromBody] CreateUserViewModel viewModel) { diff --git a/CleanArchitecture.Api/Program.cs b/CleanArchitecture.Api/Program.cs index 87dc4ee..5288d7d 100644 --- a/CleanArchitecture.Api/Program.cs +++ b/CleanArchitecture.Api/Program.cs @@ -1,7 +1,7 @@ -using System.IdentityModel.Tokens.Jwt; using System.Text; using CleanArchitecture.Application.Extensions; using CleanArchitecture.Domain.Extensions; +using CleanArchitecture.Domain.Settings; using CleanArchitecture.gRPC; using CleanArchitecture.Infrastructure.Database; using CleanArchitecture.Infrastructure.Extensions; @@ -46,6 +46,11 @@ builder.Services.AddCommandHandlers(); builder.Services.AddNotificationHandlers(); builder.Services.AddApiUser(); +builder.Services + .AddOptions() + .Bind(builder.Configuration.GetSection("Auth")) + .ValidateOnStart(); + builder.Services.AddMediatR(cfg => { cfg.RegisterServicesFromAssemblies(typeof(Program).Assembly); diff --git a/CleanArchitecture.Application/Interfaces/IUserService.cs b/CleanArchitecture.Application/Interfaces/IUserService.cs index 5f65254..85335d0 100644 --- a/CleanArchitecture.Application/Interfaces/IUserService.cs +++ b/CleanArchitecture.Application/Interfaces/IUserService.cs @@ -8,6 +8,7 @@ namespace CleanArchitecture.Application.Interfaces; public interface IUserService { public Task GetUserByUserIdAsync(Guid userId, bool isDeleted); + public Task GetCurrentUserAsync(); public Task> GetAllUsersAsync(); public Task CreateUserAsync(CreateUserViewModel user); public Task UpdateUserAsync(UpdateUserViewModel user); diff --git a/CleanArchitecture.Application/Services/UserService.cs b/CleanArchitecture.Application/Services/UserService.cs index 4204870..cd622f5 100644 --- a/CleanArchitecture.Application/Services/UserService.cs +++ b/CleanArchitecture.Application/Services/UserService.cs @@ -15,16 +15,23 @@ namespace CleanArchitecture.Application.Services; public sealed class UserService : IUserService { private readonly IMediatorHandler _bus; + private readonly IUser _user; - public UserService(IMediatorHandler bus) + public UserService(IMediatorHandler bus, IUser user) { _bus = bus; + _user = user; } public async Task GetUserByUserIdAsync(Guid userId, bool isDeleted) { return await _bus.QueryAsync(new GetUserByIdQuery(userId, isDeleted)); } + + public async Task GetCurrentUserAsync() + { + return await _bus.QueryAsync(new GetUserByIdQuery(_user.GetUserId(), false)); + } public async Task> GetAllUsersAsync() { diff --git a/CleanArchitecture.Application/viewmodels/Users/UpdateUserViewModel.cs b/CleanArchitecture.Application/viewmodels/Users/UpdateUserViewModel.cs index e64fd7d..137d30f 100644 --- a/CleanArchitecture.Application/viewmodels/Users/UpdateUserViewModel.cs +++ b/CleanArchitecture.Application/viewmodels/Users/UpdateUserViewModel.cs @@ -1,4 +1,5 @@ using System; +using CleanArchitecture.Domain.Enums; namespace CleanArchitecture.Application.ViewModels.Users; @@ -6,4 +7,5 @@ public sealed record UpdateUserViewModel( Guid Id, string Email, string Surname, - string GivenName); \ No newline at end of file + string GivenName, + UserRole Role); \ No newline at end of file diff --git a/CleanArchitecture.Domain/Commands/Users/ChangePassword/ChangePasswordCommand.cs b/CleanArchitecture.Domain/Commands/Users/ChangePassword/ChangePasswordCommand.cs new file mode 100644 index 0000000..45f6aca --- /dev/null +++ b/CleanArchitecture.Domain/Commands/Users/ChangePassword/ChangePasswordCommand.cs @@ -0,0 +1,6 @@ +namespace CleanArchitecture.Domain.Commands.Users.ChangePassword; + +public sealed class ChangePasswordCommand +{ + +} \ No newline at end of file diff --git a/CleanArchitecture.Domain/Commands/Users/CreateUser/CreateUserCommandHandler.cs b/CleanArchitecture.Domain/Commands/Users/CreateUser/CreateUserCommandHandler.cs index 234364c..2f7f511 100644 --- a/CleanArchitecture.Domain/Commands/Users/CreateUser/CreateUserCommandHandler.cs +++ b/CleanArchitecture.Domain/Commands/Users/CreateUser/CreateUserCommandHandler.cs @@ -44,6 +44,18 @@ public sealed class CreateUserCommandHandler : CommandHandlerBase, DomainErrorCodes.UserAlreadyExists)); return; } + + existingUser = await _userRepository.GetByEmailAsync(request.Email); + + if (existingUser != null) + { + await _bus.RaiseEventAsync( + new DomainNotification( + request.MessageType, + $"There is already a User with Email {request.Email}", + DomainErrorCodes.UserAlreadyExists)); + return; + } var passwordHash = BC.HashPassword(request.Password); diff --git a/CleanArchitecture.Domain/Commands/Users/CreateUser/CreateUserCommandValidation.cs b/CleanArchitecture.Domain/Commands/Users/CreateUser/CreateUserCommandValidation.cs index 3c14aef..e1b7ed9 100644 --- a/CleanArchitecture.Domain/Commands/Users/CreateUser/CreateUserCommandValidation.cs +++ b/CleanArchitecture.Domain/Commands/Users/CreateUser/CreateUserCommandValidation.cs @@ -1,4 +1,5 @@ using CleanArchitecture.Domain.Errors; +using CleanArchitecture.Domain.Extensions.Validation; using FluentValidation; namespace CleanArchitecture.Domain.Commands.Users.CreateUser; @@ -11,6 +12,7 @@ public sealed class CreateUserCommandValidation : AbstractValidator cmd.Password) + .Password(); + } } \ No newline at end of file diff --git a/CleanArchitecture.Domain/Commands/Users/DeleteUser/DeleteUserCommandHandler.cs b/CleanArchitecture.Domain/Commands/Users/DeleteUser/DeleteUserCommandHandler.cs index f06cf41..366c240 100644 --- a/CleanArchitecture.Domain/Commands/Users/DeleteUser/DeleteUserCommandHandler.cs +++ b/CleanArchitecture.Domain/Commands/Users/DeleteUser/DeleteUserCommandHandler.cs @@ -53,7 +53,7 @@ public sealed class DeleteUserCommandHandler : CommandHandlerBase, new DomainNotification( request.MessageType, $"No permission to delete user {request.UserId}", - ErrorCodes.Unauthorized)); + ErrorCodes.InsufficientPermissions)); return; } diff --git a/CleanArchitecture.Domain/Commands/Users/LoginUser/LoginUserCommand.cs b/CleanArchitecture.Domain/Commands/Users/LoginUser/LoginUserCommand.cs new file mode 100644 index 0000000..19f2c9b --- /dev/null +++ b/CleanArchitecture.Domain/Commands/Users/LoginUser/LoginUserCommand.cs @@ -0,0 +1,6 @@ +namespace CleanArchitecture.Domain.Commands.Users.LoginUser; + +public sealed class LoginCommand +{ + +} \ No newline at end of file diff --git a/CleanArchitecture.Domain/Commands/Users/UpdateUser/UpdateUserCommandHandler.cs b/CleanArchitecture.Domain/Commands/Users/UpdateUser/UpdateUserCommandHandler.cs index a5aff0e..bbf140c 100644 --- a/CleanArchitecture.Domain/Commands/Users/UpdateUser/UpdateUserCommandHandler.cs +++ b/CleanArchitecture.Domain/Commands/Users/UpdateUser/UpdateUserCommandHandler.cs @@ -53,7 +53,7 @@ public sealed class UpdateUserCommandHandler : CommandHandlerBase, new DomainNotification( request.MessageType, $"No permission to update user {request.UserId}", - ErrorCodes.Unauthorized)); + ErrorCodes.InsufficientPermissions)); return; } diff --git a/CleanArchitecture.Domain/Errors/DomainErrorCodes.cs b/CleanArchitecture.Domain/Errors/DomainErrorCodes.cs index 045b455..9121d0b 100644 --- a/CleanArchitecture.Domain/Errors/DomainErrorCodes.cs +++ b/CleanArchitecture.Domain/Errors/DomainErrorCodes.cs @@ -11,6 +11,15 @@ public static class DomainErrorCodes public const string UserGivenNameExceedsMaxLength = "USER_GIVEN_NAME_EXCEEDS_MAX_LENGTH"; public const string UserInvalidEmail = "USER_INVALID_EMAIL"; + // User Password Validation + public const string UserEmptyPassword = "USER_PASSWORD_MAY_NOT_BE_EMPTY"; + public const string UserShortPassword = "USER_PASSWORD_MAY_NOT_BE_SHORTER_THAN_6_CHARACTERS"; + public const string UserLongPassword = "USER_PASSWORD_MAY_NOT_BE_LONGER_THAN_50_CHARACTERS"; + public const string UserUppercaseLetterPassword = "USER_PASSWORD_MUST_CONTAIN_A_UPPERCASE_LETTER"; + public const string UserLowercaseLetterPassword = "USER_PASSWORD_MUST_CONTAIN_A_LOWERCASE_LETTER"; + public const string UserNumberPassword = "USER_PASSWORD_MUST_CONTAIN_A_NUMBER"; + public const string UserSpecialCharPassword = "USER_PASSWORD_MUST_CONTAIN_A_SPECIAL_CHARACTER"; + // User public const string UserAlreadyExists = "USER_ALREADY_EXISTS"; } \ No newline at end of file diff --git a/CleanArchitecture.Domain/Errors/ErrorCodes.cs b/CleanArchitecture.Domain/Errors/ErrorCodes.cs index 427c50d..6cc6fd7 100644 --- a/CleanArchitecture.Domain/Errors/ErrorCodes.cs +++ b/CleanArchitecture.Domain/Errors/ErrorCodes.cs @@ -4,5 +4,5 @@ public static class ErrorCodes { public const string CommitFailed = "COMMIT_FAILED"; public const string ObjectNotFound = "OBJECT_NOT_FOUND"; - public const string Unauthorized = "UNAUTHORIZED"; + public const string InsufficientPermissions = "UNAUTHORIZED"; } \ No newline at end of file diff --git a/CleanArchitecture.Domain/Extensions/Validation/CustomValidator.cs b/CleanArchitecture.Domain/Extensions/Validation/CustomValidator.cs new file mode 100644 index 0000000..ecacf87 --- /dev/null +++ b/CleanArchitecture.Domain/Extensions/Validation/CustomValidator.cs @@ -0,0 +1,32 @@ +using System.Text.RegularExpressions; +using CleanArchitecture.Domain.Errors; +using FluentValidation; + +namespace CleanArchitecture.Domain.Extensions.Validation; + +public static class CustomValidator +{ + public static IRuleBuilderOptions StringMustBeBase64(this IRuleBuilder ruleBuilder) + { + return ruleBuilder.Must(x => IsBase64String(x)); + } + + private static bool IsBase64String(string base64) + { + base64 = base64.Trim(); + return base64.Length % 4 == 0 && Regex.IsMatch(base64, @"^[a-zA-Z0-9\+/]*={0,3}$", RegexOptions.None); + } + + public static IRuleBuilder Password(this IRuleBuilder ruleBuilder, int minLength = 8, int maxLength = 50) + { + var options = ruleBuilder + .NotEmpty().WithErrorCode(DomainErrorCodes.UserEmptyPassword) + .MinimumLength(minLength).WithErrorCode(DomainErrorCodes.UserShortPassword) + .MaximumLength(maxLength).WithErrorCode(DomainErrorCodes.UserLongPassword) + .Matches("[A-Z]").WithErrorCode(DomainErrorCodes.UserUppercaseLetterPassword) + .Matches("[a-z]").WithErrorCode(DomainErrorCodes.UserLowercaseLetterPassword) + .Matches("[0-9]").WithErrorCode(DomainErrorCodes.UserNumberPassword) + .Matches("[^a-zA-Z0-9]").WithErrorCode(DomainErrorCodes.UserSpecialCharPassword); + return options; + } +} \ No newline at end of file diff --git a/CleanArchitecture.Domain/Settings/TokenSettings.cs b/CleanArchitecture.Domain/Settings/TokenSettings.cs new file mode 100644 index 0000000..96cbe01 --- /dev/null +++ b/CleanArchitecture.Domain/Settings/TokenSettings.cs @@ -0,0 +1,8 @@ +namespace CleanArchitecture.Domain.Settings; + +public sealed class TokenSettings +{ + public string Issuer { get; set; } = null!; + public string Audience { get; set; } = null!; + public string Secret { get; set; } = null!; +} \ No newline at end of file diff --git a/CleanArchitecture.Infrastructure/Configurations/UserConfiguration.cs b/CleanArchitecture.Infrastructure/Configurations/UserConfiguration.cs index 2723e12..6fe7426 100644 --- a/CleanArchitecture.Infrastructure/Configurations/UserConfiguration.cs +++ b/CleanArchitecture.Infrastructure/Configurations/UserConfiguration.cs @@ -1,4 +1,6 @@ +using System; using CleanArchitecture.Domain.Entities; +using CleanArchitecture.Domain.Enums; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata.Builders; @@ -22,5 +24,19 @@ public sealed class UserConfiguration : IEntityTypeConfiguration .Property(user => user.Surname) .IsRequired() .HasMaxLength(100); + + builder + .Property(user => user.Password) + .IsRequired() + .HasMaxLength(128); + + builder.HasData(new User( + Guid.NewGuid(), + "admin@email.com", + "Admin", + "User", + // !Password123# + "$2a$12$Blal/uiFIJdYsCLTMUik/egLbfg3XhbnxBC6Sb5IKz2ZYhiU/MzL2", + UserRole.Admin)); } } \ No newline at end of file From 983c63b38ec491a0f1bd0fe624848e2215fea6b0 Mon Sep 17 00:00:00 2001 From: Alexander Konietzko Date: Mon, 20 Mar 2023 22:26:12 +0100 Subject: [PATCH 04/10] Implement new user commands --- .../CleanArchitecture.Api.csproj | 4 + CleanArchitecture.Domain/ApiUser.cs | 13 +++ .../CleanArchitecture.Domain.csproj | 2 + .../ChangePassword/ChangePasswordCommand.cs | 21 +++- .../ChangePasswordCommandHandler.cs | 70 ++++++++++++ .../ChangePasswordCommandValidation.cs | 7 ++ .../Users/LoginUser/LoginUserCommand.cs | 26 ++++- .../LoginUser/LoginUserCommandHandler.cs | 104 ++++++++++++++++++ .../LoginUser/LoginUserCommandValidation.cs | 7 ++ .../Errors/DomainErrorCodes.cs | 1 + ...0204057_AddUserRoleAndPassword.Designer.cs | 82 ++++++++++++++ .../20230320204057_AddUserRoleAndPassword.cs | 52 +++++++++ .../ApplicationDbContextModelSnapshot.cs | 22 +++- CleanArchitecture.sln | 37 +++++-- 14 files changed, 432 insertions(+), 16 deletions(-) create mode 100644 CleanArchitecture.Domain/Commands/Users/ChangePassword/ChangePasswordCommandHandler.cs create mode 100644 CleanArchitecture.Domain/Commands/Users/ChangePassword/ChangePasswordCommandValidation.cs create mode 100644 CleanArchitecture.Domain/Commands/Users/LoginUser/LoginUserCommandHandler.cs create mode 100644 CleanArchitecture.Domain/Commands/Users/LoginUser/LoginUserCommandValidation.cs create mode 100644 CleanArchitecture.Infrastructure/Migrations/20230320204057_AddUserRoleAndPassword.Designer.cs create mode 100644 CleanArchitecture.Infrastructure/Migrations/20230320204057_AddUserRoleAndPassword.cs diff --git a/CleanArchitecture.Api/CleanArchitecture.Api.csproj b/CleanArchitecture.Api/CleanArchitecture.Api.csproj index 7b9dbb2..3ba9cc6 100644 --- a/CleanArchitecture.Api/CleanArchitecture.Api.csproj +++ b/CleanArchitecture.Api/CleanArchitecture.Api.csproj @@ -9,6 +9,10 @@ + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/CleanArchitecture.Domain/ApiUser.cs b/CleanArchitecture.Domain/ApiUser.cs index 705bd4e..c991c79 100644 --- a/CleanArchitecture.Domain/ApiUser.cs +++ b/CleanArchitecture.Domain/ApiUser.cs @@ -29,6 +29,19 @@ public sealed class ApiUser : IUser throw new ArgumentException("Could not parse user id to guid"); } + public string GetUserEmail() + { + var claim = _httpContextAccessor.HttpContext?.User.Claims + .FirstOrDefault(x => string.Equals(x.Type, ClaimTypes.Email)); + + if (!string.IsNullOrWhiteSpace(claim?.Value)) + { + return claim?.Value!; + } + + throw new ArgumentException("Could not parse user email"); + } + public UserRole GetUserRole() { var claim = _httpContextAccessor.HttpContext?.User.Claims diff --git a/CleanArchitecture.Domain/CleanArchitecture.Domain.csproj b/CleanArchitecture.Domain/CleanArchitecture.Domain.csproj index 496b495..bd7f84a 100644 --- a/CleanArchitecture.Domain/CleanArchitecture.Domain.csproj +++ b/CleanArchitecture.Domain/CleanArchitecture.Domain.csproj @@ -10,6 +10,8 @@ + + diff --git a/CleanArchitecture.Domain/Commands/Users/ChangePassword/ChangePasswordCommand.cs b/CleanArchitecture.Domain/Commands/Users/ChangePassword/ChangePasswordCommand.cs index 45f6aca..44f0d86 100644 --- a/CleanArchitecture.Domain/Commands/Users/ChangePassword/ChangePasswordCommand.cs +++ b/CleanArchitecture.Domain/Commands/Users/ChangePassword/ChangePasswordCommand.cs @@ -1,6 +1,23 @@ +using System; + namespace CleanArchitecture.Domain.Commands.Users.ChangePassword; -public sealed class ChangePasswordCommand +public sealed class ChangePasswordCommand : CommandBase { - + private readonly ChangePasswordCommandValidation _validation = new(); + + public string Password { get; } + public string NewPassword { get; } + + public ChangePasswordCommand(string password, string newPassword) : base(Guid.NewGuid()) + { + Password = password; + NewPassword = newPassword; + } + + public override bool IsValid() + { + ValidationResult = _validation.Validate(this); + return ValidationResult.IsValid; + } } \ No newline at end of file diff --git a/CleanArchitecture.Domain/Commands/Users/ChangePassword/ChangePasswordCommandHandler.cs b/CleanArchitecture.Domain/Commands/Users/ChangePassword/ChangePasswordCommandHandler.cs new file mode 100644 index 0000000..81a84cb --- /dev/null +++ b/CleanArchitecture.Domain/Commands/Users/ChangePassword/ChangePasswordCommandHandler.cs @@ -0,0 +1,70 @@ +using System.Threading; +using System.Threading.Tasks; +using CleanArchitecture.Domain.Errors; +using CleanArchitecture.Domain.Interfaces; +using CleanArchitecture.Domain.Interfaces.Repositories; +using CleanArchitecture.Domain.Notifications; +using MediatR; +using BC = BCrypt.Net.BCrypt; + +namespace CleanArchitecture.Domain.Commands.Users.ChangePassword; + +public sealed class ChangePasswordCommandHandler : CommandHandlerBase, + IRequestHandler +{ + private readonly IUserRepository _userRepository; + private readonly IUser _user; + + public ChangePasswordCommandHandler( + IMediatorHandler bus, + IUnitOfWork unitOfWork, + INotificationHandler notifications, + IUserRepository userRepository, + IUser user) : base(bus, unitOfWork, notifications) + { + _userRepository = userRepository; + _user = user; + } + + public async Task Handle(ChangePasswordCommand request, CancellationToken cancellationToken) + { + if (!await TestValidityAsync(request)) + { + return; + } + + var user = await _userRepository.GetByIdAsync(_user.GetUserId()); + + if (user == null) + { + await NotifyAsync( + new DomainNotification( + request.MessageType, + $"There is no User with Id {_user.GetUserId()}", + ErrorCodes.ObjectNotFound)); + + return; + } + + if (!BC.Verify(request.Password, user.Password)) + { + await NotifyAsync( + new DomainNotification( + request.MessageType, + "The password is incorrect", + DomainErrorCodes.UserPasswordIncorrect)); + + return; + } + + string passwordHash = BC.HashPassword(request.NewPassword); + user.SetPassword(passwordHash); + + _userRepository.Update(user); + + if (await CommitAsync()) + { + await _bus.RaiseEventAsync(new User(user.Id)); + } + } +} diff --git a/CleanArchitecture.Domain/Commands/Users/ChangePassword/ChangePasswordCommandValidation.cs b/CleanArchitecture.Domain/Commands/Users/ChangePassword/ChangePasswordCommandValidation.cs new file mode 100644 index 0000000..2fe6a99 --- /dev/null +++ b/CleanArchitecture.Domain/Commands/Users/ChangePassword/ChangePasswordCommandValidation.cs @@ -0,0 +1,7 @@ +using FluentValidation; + +namespace CleanArchitecture.Domain.Commands.Users.ChangePassword; + +public sealed class ChangePasswordCommandValidation : AbstractValidator +{ +} diff --git a/CleanArchitecture.Domain/Commands/Users/LoginUser/LoginUserCommand.cs b/CleanArchitecture.Domain/Commands/Users/LoginUser/LoginUserCommand.cs index 19f2c9b..b0a7999 100644 --- a/CleanArchitecture.Domain/Commands/Users/LoginUser/LoginUserCommand.cs +++ b/CleanArchitecture.Domain/Commands/Users/LoginUser/LoginUserCommand.cs @@ -1,6 +1,28 @@ +using System; +using MediatR; + namespace CleanArchitecture.Domain.Commands.Users.LoginUser; -public sealed class LoginCommand +public sealed class LoginUserCommand : CommandBase, + IRequest { - + private readonly LoginUserCommandValidation _validation = new(); + + public string Email { get; set; } + public string Password { get; set; } + + + public LoginUserCommand( + string email, + string password) : base(Guid.NewGuid()) + { + Email = email; + Password = password; + } + + public override bool IsValid() + { + ValidationResult = _validation.Validate(this); + return ValidationResult.IsValid; + } } \ No newline at end of file diff --git a/CleanArchitecture.Domain/Commands/Users/LoginUser/LoginUserCommandHandler.cs b/CleanArchitecture.Domain/Commands/Users/LoginUser/LoginUserCommandHandler.cs new file mode 100644 index 0000000..73fda8a --- /dev/null +++ b/CleanArchitecture.Domain/Commands/Users/LoginUser/LoginUserCommandHandler.cs @@ -0,0 +1,104 @@ +using System.Security.Claims; +using System.Text; +using System; +using System.Threading; +using System.Threading.Tasks; +using CleanArchitecture.Domain.Enums; +using CleanArchitecture.Domain.Errors; +using CleanArchitecture.Domain.Interfaces; +using CleanArchitecture.Domain.Interfaces.Repositories; +using CleanArchitecture.Domain.Notifications; +using CleanArchitecture.Domain.Settings; +using MediatR; +using BC = BCrypt.Net.BCrypt; +using System.IdentityModel.Tokens.Jwt; +using Microsoft.IdentityModel.Tokens; +using Microsoft.Extensions.Options; + +namespace CleanArchitecture.Domain.Commands.Users.LoginUser; + +public sealed class LoginUserCommandHandler : CommandHandlerBase, + IRequestHandler +{ + private const double EXPIRY_DURATION_MINUTES = 30; + + private readonly IUserRepository _userRepository; + private readonly TokenSettings _tokenSettings; + + public LoginUserCommandHandler( + IMediatorHandler bus, + IUnitOfWork unitOfWork, + INotificationHandler notifications, + IUserRepository userRepository, + IOptions tokenSettings) : base(bus, unitOfWork, notifications) + { + _userRepository = userRepository; + _tokenSettings = tokenSettings.Value; + } + + public async Task Handle(LoginUserCommand request, CancellationToken cancellationToken) + { + if (!await TestValidityAsync(request)) + { + return ""; + } + + var user = await _userRepository.GetByEmailAsync(request.Email); + + if (user == null) + { + await NotifyAsync( + new DomainNotification( + request.MessageType, + $"There is no User with Email {request.Email}", + ErrorCodes.ObjectNotFound)); + + return ""; + } + + var passwordVerified = BC.Verify(request.Password, user.Password); + + if (!passwordVerified) + { + await NotifyAsync( + new DomainNotification( + request.MessageType, + "The password is incorrect", + DomainErrorCodes.UserPasswordIncorrect)); + + return ""; + } + + return BuildToken( + user.Email, + user.Role, + user.Id, + _tokenSettings); + } + + public static string BuildToken(string email, UserRole role, Guid Id, TokenSettings tokenSettings) + { + var claims = new[] + { + new Claim(ClaimTypes.Email, email), + new Claim(ClaimTypes.Role, role.ToString()), + new Claim(ClaimTypes.NameIdentifier, Id.ToString()) + }; + + var securityKey = new SymmetricSecurityKey( + Encoding.UTF8.GetBytes(tokenSettings.Secret)); + + var credentials = new SigningCredentials( + securityKey, + SecurityAlgorithms.HmacSha256Signature); + + var tokenDescriptor = new JwtSecurityToken( + tokenSettings.Issuer, + tokenSettings.Audience, + claims, + expires: DateTime.Now.AddMinutes(EXPIRY_DURATION_MINUTES), + signingCredentials: credentials); + + return new JwtSecurityTokenHandler().WriteToken(tokenDescriptor); + } +} diff --git a/CleanArchitecture.Domain/Commands/Users/LoginUser/LoginUserCommandValidation.cs b/CleanArchitecture.Domain/Commands/Users/LoginUser/LoginUserCommandValidation.cs new file mode 100644 index 0000000..18bd23f --- /dev/null +++ b/CleanArchitecture.Domain/Commands/Users/LoginUser/LoginUserCommandValidation.cs @@ -0,0 +1,7 @@ +using FluentValidation; + +namespace CleanArchitecture.Domain.Commands.Users.LoginUser; + +public sealed class LoginUserCommandValidation : AbstractValidator +{ +} diff --git a/CleanArchitecture.Domain/Errors/DomainErrorCodes.cs b/CleanArchitecture.Domain/Errors/DomainErrorCodes.cs index 9121d0b..147c19f 100644 --- a/CleanArchitecture.Domain/Errors/DomainErrorCodes.cs +++ b/CleanArchitecture.Domain/Errors/DomainErrorCodes.cs @@ -22,4 +22,5 @@ public static class DomainErrorCodes // User public const string UserAlreadyExists = "USER_ALREADY_EXISTS"; + public const string UserPasswordIncorrect = "USER_PASSWORD_INCORRECT"; } \ No newline at end of file diff --git a/CleanArchitecture.Infrastructure/Migrations/20230320204057_AddUserRoleAndPassword.Designer.cs b/CleanArchitecture.Infrastructure/Migrations/20230320204057_AddUserRoleAndPassword.Designer.cs new file mode 100644 index 0000000..72080c0 --- /dev/null +++ b/CleanArchitecture.Infrastructure/Migrations/20230320204057_AddUserRoleAndPassword.Designer.cs @@ -0,0 +1,82 @@ +// +using System; +using CleanArchitecture.Infrastructure.Database; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace CleanArchitecture.Infrastructure.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + [Migration("20230320204057_AddUserRoleAndPassword")] + partial class AddUserRoleAndPassword + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "7.0.4") + .HasAnnotation("Proxies:ChangeTracking", false) + .HasAnnotation("Proxies:CheckEquality", false) + .HasAnnotation("Proxies:LazyLoading", true) + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("CleanArchitecture.Domain.Entities.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("Deleted") + .HasColumnType("bit"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(320) + .HasColumnType("nvarchar(320)"); + + b.Property("GivenName") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("Password") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("nvarchar(128)"); + + b.Property("Role") + .HasColumnType("int"); + + b.Property("Surname") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.HasKey("Id"); + + b.ToTable("Users"); + + b.HasData( + new + { + Id = new Guid("3fc7aacd-41cc-4ca2-b842-32edcd0782d5"), + Deleted = false, + Email = "admin@email.com", + GivenName = "User", + Password = "$2a$12$Blal/uiFIJdYsCLTMUik/egLbfg3XhbnxBC6Sb5IKz2ZYhiU/MzL2", + Role = 0, + Surname = "Admin" + }); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/CleanArchitecture.Infrastructure/Migrations/20230320204057_AddUserRoleAndPassword.cs b/CleanArchitecture.Infrastructure/Migrations/20230320204057_AddUserRoleAndPassword.cs new file mode 100644 index 0000000..438e65c --- /dev/null +++ b/CleanArchitecture.Infrastructure/Migrations/20230320204057_AddUserRoleAndPassword.cs @@ -0,0 +1,52 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace CleanArchitecture.Infrastructure.Migrations +{ + /// + public partial class AddUserRoleAndPassword : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "Password", + table: "Users", + type: "nvarchar(128)", + maxLength: 128, + nullable: false, + defaultValue: ""); + + migrationBuilder.AddColumn( + name: "Role", + table: "Users", + type: "int", + nullable: false, + defaultValue: 0); + + migrationBuilder.InsertData( + table: "Users", + columns: new[] { "Id", "Deleted", "Email", "GivenName", "Password", "Role", "Surname" }, + values: new object[] { new Guid("3fc7aacd-41cc-4ca2-b842-32edcd0782d5"), false, "admin@email.com", "User", "$2a$12$Blal/uiFIJdYsCLTMUik/egLbfg3XhbnxBC6Sb5IKz2ZYhiU/MzL2", 0, "Admin" }); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DeleteData( + table: "Users", + keyColumn: "Id", + keyValue: new Guid("3fc7aacd-41cc-4ca2-b842-32edcd0782d5")); + + migrationBuilder.DropColumn( + name: "Password", + table: "Users"); + + migrationBuilder.DropColumn( + name: "Role", + table: "Users"); + } + } +} diff --git a/CleanArchitecture.Infrastructure/Migrations/ApplicationDbContextModelSnapshot.cs b/CleanArchitecture.Infrastructure/Migrations/ApplicationDbContextModelSnapshot.cs index dcc40d6..249004e 100644 --- a/CleanArchitecture.Infrastructure/Migrations/ApplicationDbContextModelSnapshot.cs +++ b/CleanArchitecture.Infrastructure/Migrations/ApplicationDbContextModelSnapshot.cs @@ -17,7 +17,7 @@ namespace CleanArchitecture.Infrastructure.Migrations { #pragma warning disable 612, 618 modelBuilder - .HasAnnotation("ProductVersion", "7.0.3") + .HasAnnotation("ProductVersion", "7.0.4") .HasAnnotation("Proxies:ChangeTracking", false) .HasAnnotation("Proxies:CheckEquality", false) .HasAnnotation("Proxies:LazyLoading", true) @@ -44,6 +44,14 @@ namespace CleanArchitecture.Infrastructure.Migrations .HasMaxLength(100) .HasColumnType("nvarchar(100)"); + b.Property("Password") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("nvarchar(128)"); + + b.Property("Role") + .HasColumnType("int"); + b.Property("Surname") .IsRequired() .HasMaxLength(100) @@ -52,6 +60,18 @@ namespace CleanArchitecture.Infrastructure.Migrations b.HasKey("Id"); b.ToTable("Users"); + + b.HasData( + new + { + Id = new Guid("3fc7aacd-41cc-4ca2-b842-32edcd0782d5"), + Deleted = false, + Email = "admin@email.com", + GivenName = "User", + Password = "$2a$12$Blal/uiFIJdYsCLTMUik/egLbfg3XhbnxBC6Sb5IKz2ZYhiU/MzL2", + Role = 0, + Surname = "Admin" + }); }); #pragma warning restore 612, 618 } diff --git a/CleanArchitecture.sln b/CleanArchitecture.sln index 31560fb..1209ff0 100644 --- a/CleanArchitecture.sln +++ b/CleanArchitecture.sln @@ -1,26 +1,31 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CleanArchitecture.Api", "CleanArchitecture.Api\CleanArchitecture.Api.csproj", "{CD720672-0ED9-4FDD-AD69-A416CB394318}" +# Visual Studio Version 17 +VisualStudioVersion = 17.5.33502.453 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CleanArchitecture.Api", "CleanArchitecture.Api\CleanArchitecture.Api.csproj", "{CD720672-0ED9-4FDD-AD69-A416CB394318}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CleanArchitecture.Application", "CleanArchitecture.Application\CleanArchitecture.Application.csproj", "{859B50AF-9C8D-4489-B64A-EEBDF756A012}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CleanArchitecture.Application", "CleanArchitecture.Application\CleanArchitecture.Application.csproj", "{859B50AF-9C8D-4489-B64A-EEBDF756A012}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CleanArchitecture.Domain", "CleanArchitecture.Domain\CleanArchitecture.Domain.csproj", "{12C5BEEF-9BFD-450A-8627-6205702CA32B}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CleanArchitecture.Domain", "CleanArchitecture.Domain\CleanArchitecture.Domain.csproj", "{12C5BEEF-9BFD-450A-8627-6205702CA32B}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CleanArchitecture.Infrastructure", "CleanArchitecture.Infrastructure\CleanArchitecture.Infrastructure.csproj", "{B6D046D8-D84A-4B7E-B05B-310B85EC8F1C}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CleanArchitecture.Infrastructure", "CleanArchitecture.Infrastructure\CleanArchitecture.Infrastructure.csproj", "{B6D046D8-D84A-4B7E-B05B-310B85EC8F1C}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CleanArchitecture.Application.Tests", "CleanArchitecture.Application.Tests\CleanArchitecture.Application.Tests.csproj", "{6794B922-2AFD-4187-944D-7984B9973259}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CleanArchitecture.Application.Tests", "CleanArchitecture.Application.Tests\CleanArchitecture.Application.Tests.csproj", "{6794B922-2AFD-4187-944D-7984B9973259}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CleanArchitecture.Domain.Tests", "CleanArchitecture.Domain.Tests\CleanArchitecture.Domain.Tests.csproj", "{E1F25916-EBBE-4CBD-99A2-1EB2F604D55C}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CleanArchitecture.Domain.Tests", "CleanArchitecture.Domain.Tests\CleanArchitecture.Domain.Tests.csproj", "{E1F25916-EBBE-4CBD-99A2-1EB2F604D55C}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CleanArchitecture.Infrastructure.Tests", "CleanArchitecture.Infrastructure.Tests\CleanArchitecture.Infrastructure.Tests.csproj", "{EC8122EB-C5E0-452F-B1CE-DA47DAEBC8F2}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CleanArchitecture.Infrastructure.Tests", "CleanArchitecture.Infrastructure.Tests\CleanArchitecture.Infrastructure.Tests.csproj", "{EC8122EB-C5E0-452F-B1CE-DA47DAEBC8F2}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CleanArchitecture.IntegrationTests", "CleanArchitecture.IntegrationTests\CleanArchitecture.IntegrationTests.csproj", "{39732BD4-909F-410C-8737-1F9FE3E269A7}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CleanArchitecture.IntegrationTests", "CleanArchitecture.IntegrationTests\CleanArchitecture.IntegrationTests.csproj", "{39732BD4-909F-410C-8737-1F9FE3E269A7}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CleanArchitecture.gRPC", "CleanArchitecture.gRPC\CleanArchitecture.gRPC.csproj", "{7A6353A9-B60C-4B13-A849-D21B315047EE}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CleanArchitecture.gRPC", "CleanArchitecture.gRPC\CleanArchitecture.gRPC.csproj", "{7A6353A9-B60C-4B13-A849-D21B315047EE}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CleanArchitecture.Proto", "CleanArchitecture.Proto\CleanArchitecture.Proto.csproj", "{5F978903-7A7A-45C2-ABE0-C2906ECD326B}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CleanArchitecture.Proto", "CleanArchitecture.Proto\CleanArchitecture.Proto.csproj", "{5F978903-7A7A-45C2-ABE0-C2906ECD326B}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CleanArchitecture.gRPC.Tests", "CleanArchitecture.gRPC.Tests\CleanArchitecture.gRPC.Tests.csproj", "{E3A836DD-85DB-44FD-BC19-DDFE111D9EB0}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CleanArchitecture.gRPC.Tests", "CleanArchitecture.gRPC.Tests\CleanArchitecture.gRPC.Tests.csproj", "{E3A836DD-85DB-44FD-BC19-DDFE111D9EB0}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{D3DF9DF5-BD7D-48BC-8BE6-DBD0FABB8B45}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -73,4 +78,14 @@ Global {E3A836DD-85DB-44FD-BC19-DDFE111D9EB0}.Release|Any CPU.ActiveCfg = Release|Any CPU {E3A836DD-85DB-44FD-BC19-DDFE111D9EB0}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {6794B922-2AFD-4187-944D-7984B9973259} = {D3DF9DF5-BD7D-48BC-8BE6-DBD0FABB8B45} + {E1F25916-EBBE-4CBD-99A2-1EB2F604D55C} = {D3DF9DF5-BD7D-48BC-8BE6-DBD0FABB8B45} + {EC8122EB-C5E0-452F-B1CE-DA47DAEBC8F2} = {D3DF9DF5-BD7D-48BC-8BE6-DBD0FABB8B45} + {39732BD4-909F-410C-8737-1F9FE3E269A7} = {D3DF9DF5-BD7D-48BC-8BE6-DBD0FABB8B45} + {E3A836DD-85DB-44FD-BC19-DDFE111D9EB0} = {D3DF9DF5-BD7D-48BC-8BE6-DBD0FABB8B45} + EndGlobalSection EndGlobal From 3b1a76438b28b24fc85059f2b5c11dc5b804aa64 Mon Sep 17 00:00:00 2001 From: Alexander Konietzko Date: Tue, 21 Mar 2023 09:29:49 +0100 Subject: [PATCH 05/10] Connect commands to endpoints --- .../Controllers/UserController.cs | 15 +++++++++++ CleanArchitecture.Api/Dockerfile | 25 ------------------- .../Interfaces/IUserService.cs | 2 ++ .../Services/UserService.cs | 13 ++++++++++ .../Users/ChangePasswordViewModel.cs | 3 +++ .../ViewModels/Users/LoginUserViewModel.cs | 3 +++ .../ChangePasswordCommandHandler.cs | 3 ++- .../EventHandler/UserEventHandler.cs | 8 +++++- .../Events/User/PasswordChangedEvent.cs | 13 ++++++++++ .../Extensions/ServiceCollectionExtension.cs | 10 ++++++-- Dockerfile | 16 ++++++++++++ 11 files changed, 82 insertions(+), 29 deletions(-) delete mode 100644 CleanArchitecture.Api/Dockerfile create mode 100644 CleanArchitecture.Application/ViewModels/Users/ChangePasswordViewModel.cs create mode 100644 CleanArchitecture.Application/ViewModels/Users/LoginUserViewModel.cs create mode 100644 CleanArchitecture.Domain/Events/User/PasswordChangedEvent.cs create mode 100644 Dockerfile diff --git a/CleanArchitecture.Api/Controllers/UserController.cs b/CleanArchitecture.Api/Controllers/UserController.cs index ba98cac..07f2645 100644 --- a/CleanArchitecture.Api/Controllers/UserController.cs +++ b/CleanArchitecture.Api/Controllers/UserController.cs @@ -70,4 +70,19 @@ public class UserController : ApiController await _userService.UpdateUserAsync(viewModel); return Response(viewModel); } + + [Authorize] + [HttpPost("changePassword")] + public async Task ChangePasswordAsync([FromBody] ChangePasswordViewModel viewModel) + { + await _userService.ChangePasswordAsync(viewModel); + return Response(viewModel); + } + + [HttpPost("login")] + public async Task LoginUserAsync([FromBody] LoginUserViewModel viewModel) + { + var token = await _userService.LoginUserAsync(viewModel); + return Response(token); + } } \ No newline at end of file diff --git a/CleanArchitecture.Api/Dockerfile b/CleanArchitecture.Api/Dockerfile deleted file mode 100644 index eaa2fcf..0000000 --- a/CleanArchitecture.Api/Dockerfile +++ /dev/null @@ -1,25 +0,0 @@ -#See https://aka.ms/customizecontainer to learn how to customize your debug container and how Visual Studio uses this Dockerfile to build your images for faster debugging. - -FROM mcr.microsoft.com/dotnet/aspnet:7.0 AS base -WORKDIR /app -EXPOSE 80 -EXPOSE 443 - -FROM mcr.microsoft.com/dotnet/sdk:7.0 AS build -WORKDIR /src -COPY ["CleanArchitecture.Api/CleanArchitecture.Api.csproj", "CleanArchitecture.Api/"] -COPY ["CleanArchitecture.Application/CleanArchitecture.Application.csproj", "CleanArchitecture.Application/"] -COPY ["CleanArchitecture.Domain/CleanArchitecture.Domain.csproj", "CleanArchitecture.Domain/"] -COPY ["CleanArchitecture.Infrastructure/CleanArchitecture.Infrastructure.csproj", "CleanArchitecture.Infrastructure/"] -RUN dotnet restore "CleanArchitecture.Api/CleanArchitecture.Api.csproj" -COPY . . -WORKDIR "/src/CleanArchitecture.Api" -RUN dotnet build "CleanArchitecture.Api.csproj" -c Release -o /app/build - -FROM build AS publish -RUN dotnet publish "CleanArchitecture.Api.csproj" -c Release -o /app/publish /p:UseAppHost=false - -FROM base AS final -WORKDIR /app -COPY --from=publish /app/publish . -ENTRYPOINT ["dotnet", "CleanArchitecture.Api.dll"] \ No newline at end of file diff --git a/CleanArchitecture.Application/Interfaces/IUserService.cs b/CleanArchitecture.Application/Interfaces/IUserService.cs index 85335d0..a04dfcb 100644 --- a/CleanArchitecture.Application/Interfaces/IUserService.cs +++ b/CleanArchitecture.Application/Interfaces/IUserService.cs @@ -13,4 +13,6 @@ public interface IUserService public Task CreateUserAsync(CreateUserViewModel user); public Task UpdateUserAsync(UpdateUserViewModel user); public Task DeleteUserAsync(Guid userId); + public Task ChangePasswordAsync(ChangePasswordViewModel viewModel); + public Task LoginUserAsync(LoginUserViewModel viewModel); } \ No newline at end of file diff --git a/CleanArchitecture.Application/Services/UserService.cs b/CleanArchitecture.Application/Services/UserService.cs index cd622f5..e85af26 100644 --- a/CleanArchitecture.Application/Services/UserService.cs +++ b/CleanArchitecture.Application/Services/UserService.cs @@ -5,8 +5,10 @@ using CleanArchitecture.Application.Interfaces; using CleanArchitecture.Application.Queries.Users.GetAll; using CleanArchitecture.Application.Queries.Users.GetUserById; using CleanArchitecture.Application.ViewModels.Users; +using CleanArchitecture.Domain.Commands.Users.ChangePassword; using CleanArchitecture.Domain.Commands.Users.CreateUser; using CleanArchitecture.Domain.Commands.Users.DeleteUser; +using CleanArchitecture.Domain.Commands.Users.LoginUser; using CleanArchitecture.Domain.Commands.Users.UpdateUser; using CleanArchitecture.Domain.Interfaces; @@ -66,4 +68,15 @@ public sealed class UserService : IUserService { await _bus.SendCommandAsync(new DeleteUserCommand(userId)); } + + public async Task ChangePasswordAsync(ChangePasswordViewModel viewModel) + { + await _bus.SendCommandAsync(new ChangePasswordCommand(viewModel.Password, viewModel.NewPassword)); + } + + public async Task LoginUserAsync(LoginUserViewModel viewModel) + { + + return await _bus.QueryAsync(new LoginUserCommand(viewModel.Email, viewModel.Password)); + } } \ No newline at end of file diff --git a/CleanArchitecture.Application/ViewModels/Users/ChangePasswordViewModel.cs b/CleanArchitecture.Application/ViewModels/Users/ChangePasswordViewModel.cs new file mode 100644 index 0000000..fb02751 --- /dev/null +++ b/CleanArchitecture.Application/ViewModels/Users/ChangePasswordViewModel.cs @@ -0,0 +1,3 @@ +namespace CleanArchitecture.Application.ViewModels.Users; + +public sealed record ChangePasswordViewModel(string Password, string NewPassword); diff --git a/CleanArchitecture.Application/ViewModels/Users/LoginUserViewModel.cs b/CleanArchitecture.Application/ViewModels/Users/LoginUserViewModel.cs new file mode 100644 index 0000000..3883c05 --- /dev/null +++ b/CleanArchitecture.Application/ViewModels/Users/LoginUserViewModel.cs @@ -0,0 +1,3 @@ +namespace CleanArchitecture.Application.ViewModels.Users; + +public sealed record LoginUserViewModel(string Email, string Password); diff --git a/CleanArchitecture.Domain/Commands/Users/ChangePassword/ChangePasswordCommandHandler.cs b/CleanArchitecture.Domain/Commands/Users/ChangePassword/ChangePasswordCommandHandler.cs index 81a84cb..907088d 100644 --- a/CleanArchitecture.Domain/Commands/Users/ChangePassword/ChangePasswordCommandHandler.cs +++ b/CleanArchitecture.Domain/Commands/Users/ChangePassword/ChangePasswordCommandHandler.cs @@ -1,6 +1,7 @@ using System.Threading; using System.Threading.Tasks; using CleanArchitecture.Domain.Errors; +using CleanArchitecture.Domain.Events.User; using CleanArchitecture.Domain.Interfaces; using CleanArchitecture.Domain.Interfaces.Repositories; using CleanArchitecture.Domain.Notifications; @@ -64,7 +65,7 @@ public sealed class ChangePasswordCommandHandler : CommandHandlerBase, if (await CommitAsync()) { - await _bus.RaiseEventAsync(new User(user.Id)); + await _bus.RaiseEventAsync(new PasswordChangedEvent(user.Id)); } } } diff --git a/CleanArchitecture.Domain/EventHandler/UserEventHandler.cs b/CleanArchitecture.Domain/EventHandler/UserEventHandler.cs index 45428b8..a6f7e47 100644 --- a/CleanArchitecture.Domain/EventHandler/UserEventHandler.cs +++ b/CleanArchitecture.Domain/EventHandler/UserEventHandler.cs @@ -8,7 +8,8 @@ namespace CleanArchitecture.Domain.EventHandler; public sealed class UserEventHandler : INotificationHandler, INotificationHandler, - INotificationHandler + INotificationHandler, + INotificationHandler { public Task Handle(UserDeletedEvent notification, CancellationToken cancellationToken) { @@ -24,4 +25,9 @@ public sealed class UserEventHandler : { return Task.CompletedTask; } + + public Task Handle(PasswordChangedEvent notification, CancellationToken cancellationToken) + { + return Task.CompletedTask; + } } \ No newline at end of file diff --git a/CleanArchitecture.Domain/Events/User/PasswordChangedEvent.cs b/CleanArchitecture.Domain/Events/User/PasswordChangedEvent.cs new file mode 100644 index 0000000..32e7a53 --- /dev/null +++ b/CleanArchitecture.Domain/Events/User/PasswordChangedEvent.cs @@ -0,0 +1,13 @@ +using System; + +namespace CleanArchitecture.Domain.Events.User; + +public sealed class PasswordChangedEvent : DomainEvent +{ + public Guid UserId { get; } + + public PasswordChangedEvent(Guid userId) : base(userId) + { + UserId = userId; + } +} diff --git a/CleanArchitecture.Domain/Extensions/ServiceCollectionExtension.cs b/CleanArchitecture.Domain/Extensions/ServiceCollectionExtension.cs index f827036..d56c338 100644 --- a/CleanArchitecture.Domain/Extensions/ServiceCollectionExtension.cs +++ b/CleanArchitecture.Domain/Extensions/ServiceCollectionExtension.cs @@ -1,5 +1,7 @@ +using CleanArchitecture.Domain.Commands.Users.ChangePassword; using CleanArchitecture.Domain.Commands.Users.CreateUser; using CleanArchitecture.Domain.Commands.Users.DeleteUser; +using CleanArchitecture.Domain.Commands.Users.LoginUser; using CleanArchitecture.Domain.Commands.Users.UpdateUser; using CleanArchitecture.Domain.EventHandler; using CleanArchitecture.Domain.Events.User; @@ -17,7 +19,10 @@ public static class ServiceCollectionExtension services.AddScoped, CreateUserCommandHandler>(); services.AddScoped, UpdateUserCommandHandler>(); services.AddScoped, DeleteUserCommandHandler>(); - + services.AddScoped, ChangePasswordCommandHandler>(); + services.AddScoped, LoginUserCommandHandler>(); + + return services; } @@ -27,7 +32,8 @@ public static class ServiceCollectionExtension services.AddScoped, UserEventHandler>(); services.AddScoped, UserEventHandler>(); services.AddScoped, UserEventHandler>(); - + services.AddScoped, UserEventHandler>(); + return services; } diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..db05150 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,16 @@ +FROM mcr.microsoft.com/dotnet/sdk:7.0 AS build +WORKDIR /app + +# copy csproj and restore as distinct layers +COPY . . +RUN dotnet restore + +# copy everything else and build app +COPY CleanArchitecture.Api/. ./CleanArchitecture.Api/ +WORKDIR /app/CleanArchitecture.Api +RUN dotnet publish -c Release -o out + +FROM mcr.microsoft.com/dotnet/aspnet:7.0 AS runtime +WORKDIR /app +COPY --from=build /app/CleanArchitecture.Api/out ./ +ENTRYPOINT ["dotnet", "CleanArchitecture.Api.dll"] From a56fc0e5bb818770b4622827a8fafb4400997c3d Mon Sep 17 00:00:00 2001 From: Alexander Konietzko Date: Tue, 21 Mar 2023 11:03:05 +0100 Subject: [PATCH 06/10] Add Swagger and domain tests --- .../CleanArchitecture.Api.csproj | 3 +- .../Controllers/UserController.cs | 21 ++++- CleanArchitecture.Api/Program.cs | 53 +++++++++++- .../CleanArchitecture.Domain.Tests.csproj | 1 + .../ChangePasswordCommandHandlerTests.cs | 63 ++++++++++++++ .../ChangePasswordCommandTestFixture.cs | 52 ++++++++++++ .../ChangePasswordCommandValidationTests.cs | 11 +++ .../CreateUserCommandHandlerTests.cs | 4 +- .../CreateUserCommandValidationTests.cs | 2 +- .../LoginUser/LoginUserCommandHandlerTests.cs | 82 +++++++++++++++++++ .../LoginUser/LoginUserCommandTestFixture.cs | 55 +++++++++++++ .../LoginUserCommandValidationTests.cs | 11 +++ .../UpdateUserCommandValidationTests.cs | 2 +- .../ChangePasswordCommandValidation.cs | 20 ++++- .../LoginUser/LoginUserCommandValidation.cs | 26 +++++- .../UpdateUser/UpdateUserCommandValidation.cs | 9 ++ .../Errors/DomainErrorCodes.cs | 3 +- Todo.txt | 2 - 18 files changed, 406 insertions(+), 14 deletions(-) create mode 100644 CleanArchitecture.Domain.Tests/CommandHandler/User/ChangePassword/ChangePasswordCommandHandlerTests.cs create mode 100644 CleanArchitecture.Domain.Tests/CommandHandler/User/ChangePassword/ChangePasswordCommandTestFixture.cs create mode 100644 CleanArchitecture.Domain.Tests/CommandHandler/User/ChangePassword/ChangePasswordCommandValidationTests.cs create mode 100644 CleanArchitecture.Domain.Tests/CommandHandler/User/LoginUser/LoginUserCommandHandlerTests.cs create mode 100644 CleanArchitecture.Domain.Tests/CommandHandler/User/LoginUser/LoginUserCommandTestFixture.cs create mode 100644 CleanArchitecture.Domain.Tests/CommandHandler/User/LoginUser/LoginUserCommandValidationTests.cs delete mode 100644 Todo.txt diff --git a/CleanArchitecture.Api/CleanArchitecture.Api.csproj b/CleanArchitecture.Api/CleanArchitecture.Api.csproj index 3ba9cc6..3662059 100644 --- a/CleanArchitecture.Api/CleanArchitecture.Api.csproj +++ b/CleanArchitecture.Api/CleanArchitecture.Api.csproj @@ -1,4 +1,4 @@ - + net7.0 @@ -15,6 +15,7 @@ + diff --git a/CleanArchitecture.Api/Controllers/UserController.cs b/CleanArchitecture.Api/Controllers/UserController.cs index 07f2645..3d822b4 100644 --- a/CleanArchitecture.Api/Controllers/UserController.cs +++ b/CleanArchitecture.Api/Controllers/UserController.cs @@ -1,16 +1,19 @@ using System; +using System.Collections.Generic; using System.Threading.Tasks; +using CleanArchitecture.Api.Models; using CleanArchitecture.Application.Interfaces; using CleanArchitecture.Application.ViewModels.Users; using CleanArchitecture.Domain.Notifications; using MediatR; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; +using Swashbuckle.AspNetCore.Annotations; namespace CleanArchitecture.Api.Controllers; [ApiController] -[Route("[controller]")] +[Route("/api/v1/[controller]")] public class UserController : ApiController { private readonly IUserService _userService; @@ -24,6 +27,8 @@ public class UserController : ApiController [Authorize] [HttpGet] + [SwaggerOperation("Get a list of all users")] + [SwaggerResponse(200, "Request successful", typeof(ResponseMessage>))] public async Task GetAllUsersAsync() { var users = await _userService.GetAllUsersAsync(); @@ -32,6 +37,8 @@ public class UserController : ApiController [Authorize] [HttpGet("{id}")] + [SwaggerOperation("Get a user by id")] + [SwaggerResponse(200, "Request successful", typeof(ResponseMessage))] public async Task GetUserByIdAsync( [FromRoute] Guid id, [FromQuery] bool isDeleted = false) @@ -42,6 +49,8 @@ public class UserController : ApiController [Authorize] [HttpGet("me")] + [SwaggerOperation("Get the current active user")] + [SwaggerResponse(200, "Request successful", typeof(ResponseMessage))] public async Task GetCurrentUserAsync() { var user = await _userService.GetCurrentUserAsync(); @@ -49,6 +58,8 @@ public class UserController : ApiController } [HttpPost] + [SwaggerOperation("Create a new user")] + [SwaggerResponse(200, "Request successful", typeof(ResponseMessage))] public async Task CreateUserAsync([FromBody] CreateUserViewModel viewModel) { var userId = await _userService.CreateUserAsync(viewModel); @@ -57,6 +68,8 @@ public class UserController : ApiController [Authorize] [HttpDelete("{id}")] + [SwaggerOperation("Delete a user")] + [SwaggerResponse(200, "Request successful", typeof(ResponseMessage))] public async Task DeleteUserAsync([FromRoute] Guid id) { await _userService.DeleteUserAsync(id); @@ -65,6 +78,8 @@ public class UserController : ApiController [Authorize] [HttpPut] + [SwaggerOperation("Update a user")] + [SwaggerResponse(200, "Request successful", typeof(ResponseMessage))] public async Task UpdateUserAsync([FromBody] UpdateUserViewModel viewModel) { await _userService.UpdateUserAsync(viewModel); @@ -73,6 +88,8 @@ public class UserController : ApiController [Authorize] [HttpPost("changePassword")] + [SwaggerOperation("Change a password for the current active user")] + [SwaggerResponse(200, "Request successful", typeof(ResponseMessage))] public async Task ChangePasswordAsync([FromBody] ChangePasswordViewModel viewModel) { await _userService.ChangePasswordAsync(viewModel); @@ -80,6 +97,8 @@ public class UserController : ApiController } [HttpPost("login")] + [SwaggerOperation("Get a signed token for a user")] + [SwaggerResponse(200, "Request successful", typeof(ResponseMessage))] public async Task LoginUserAsync([FromBody] LoginUserViewModel viewModel) { var token = await _userService.LoginUserAsync(viewModel); diff --git a/CleanArchitecture.Api/Program.cs b/CleanArchitecture.Api/Program.cs index 5288d7d..9a76f47 100644 --- a/CleanArchitecture.Api/Program.cs +++ b/CleanArchitecture.Api/Program.cs @@ -1,3 +1,4 @@ +using System.Collections.Generic; using System.Text; using CleanArchitecture.Application.Extensions; using CleanArchitecture.Domain.Extensions; @@ -10,15 +11,57 @@ using Microsoft.AspNetCore.Builder; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; using Microsoft.IdentityModel.Tokens; +using Microsoft.OpenApi.Models; var builder = WebApplication.CreateBuilder(args); builder.Services.AddControllers(); builder.Services.AddGrpc(); builder.Services.AddEndpointsApiExplorer(); -builder.Services.AddSwaggerGen(); +builder.Services.AddSwaggerGen(c => +{ + c.EnableAnnotations(); + c.SwaggerDoc("v1", new OpenApiInfo + { + Title = "CleanArchitecture", + Version = "v1", + Description = "A clean architecture API", + }); + + c.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme + { + Description = "JWT Authorization header using the Bearer scheme. " + + "Use the /auth/azureLogin endpoint to generate a token (use the id_token here), " + + "or create a personal access token in centralhub.", + Name = "Authorization", + In = ParameterLocation.Header, + Type = SecuritySchemeType.Http, + Scheme = "bearer" + }); + + c.AddSecurityRequirement(new OpenApiSecurityRequirement() + { + { + new OpenApiSecurityScheme + { + Reference = new OpenApiReference + { + Type = ReferenceType.SecurityScheme, + Id = "Bearer" + }, + Scheme = "oauth2", + Name = "Bearer", + In = ParameterLocation.Header, + }, + new List() + } + }); +}); + +builder.Services.AddHealthChecks(); builder.Services.AddHttpContextAccessor(); builder.Services.AddDbContext(options => @@ -58,8 +101,11 @@ builder.Services.AddMediatR(cfg => var app = builder.Build(); -app.UseSwagger(); -app.UseSwaggerUI(); +if (app.Environment.IsDevelopment()) +{ + app.UseSwagger(); + app.UseSwaggerUI(); +} app.UseHttpsRedirection(); @@ -67,6 +113,7 @@ app.UseAuthentication(); app.UseAuthorization(); app.MapControllers(); +app.MapHealthChecks("/health"); app.MapGrpcService(); using (IServiceScope scope = app.Services.CreateScope()) diff --git a/CleanArchitecture.Domain.Tests/CleanArchitecture.Domain.Tests.csproj b/CleanArchitecture.Domain.Tests/CleanArchitecture.Domain.Tests.csproj index 6cc4fcb..72a5f62 100644 --- a/CleanArchitecture.Domain.Tests/CleanArchitecture.Domain.Tests.csproj +++ b/CleanArchitecture.Domain.Tests/CleanArchitecture.Domain.Tests.csproj @@ -8,6 +8,7 @@ + diff --git a/CleanArchitecture.Domain.Tests/CommandHandler/User/ChangePassword/ChangePasswordCommandHandlerTests.cs b/CleanArchitecture.Domain.Tests/CommandHandler/User/ChangePassword/ChangePasswordCommandHandlerTests.cs new file mode 100644 index 0000000..a6fc1e9 --- /dev/null +++ b/CleanArchitecture.Domain.Tests/CommandHandler/User/ChangePassword/ChangePasswordCommandHandlerTests.cs @@ -0,0 +1,63 @@ +using System.Threading.Tasks; +using CleanArchitecture.Domain.Commands.Users.ChangePassword; +using CleanArchitecture.Domain.Errors; +using CleanArchitecture.Domain.Events.User; +using Xunit; + +namespace CleanArchitecture.Domain.Tests.CommandHandler.User.ChangePassword; + +public sealed class ChangePasswordCommandHandlerTests +{ + private readonly ChangePasswordCommandTestFixture _fixture = new(); + + [Fact] + public async Task Should_Change_Password() + { + var user = _fixture.SetupUser(); + + var command = new ChangePasswordCommand("z8]tnayvd5FNLU9:]AQm", "z8]tnayvd5FNLU9:]AQw"); + + await _fixture.CommandHandler.Handle(command, default); + + _fixture + .VerifyNoDomainNotification() + .VerifyCommit() + .VerifyRaisedEvent(x => x.UserId == user.Id); + } + + [Fact] + public async Task Should_Not_Change_Password_No_User() + { + var userId = _fixture.SetupMissingUser(); + + var command = new ChangePasswordCommand("z8]tnayvd5FNLU9:]AQm", "z8]tnayvd5FNLU9:]AQw"); + + await _fixture.CommandHandler.Handle(command, default); + + _fixture + .VerifyNoCommit() + .VerifyNoRaisedEvent() + .VerifyAnyDomainNotification() + .VerifyExistingNotification( + ErrorCodes.ObjectNotFound, + $"There is no User with Id {userId}"); + } + + [Fact] + public async Task Should_Not_Change_Password_Incorrect_Password() + { + _fixture.SetupUser(); + + var command = new ChangePasswordCommand("z8]tnayvd5FNLU9:]AQw", "z8]tnayvd5FNLU9:]AQx"); + + await _fixture.CommandHandler.Handle(command, default); + + _fixture + .VerifyNoCommit() + .VerifyNoRaisedEvent() + .VerifyAnyDomainNotification() + .VerifyExistingNotification( + DomainErrorCodes.UserPasswordIncorrect, + "The password is incorrect"); + } +} diff --git a/CleanArchitecture.Domain.Tests/CommandHandler/User/ChangePassword/ChangePasswordCommandTestFixture.cs b/CleanArchitecture.Domain.Tests/CommandHandler/User/ChangePassword/ChangePasswordCommandTestFixture.cs new file mode 100644 index 0000000..6162a21 --- /dev/null +++ b/CleanArchitecture.Domain.Tests/CommandHandler/User/ChangePassword/ChangePasswordCommandTestFixture.cs @@ -0,0 +1,52 @@ +using System; +using CleanArchitecture.Domain.Commands.Users.ChangePassword; +using CleanArchitecture.Domain.Enums; +using CleanArchitecture.Domain.Interfaces.Repositories; +using Moq; +using BC = BCrypt.Net.BCrypt; + +namespace CleanArchitecture.Domain.Tests.CommandHandler.User.ChangePassword; + +public sealed class ChangePasswordCommandTestFixture : CommandHandlerFixtureBase +{ + public ChangePasswordCommandHandler CommandHandler { get; set; } + public Mock UserRepository { get; set; } + + public ChangePasswordCommandTestFixture() + { + UserRepository = new Mock(); + + CommandHandler = new( + Bus.Object, + UnitOfWork.Object, + NotificationHandler.Object, + UserRepository.Object, + User.Object); + } + + public Entities.User SetupUser() + { + var user = new Entities.User( + Guid.NewGuid(), + "max@mustermann.com", + "Max", + "Mustermann", + BC.HashPassword("z8]tnayvd5FNLU9:]AQm"), + UserRole.User); + + User.Setup(x => x.GetUserId()).Returns(user.Id); + + UserRepository + .Setup(x => x.GetByIdAsync(It.Is(y => y == user.Id))) + .ReturnsAsync(user); + + return user; + } + + public Guid SetupMissingUser() + { + var id = Guid.NewGuid(); + User.Setup(x => x.GetUserId()).Returns(id); + return id; + } +} diff --git a/CleanArchitecture.Domain.Tests/CommandHandler/User/ChangePassword/ChangePasswordCommandValidationTests.cs b/CleanArchitecture.Domain.Tests/CommandHandler/User/ChangePassword/ChangePasswordCommandValidationTests.cs new file mode 100644 index 0000000..52fd44a --- /dev/null +++ b/CleanArchitecture.Domain.Tests/CommandHandler/User/ChangePassword/ChangePasswordCommandValidationTests.cs @@ -0,0 +1,11 @@ +using CleanArchitecture.Domain.Commands.Users.ChangePassword; + +namespace CleanArchitecture.Domain.Tests.CommandHandler.User.ChangePassword; + +public sealed class ChangePasswordCommandValidationTests : + ValidationTestBase +{ + public ChangePasswordCommandValidationTests() : base(new ChangePasswordCommandValidation()) + { + } +} diff --git a/CleanArchitecture.Domain.Tests/CommandHandler/User/CreateUser/CreateUserCommandHandlerTests.cs b/CleanArchitecture.Domain.Tests/CommandHandler/User/CreateUser/CreateUserCommandHandlerTests.cs index c4072be..50eaa79 100644 --- a/CleanArchitecture.Domain.Tests/CommandHandler/User/CreateUser/CreateUserCommandHandlerTests.cs +++ b/CleanArchitecture.Domain.Tests/CommandHandler/User/CreateUser/CreateUserCommandHandlerTests.cs @@ -20,7 +20,7 @@ public sealed class CreateUserCommandHandlerTests "test@email.com", "Test", "Email", - "SomePassword"); + "Po=PF]PC6t.?8?ks)A6W"); _fixture.CommandHandler.Handle(command, default).Wait(); @@ -40,7 +40,7 @@ public sealed class CreateUserCommandHandlerTests "test@email.com", "Test", "Email", - "SomePassword"); + "Po=PF]PC6t.?8?ks)A6W"); _fixture.CommandHandler.Handle(command, default).Wait(); diff --git a/CleanArchitecture.Domain.Tests/CommandHandler/User/CreateUser/CreateUserCommandValidationTests.cs b/CleanArchitecture.Domain.Tests/CommandHandler/User/CreateUser/CreateUserCommandValidationTests.cs index 2b92763..e418aa0 100644 --- a/CleanArchitecture.Domain.Tests/CommandHandler/User/CreateUser/CreateUserCommandValidationTests.cs +++ b/CleanArchitecture.Domain.Tests/CommandHandler/User/CreateUser/CreateUserCommandValidationTests.cs @@ -119,5 +119,5 @@ public sealed class CreateUserCommandValidationTests : email ?? "test@email.com", surName ?? "test", givenName ?? "email", - password ?? "some password"); + password ?? "Po=PF]PC6t.?8?ks)A6W"); } \ No newline at end of file diff --git a/CleanArchitecture.Domain.Tests/CommandHandler/User/LoginUser/LoginUserCommandHandlerTests.cs b/CleanArchitecture.Domain.Tests/CommandHandler/User/LoginUser/LoginUserCommandHandlerTests.cs new file mode 100644 index 0000000..700da13 --- /dev/null +++ b/CleanArchitecture.Domain.Tests/CommandHandler/User/LoginUser/LoginUserCommandHandlerTests.cs @@ -0,0 +1,82 @@ +using System; +using System.IdentityModel.Tokens.Jwt; +using System.Linq; +using System.Security.Claims; +using System.Threading.Tasks; +using CleanArchitecture.Domain.Commands.Users.LoginUser; +using CleanArchitecture.Domain.Errors; +using FluentAssertions; +using Xunit; + +namespace CleanArchitecture.Domain.Tests.CommandHandler.User.LoginUser; + +public sealed class LoginUserCommandHandlerTests +{ + private readonly LoginUserCommandTestFixture _fixture = new(); + + [Fact] + public async Task Should_Login_User() + { + var user = _fixture.SetupUser(); + + var command = new LoginUserCommand(user.Email, "z8]tnayvd5FNLU9:]AQm"); + + var token = await _fixture.CommandHandler.Handle(command, default); + + _fixture.VerifyNoDomainNotification(); + + token.Should().NotBeNullOrEmpty(); + + var handler = new JwtSecurityTokenHandler(); + var decodedToken = handler.ReadToken(token) as JwtSecurityToken; + + var userIdClaim = decodedToken!.Claims + .FirstOrDefault(x => string.Equals(x.Type, ClaimTypes.NameIdentifier)); + + Guid.Parse(userIdClaim!.Value).Should().Be(user.Id); + + var userEmailClaim = decodedToken!.Claims + .FirstOrDefault(x => string.Equals(x.Type, ClaimTypes.Email)); + + userEmailClaim!.Value.Should().Be(user.Email); + + var userRoleClaim = decodedToken!.Claims + .FirstOrDefault(x => string.Equals(x.Type, ClaimTypes.Role)); + + userRoleClaim!.Value.Should().Be(user.Role.ToString()); + } + + [Fact] + public async Task Should_Not_Login_User_No_User() + { + var command = new LoginUserCommand("test@email.com", "z8]tnayvd5FNLU9:]AQm"); + + var token = await _fixture.CommandHandler.Handle(command, default); + + _fixture + .VerifyAnyDomainNotification() + .VerifyExistingNotification( + ErrorCodes.ObjectNotFound, + $"There is no User with Email {command.Email}"); + + token.Should().BeEmpty(); + } + + [Fact] + public async Task Should_Not_Login_User_Incorrect_Password() + { + var user = _fixture.SetupUser(); + + var command = new LoginUserCommand(user.Email, "z8]tnayvd5FNLU9:]AQw"); + + var token = await _fixture.CommandHandler.Handle(command, default); + + _fixture + .VerifyAnyDomainNotification() + .VerifyExistingNotification( + DomainErrorCodes.UserPasswordIncorrect, + "The password is incorrect"); + + token.Should().BeEmpty(); + } +} diff --git a/CleanArchitecture.Domain.Tests/CommandHandler/User/LoginUser/LoginUserCommandTestFixture.cs b/CleanArchitecture.Domain.Tests/CommandHandler/User/LoginUser/LoginUserCommandTestFixture.cs new file mode 100644 index 0000000..8010194 --- /dev/null +++ b/CleanArchitecture.Domain.Tests/CommandHandler/User/LoginUser/LoginUserCommandTestFixture.cs @@ -0,0 +1,55 @@ +using CleanArchitecture.Domain.Commands.Users.LoginUser; +using CleanArchitecture.Domain.Enums; +using CleanArchitecture.Domain.Interfaces.Repositories; +using CleanArchitecture.Domain.Settings; +using Microsoft.Extensions.Options; +using Moq; +using System; +using BC = BCrypt.Net.BCrypt; + +namespace CleanArchitecture.Domain.Tests.CommandHandler.User.LoginUser; + +public sealed class LoginUserCommandTestFixture : CommandHandlerFixtureBase +{ + public LoginUserCommandHandler CommandHandler { get; set; } + public Mock UserRepository { get; set; } + public IOptions TokenSettings { get; set; } + + public LoginUserCommandTestFixture() + { + UserRepository = new Mock(); + + TokenSettings = Options.Create(new TokenSettings + { + Issuer = "TestIssuer", + Audience = "TestAudience", + Secret = "asjdlkasjd87439284)@#(*" + }); + + CommandHandler = new( + Bus.Object, + UnitOfWork.Object, + NotificationHandler.Object, + UserRepository.Object, + TokenSettings); + } + + public Entities.User SetupUser() + { + var user = new Entities.User( + Guid.NewGuid(), + "max@mustermann.com", + "Max", + "Mustermann", + BC.HashPassword("z8]tnayvd5FNLU9:]AQm"), + UserRole.User); + + User.Setup(x => x.GetUserId()).Returns(user.Id); + + UserRepository + .Setup(x => x.GetByEmailAsync(It.Is(y => y == user.Email))) + .ReturnsAsync(user); + + return user; + } +} diff --git a/CleanArchitecture.Domain.Tests/CommandHandler/User/LoginUser/LoginUserCommandValidationTests.cs b/CleanArchitecture.Domain.Tests/CommandHandler/User/LoginUser/LoginUserCommandValidationTests.cs new file mode 100644 index 0000000..746e492 --- /dev/null +++ b/CleanArchitecture.Domain.Tests/CommandHandler/User/LoginUser/LoginUserCommandValidationTests.cs @@ -0,0 +1,11 @@ +using CleanArchitecture.Domain.Commands.Users.LoginUser; + +namespace CleanArchitecture.Domain.Tests.CommandHandler.User.LoginUser; + +public sealed class LoginUserCommandValidationTests : + ValidationTestBase +{ + public LoginUserCommandValidationTests() : base(new LoginUserCommandValidation()) + { + } +} diff --git a/CleanArchitecture.Domain.Tests/CommandHandler/User/UpdateUser/UpdateUserCommandValidationTests.cs b/CleanArchitecture.Domain.Tests/CommandHandler/User/UpdateUser/UpdateUserCommandValidationTests.cs index 0c80928..dcf9d11 100644 --- a/CleanArchitecture.Domain.Tests/CommandHandler/User/UpdateUser/UpdateUserCommandValidationTests.cs +++ b/CleanArchitecture.Domain.Tests/CommandHandler/User/UpdateUser/UpdateUserCommandValidationTests.cs @@ -109,7 +109,7 @@ public sealed class UpdateUserCommandValidationTests : "Given name may not be longer than 100 characters"); } - private UpdateUserCommand CreateTestCommand( + private static UpdateUserCommand CreateTestCommand( Guid? userId = null, string? email = null, string? surName = null, diff --git a/CleanArchitecture.Domain/Commands/Users/ChangePassword/ChangePasswordCommandValidation.cs b/CleanArchitecture.Domain/Commands/Users/ChangePassword/ChangePasswordCommandValidation.cs index 2fe6a99..e90e964 100644 --- a/CleanArchitecture.Domain/Commands/Users/ChangePassword/ChangePasswordCommandValidation.cs +++ b/CleanArchitecture.Domain/Commands/Users/ChangePassword/ChangePasswordCommandValidation.cs @@ -1,7 +1,25 @@ -using FluentValidation; +using CleanArchitecture.Domain.Extensions.Validation; +using FluentValidation; namespace CleanArchitecture.Domain.Commands.Users.ChangePassword; public sealed class ChangePasswordCommandValidation : AbstractValidator { + public ChangePasswordCommandValidation() + { + AddRuleForPassword(); + AddRuleForNewPassword(); + } + + private void AddRuleForPassword() + { + RuleFor(cmd => cmd.Password) + .Password(); + } + + private void AddRuleForNewPassword() + { + RuleFor(cmd => cmd.NewPassword) + .Password(); + } } diff --git a/CleanArchitecture.Domain/Commands/Users/LoginUser/LoginUserCommandValidation.cs b/CleanArchitecture.Domain/Commands/Users/LoginUser/LoginUserCommandValidation.cs index 18bd23f..64bc452 100644 --- a/CleanArchitecture.Domain/Commands/Users/LoginUser/LoginUserCommandValidation.cs +++ b/CleanArchitecture.Domain/Commands/Users/LoginUser/LoginUserCommandValidation.cs @@ -1,7 +1,31 @@ -using FluentValidation; +using CleanArchitecture.Domain.Errors; +using CleanArchitecture.Domain.Extensions.Validation; +using FluentValidation; namespace CleanArchitecture.Domain.Commands.Users.LoginUser; public sealed class LoginUserCommandValidation : AbstractValidator { + public LoginUserCommandValidation() + { + AddRuleForEmail(); + AddRuleForPassword(); + } + + private void AddRuleForEmail() + { + RuleFor(cmd => cmd.Email) + .EmailAddress() + .WithErrorCode(DomainErrorCodes.UserInvalidEmail) + .WithMessage("Email is not a valid email address") + .MaximumLength(320) + .WithErrorCode(DomainErrorCodes.UserEmailExceedsMaxLength) + .WithMessage("Email may not be longer than 320 characters"); + } + + private void AddRuleForPassword() + { + RuleFor(cmd => cmd.Password) + .Password(); + } } diff --git a/CleanArchitecture.Domain/Commands/Users/UpdateUser/UpdateUserCommandValidation.cs b/CleanArchitecture.Domain/Commands/Users/UpdateUser/UpdateUserCommandValidation.cs index 0b7cc35..58a901f 100644 --- a/CleanArchitecture.Domain/Commands/Users/UpdateUser/UpdateUserCommandValidation.cs +++ b/CleanArchitecture.Domain/Commands/Users/UpdateUser/UpdateUserCommandValidation.cs @@ -11,6 +11,7 @@ public sealed class UpdateUserCommandValidation : AbstractValidator cmd.Role) + .IsInEnum() + .WithErrorCode(DomainErrorCodes.UserInvalidRole) + .WithMessage("Role is not a valid role"); + } } \ No newline at end of file diff --git a/CleanArchitecture.Domain/Errors/DomainErrorCodes.cs b/CleanArchitecture.Domain/Errors/DomainErrorCodes.cs index 147c19f..76925e2 100644 --- a/CleanArchitecture.Domain/Errors/DomainErrorCodes.cs +++ b/CleanArchitecture.Domain/Errors/DomainErrorCodes.cs @@ -10,7 +10,8 @@ public static class DomainErrorCodes public const string UserSurnameExceedsMaxLength = "USER_SURNAME_EXCEEDS_MAX_LENGTH"; public const string UserGivenNameExceedsMaxLength = "USER_GIVEN_NAME_EXCEEDS_MAX_LENGTH"; public const string UserInvalidEmail = "USER_INVALID_EMAIL"; - + public const string UserInvalidRole = "USER_INVALID_ROLE"; + // User Password Validation public const string UserEmptyPassword = "USER_PASSWORD_MAY_NOT_BE_EMPTY"; public const string UserShortPassword = "USER_PASSWORD_MAY_NOT_BE_SHORTER_THAN_6_CHARACTERS"; diff --git a/Todo.txt b/Todo.txt deleted file mode 100644 index 3931b92..0000000 --- a/Todo.txt +++ /dev/null @@ -1,2 +0,0 @@ -- Remove warnings and apply suggestions -- Add authentication and authorization From 492ea93b0d4b7dc788f4650e5038a65755b185c3 Mon Sep 17 00:00:00 2001 From: alex289 Date: Wed, 22 Mar 2023 17:30:07 +0100 Subject: [PATCH 07/10] Add validation and integration tests --- .../Services/UserService.cs | 1 - .../ChangePasswordCommandValidationTests.cs | 85 +++++++++- .../CreateUserCommandValidationTests.cs | 68 ++++++++ .../LoginUserCommandValidationTests.cs | 120 +++++++++++++- .../ValidationTestBase.cs | 18 +++ .../ChangePasswordCommandHandler.cs | 2 +- .../LoginUser/LoginUserCommandHandler.cs | 8 +- .../Controller/UserControllerTests.cs | 147 ++++++++++++------ .../Fixtures/UserTestFixture.cs | 8 + 9 files changed, 401 insertions(+), 56 deletions(-) diff --git a/CleanArchitecture.Application/Services/UserService.cs b/CleanArchitecture.Application/Services/UserService.cs index e85af26..ee726a5 100644 --- a/CleanArchitecture.Application/Services/UserService.cs +++ b/CleanArchitecture.Application/Services/UserService.cs @@ -76,7 +76,6 @@ public sealed class UserService : IUserService public async Task LoginUserAsync(LoginUserViewModel viewModel) { - return await _bus.QueryAsync(new LoginUserCommand(viewModel.Email, viewModel.Password)); } } \ No newline at end of file diff --git a/CleanArchitecture.Domain.Tests/CommandHandler/User/ChangePassword/ChangePasswordCommandValidationTests.cs b/CleanArchitecture.Domain.Tests/CommandHandler/User/ChangePassword/ChangePasswordCommandValidationTests.cs index 52fd44a..d68220c 100644 --- a/CleanArchitecture.Domain.Tests/CommandHandler/User/ChangePassword/ChangePasswordCommandValidationTests.cs +++ b/CleanArchitecture.Domain.Tests/CommandHandler/User/ChangePassword/ChangePasswordCommandValidationTests.cs @@ -1,4 +1,8 @@ -using CleanArchitecture.Domain.Commands.Users.ChangePassword; +using System.Collections.Generic; +using System.Linq; +using CleanArchitecture.Domain.Commands.Users.ChangePassword; +using CleanArchitecture.Domain.Errors; +using Xunit; namespace CleanArchitecture.Domain.Tests.CommandHandler.User.ChangePassword; @@ -8,4 +12,83 @@ public sealed class ChangePasswordCommandValidationTests : public ChangePasswordCommandValidationTests() : base(new ChangePasswordCommandValidation()) { } + + [Fact] + public void Should_Be_Valid() + { + var command = CreateTestCommand(); + + ShouldBeValid(command); + } + + [Fact] + public void Should_Be_Invalid_For_Empty_Password() + { + var command = CreateTestCommand(""); + + var errors = new List + { + DomainErrorCodes.UserEmptyPassword, + DomainErrorCodes.UserSpecialCharPassword, + DomainErrorCodes.UserNumberPassword, + DomainErrorCodes.UserLowercaseLetterPassword, + DomainErrorCodes.UserUppercaseLetterPassword, + DomainErrorCodes.UserShortPassword + }; + + ShouldHaveExpectedErrors(command, errors.ToArray()); + } + + [Fact] + public void Should_Be_Invalid_For_Missing_Special_Character() + { + var command = CreateTestCommand("z8tnayvd5FNLU9AQm"); + + ShouldHaveSingleError(command, DomainErrorCodes.UserSpecialCharPassword); + } + + [Fact] + public void Should_Be_Invalid_For_Missing_Number() + { + var command = CreateTestCommand("z]tnayvdFNLU:]AQm"); + + ShouldHaveSingleError(command, DomainErrorCodes.UserNumberPassword); + } + + [Fact] + public void Should_Be_Invalid_For_Missing_Lowercase_Character() + { + var command = CreateTestCommand("Z8]TNAYVDFNLU:]AQM"); + + ShouldHaveSingleError(command, DomainErrorCodes.UserLowercaseLetterPassword); + } + + [Fact] + public void Should_Be_Invalid_For_Missing_Uppercase_Character() + { + var command = CreateTestCommand("z8]tnayvd5fnlu9:]aqm"); + + ShouldHaveSingleError(command, DomainErrorCodes.UserUppercaseLetterPassword); + } + + [Fact] + public void Should_Be_Invalid_For_Password_Too_Short() + { + var command = CreateTestCommand("zA6{"); + + ShouldHaveSingleError(command, DomainErrorCodes.UserShortPassword); + } + + [Fact] + public void Should_Be_Invalid_For_Password_Too_Long() + { + var command = CreateTestCommand(string.Concat(Enumerable.Repeat("zA6{", 12), 12)); + + ShouldHaveSingleError(command, DomainErrorCodes.UserLongPassword); + } + + private ChangePasswordCommand CreateTestCommand( + string? password = null, string? newPassword = null) => new( + password ?? "z8]tnayvd5FNLU9:]AQm", + newPassword ?? "z8]tnayvd5FNLU9:]AQw"); } diff --git a/CleanArchitecture.Domain.Tests/CommandHandler/User/CreateUser/CreateUserCommandValidationTests.cs b/CleanArchitecture.Domain.Tests/CommandHandler/User/CreateUser/CreateUserCommandValidationTests.cs index e418aa0..61a00ad 100644 --- a/CleanArchitecture.Domain.Tests/CommandHandler/User/CreateUser/CreateUserCommandValidationTests.cs +++ b/CleanArchitecture.Domain.Tests/CommandHandler/User/CreateUser/CreateUserCommandValidationTests.cs @@ -1,4 +1,6 @@ using System; +using System.Collections.Generic; +using System.Linq; using CleanArchitecture.Domain.Commands.Users.CreateUser; using CleanArchitecture.Domain.Errors; using Xunit; @@ -108,6 +110,72 @@ public sealed class CreateUserCommandValidationTests : "Given name may not be longer than 100 characters"); } + [Fact] + public void Should_Be_Invalid_For_Empty_Password() + { + var command = CreateTestCommand(password: ""); + + var errors = new List + { + DomainErrorCodes.UserEmptyPassword, + DomainErrorCodes.UserSpecialCharPassword, + DomainErrorCodes.UserNumberPassword, + DomainErrorCodes.UserLowercaseLetterPassword, + DomainErrorCodes.UserUppercaseLetterPassword, + DomainErrorCodes.UserShortPassword + }; + + ShouldHaveExpectedErrors(command, errors.ToArray()); + } + + [Fact] + public void Should_Be_Invalid_For_Missing_Special_Character() + { + var command = CreateTestCommand(password: "z8tnayvd5FNLU9AQm"); + + ShouldHaveSingleError(command, DomainErrorCodes.UserSpecialCharPassword); + } + + [Fact] + public void Should_Be_Invalid_For_Missing_Number() + { + var command = CreateTestCommand(password: "z]tnayvdFNLU:]AQm"); + + ShouldHaveSingleError(command, DomainErrorCodes.UserNumberPassword); + } + + [Fact] + public void Should_Be_Invalid_For_Missing_Lowercase_Character() + { + var command = CreateTestCommand(password: "Z8]TNAYVDFNLU:]AQM"); + + ShouldHaveSingleError(command, DomainErrorCodes.UserLowercaseLetterPassword); + } + + [Fact] + public void Should_Be_Invalid_For_Missing_Uppercase_Character() + { + var command = CreateTestCommand(password: "z8]tnayvd5fnlu9:]aqm"); + + ShouldHaveSingleError(command, DomainErrorCodes.UserUppercaseLetterPassword); + } + + [Fact] + public void Should_Be_Invalid_For_Password_Too_Short() + { + var command = CreateTestCommand(password: "zA6{"); + + ShouldHaveSingleError(command, DomainErrorCodes.UserShortPassword); + } + + [Fact] + public void Should_Be_Invalid_For_Password_Too_Long() + { + var command = CreateTestCommand(password: string.Concat(Enumerable.Repeat("zA6{", 12), 12)); + + ShouldHaveSingleError(command, DomainErrorCodes.UserLongPassword); + } + private CreateUserCommand CreateTestCommand( Guid? userId = null, string? email = null, diff --git a/CleanArchitecture.Domain.Tests/CommandHandler/User/LoginUser/LoginUserCommandValidationTests.cs b/CleanArchitecture.Domain.Tests/CommandHandler/User/LoginUser/LoginUserCommandValidationTests.cs index 746e492..223f711 100644 --- a/CleanArchitecture.Domain.Tests/CommandHandler/User/LoginUser/LoginUserCommandValidationTests.cs +++ b/CleanArchitecture.Domain.Tests/CommandHandler/User/LoginUser/LoginUserCommandValidationTests.cs @@ -1,4 +1,8 @@ -using CleanArchitecture.Domain.Commands.Users.LoginUser; +using System.Collections.Generic; +using System.Linq; +using CleanArchitecture.Domain.Commands.Users.LoginUser; +using CleanArchitecture.Domain.Errors; +using Xunit; namespace CleanArchitecture.Domain.Tests.CommandHandler.User.LoginUser; @@ -8,4 +12,118 @@ public sealed class LoginUserCommandValidationTests : public LoginUserCommandValidationTests() : base(new LoginUserCommandValidation()) { } + + [Fact] + public void Should_Be_Valid() + { + var command = CreateTestCommand(); + + ShouldBeValid(command); + } + + [Fact] + public void Should_Be_Invalid_For_Empty_Email() + { + var command = CreateTestCommand(email: string.Empty); + + ShouldHaveSingleError( + command, + DomainErrorCodes.UserInvalidEmail, + "Email is not a valid email address"); + } + + [Fact] + public void Should_Be_Invalid_For_Invalid_Email() + { + var command = CreateTestCommand(email: "not a email"); + + ShouldHaveSingleError( + command, + DomainErrorCodes.UserInvalidEmail, + "Email is not a valid email address"); + } + + [Fact] + public void Should_Be_Invalid_For_Email_Exceeds_Max_Length() + { + var command = CreateTestCommand(email: new string('a', 320) + "@test.com"); + + ShouldHaveSingleError( + command, + DomainErrorCodes.UserEmailExceedsMaxLength, + "Email may not be longer than 320 characters"); + } + + [Fact] + public void Should_Be_Invalid_For_Empty_Password() + { + var command = CreateTestCommand(password: ""); + + var errors = new List + { + DomainErrorCodes.UserEmptyPassword, + DomainErrorCodes.UserSpecialCharPassword, + DomainErrorCodes.UserNumberPassword, + DomainErrorCodes.UserLowercaseLetterPassword, + DomainErrorCodes.UserUppercaseLetterPassword, + DomainErrorCodes.UserShortPassword + }; + + ShouldHaveExpectedErrors(command, errors.ToArray()); + } + + [Fact] + public void Should_Be_Invalid_For_Missing_Special_Character() + { + var command = CreateTestCommand(password: "z8tnayvd5FNLU9AQm"); + + ShouldHaveSingleError(command, DomainErrorCodes.UserSpecialCharPassword); + } + + [Fact] + public void Should_Be_Invalid_For_Missing_Number() + { + var command = CreateTestCommand(password: "z]tnayvdFNLU:]AQm"); + + ShouldHaveSingleError(command, DomainErrorCodes.UserNumberPassword); + } + + [Fact] + public void Should_Be_Invalid_For_Missing_Lowercase_Character() + { + var command = CreateTestCommand(password: "Z8]TNAYVDFNLU:]AQM"); + + ShouldHaveSingleError(command, DomainErrorCodes.UserLowercaseLetterPassword); + } + + [Fact] + public void Should_Be_Invalid_For_Missing_Uppercase_Character() + { + var command = CreateTestCommand(password: "z8]tnayvd5fnlu9:]aqm"); + + ShouldHaveSingleError(command, DomainErrorCodes.UserUppercaseLetterPassword); + } + + [Fact] + public void Should_Be_Invalid_For_Password_Too_Short() + { + var command = CreateTestCommand(password: "zA6{"); + + ShouldHaveSingleError(command, DomainErrorCodes.UserShortPassword); + } + + [Fact] + public void Should_Be_Invalid_For_Password_Too_Long() + { + var command = CreateTestCommand(password: string.Concat(Enumerable.Repeat("zA6{", 12), 12)); + + ShouldHaveSingleError(command, DomainErrorCodes.UserLongPassword); + } + + private LoginUserCommand CreateTestCommand( + string? email = null, + string? password = null) => + new ( + email ?? "test@email.com", + password ?? "Po=PF]PC6t.?8?ks)A6W"); } diff --git a/CleanArchitecture.Domain.Tests/ValidationTestBase.cs b/CleanArchitecture.Domain.Tests/ValidationTestBase.cs index 7f985c8..cbb3692 100644 --- a/CleanArchitecture.Domain.Tests/ValidationTestBase.cs +++ b/CleanArchitecture.Domain.Tests/ValidationTestBase.cs @@ -70,4 +70,22 @@ public class ValidationTestBase .Be(1); } } + + protected void ShouldHaveExpectedErrors( + TCommand command, + params string[] expectedErrors) + { + var result = _validation.Validate(command); + + result.IsValid.Should().BeFalse(); + result.Errors.Count.Should().Be(expectedErrors.Length); + + foreach (var error in expectedErrors) + { + result.Errors + .Count(validation => validation.ErrorCode == error) + .Should() + .Be(1); + } + } } \ No newline at end of file diff --git a/CleanArchitecture.Domain/Commands/Users/ChangePassword/ChangePasswordCommandHandler.cs b/CleanArchitecture.Domain/Commands/Users/ChangePassword/ChangePasswordCommandHandler.cs index 907088d..4bd41e0 100644 --- a/CleanArchitecture.Domain/Commands/Users/ChangePassword/ChangePasswordCommandHandler.cs +++ b/CleanArchitecture.Domain/Commands/Users/ChangePassword/ChangePasswordCommandHandler.cs @@ -58,7 +58,7 @@ public sealed class ChangePasswordCommandHandler : CommandHandlerBase, return; } - string passwordHash = BC.HashPassword(request.NewPassword); + var passwordHash = BC.HashPassword(request.NewPassword); user.SetPassword(passwordHash); _userRepository.Update(user); diff --git a/CleanArchitecture.Domain/Commands/Users/LoginUser/LoginUserCommandHandler.cs b/CleanArchitecture.Domain/Commands/Users/LoginUser/LoginUserCommandHandler.cs index 73fda8a..9998952 100644 --- a/CleanArchitecture.Domain/Commands/Users/LoginUser/LoginUserCommandHandler.cs +++ b/CleanArchitecture.Domain/Commands/Users/LoginUser/LoginUserCommandHandler.cs @@ -20,7 +20,7 @@ namespace CleanArchitecture.Domain.Commands.Users.LoginUser; public sealed class LoginUserCommandHandler : CommandHandlerBase, IRequestHandler { - private const double EXPIRY_DURATION_MINUTES = 30; + private const double ExpiryDurationMinutes = 30; private readonly IUserRepository _userRepository; private readonly TokenSettings _tokenSettings; @@ -76,13 +76,13 @@ public sealed class LoginUserCommandHandler : CommandHandlerBase, _tokenSettings); } - public static string BuildToken(string email, UserRole role, Guid Id, TokenSettings tokenSettings) + public static string BuildToken(string email, UserRole role, Guid id, TokenSettings tokenSettings) { var claims = new[] { new Claim(ClaimTypes.Email, email), new Claim(ClaimTypes.Role, role.ToString()), - new Claim(ClaimTypes.NameIdentifier, Id.ToString()) + new Claim(ClaimTypes.NameIdentifier, id.ToString()) }; var securityKey = new SymmetricSecurityKey( @@ -96,7 +96,7 @@ public sealed class LoginUserCommandHandler : CommandHandlerBase, tokenSettings.Issuer, tokenSettings.Audience, claims, - expires: DateTime.Now.AddMinutes(EXPIRY_DURATION_MINUTES), + expires: DateTime.Now.AddMinutes(ExpiryDurationMinutes), signingCredentials: credentials); return new JwtSecurityTokenHandler().WriteToken(tokenDescriptor); diff --git a/CleanArchitecture.IntegrationTests/Controller/UserControllerTests.cs b/CleanArchitecture.IntegrationTests/Controller/UserControllerTests.cs index 20d92be..357dd35 100644 --- a/CleanArchitecture.IntegrationTests/Controller/UserControllerTests.cs +++ b/CleanArchitecture.IntegrationTests/Controller/UserControllerTests.cs @@ -25,31 +25,15 @@ public sealed class UserControllerTests : IClassFixture } [Fact, Priority(0)] - public async Task Should_Get_No_User() - { - var response = await _fixture.ServerClient.GetAsync("user"); - - response.StatusCode.Should().Be(HttpStatusCode.OK); - - var message = await response.Content.ReadAsJsonAsync>(); - - message?.Data.Should().NotBeNull(); - - var content = message!.Data!; - - content.Should().BeNullOrEmpty(); - } - - [Fact, Priority(5)] public async Task Should_Create_User() { var user = new CreateUserViewModel( - "test@email.com", + _fixture.CreatedUserEmail, "Test", "Email", - "Password"); + _fixture.CreatedUserPassword); - var response = await _fixture.ServerClient.PostAsJsonAsync("user", user); + var response = await _fixture.ServerClient.PostAsJsonAsync("/api/v1/user", user); response.StatusCode.Should().Be(HttpStatusCode.OK); @@ -59,11 +43,49 @@ public sealed class UserControllerTests : IClassFixture _fixture.CreatedUserId = message!.Data; } + + [Fact, Priority(5)] + public async Task Should_Login_User() + { + var user = new LoginUserViewModel( + _fixture.CreatedUserEmail, + _fixture.CreatedUserPassword); + + var response = await _fixture.ServerClient.PostAsJsonAsync("/api/v1/user/login", user); + + response.StatusCode.Should().Be(HttpStatusCode.OK); + + var message = await response.Content.ReadAsJsonAsync(); + + message?.Data.Should().NotBeEmpty(); + + _fixture.CreatedUserToken = message!.Data!; + _fixture.EnableAuthentication(); + } [Fact, Priority(10)] public async Task Should_Get_Created_Users() { - var response = await _fixture.ServerClient.GetAsync("user/" + _fixture.CreatedUserId); + var response = await _fixture.ServerClient.GetAsync("/api/v1/user/" + _fixture.CreatedUserId); + + response.StatusCode.Should().Be(HttpStatusCode.OK); + + var message = await response.Content.ReadAsJsonAsync(); + + message?.Data.Should().NotBeNull(); + + var content = message!.Data!; + + content.Id.Should().Be(_fixture.CreatedUserId); + content.Email.Should().Be("test@email.com"); + content.Surname.Should().Be("Test"); + content.GivenName.Should().Be("Email"); + } + + [Fact, Priority(10)] + public async Task Should_Get_The_Current_Active_Users() + { + var response = await _fixture.ServerClient.GetAsync("/api/v1/user/me"); response.StatusCode.Should().Be(HttpStatusCode.OK); @@ -89,7 +111,7 @@ public sealed class UserControllerTests : IClassFixture "NewEmail", UserRole.User); - var response = await _fixture.ServerClient.PutAsJsonAsync("user", user); + var response = await _fixture.ServerClient.PutAsJsonAsync("/api/v1/user", user); response.StatusCode.Should().Be(HttpStatusCode.OK); @@ -105,7 +127,7 @@ public sealed class UserControllerTests : IClassFixture [Fact, Priority(20)] public async Task Should_Get_Updated_Users() { - var response = await _fixture.ServerClient.GetAsync("user/" + _fixture.CreatedUserId); + var response = await _fixture.ServerClient.GetAsync("/api/v1/user/" + _fixture.CreatedUserId); response.StatusCode.Should().Be(HttpStatusCode.OK); @@ -119,12 +141,47 @@ public sealed class UserControllerTests : IClassFixture content.Email.Should().Be("newtest@email.com"); content.Surname.Should().Be("NewTest"); content.GivenName.Should().Be("NewEmail"); + + _fixture.CreatedUserEmail = content.Email; + } + + [Fact, Priority(25)] + public async Task Should_Change_User_Password() + { + var user = new ChangePasswordViewModel( + _fixture.CreatedUserPassword, + _fixture.CreatedUserPassword + "1"); + + var response = await _fixture.ServerClient.PostAsJsonAsync("/api/v1/user/changePassword", user); + + response.StatusCode.Should().Be(HttpStatusCode.OK); + + var message = await response.Content.ReadAsJsonAsync(); + + message?.Data.Should().NotBeNull(); + + var content = message!.Data; + + content.Should().BeEquivalentTo(user); + + // Verify the user can login with the new password + var login = new LoginUserViewModel( + _fixture.CreatedUserEmail, + _fixture.CreatedUserPassword + "1"); + + var loginResponse = await _fixture.ServerClient.PostAsJsonAsync("/api/v1/user/login", login); + + loginResponse.StatusCode.Should().Be(HttpStatusCode.OK); + + var loginMessage = await loginResponse.Content.ReadAsJsonAsync(); + + loginMessage?.Data.Should().NotBeEmpty(); } - [Fact, Priority(25)] - public async Task Should_Get_One_User() + [Fact, Priority(30)] + public async Task Should_Get_All_User() { - var response = await _fixture.ServerClient.GetAsync("user"); + var response = await _fixture.ServerClient.GetAsync("/api/v1/user"); response.StatusCode.Should().Be(HttpStatusCode.OK); @@ -134,17 +191,27 @@ public sealed class UserControllerTests : IClassFixture var content = message!.Data!.ToList(); - content.Should().ContainSingle(); - content.First().Id.Should().Be(_fixture.CreatedUserId); - content.First().Email.Should().Be("newtest@email.com"); - content.First().Surname.Should().Be("NewTest"); - content.First().GivenName.Should().Be("NewEmail"); + content.Count.Should().Be(2); + + var currentUser = content.First(x => x.Id == _fixture.CreatedUserId); + + currentUser.Id.Should().Be(_fixture.CreatedUserId); + currentUser.Role.Should().Be(UserRole.User); + currentUser.Email.Should().Be("newtest@email.com"); + currentUser.Surname.Should().Be("NewTest"); + currentUser.GivenName.Should().Be("NewEmail"); + + var adminUser = content.First(x => x.Role == UserRole.Admin); + + adminUser.Email.Should().Be("admin@email.com"); + adminUser.Surname.Should().Be("Admin"); + adminUser.GivenName.Should().Be("User"); } - [Fact, Priority(30)] + [Fact, Priority(35)] public async Task Should_Delete_User() { - var response = await _fixture.ServerClient.DeleteAsync("user/" + _fixture.CreatedUserId); + var response = await _fixture.ServerClient.DeleteAsync("/api/v1/user/" + _fixture.CreatedUserId); response.StatusCode.Should().Be(HttpStatusCode.OK); @@ -155,20 +222,4 @@ public sealed class UserControllerTests : IClassFixture var content = message!.Data; content.Should().Be(_fixture.CreatedUserId); } - - [Fact, Priority(35)] - public async Task Should_Get_No_User_Again() - { - var response = await _fixture.ServerClient.GetAsync("user"); - - response.StatusCode.Should().Be(HttpStatusCode.OK); - - var message = await response.Content.ReadAsJsonAsync>(); - - message?.Data.Should().NotBeNull(); - - var content = message!.Data!; - - content.Should().BeNullOrEmpty(); - } } diff --git a/CleanArchitecture.IntegrationTests/Fixtures/UserTestFixture.cs b/CleanArchitecture.IntegrationTests/Fixtures/UserTestFixture.cs index 5d31bea..3d8fa2e 100644 --- a/CleanArchitecture.IntegrationTests/Fixtures/UserTestFixture.cs +++ b/CleanArchitecture.IntegrationTests/Fixtures/UserTestFixture.cs @@ -5,4 +5,12 @@ namespace CleanArchitecture.IntegrationTests.Fixtures; public sealed class UserTestFixture : TestFixtureBase { public Guid CreatedUserId { get; set; } + public string CreatedUserEmail { get; set; } = "test@email.com"; + public string CreatedUserPassword { get; set; } = "z8]tnayvd5FNLU9:]AQm"; + public string CreatedUserToken { get; set; } = string.Empty; + + public void EnableAuthentication() + { + ServerClient.DefaultRequestHeaders.Add("Authorization", $"Bearer {CreatedUserToken}"); + } } From df5530c72620b7cf5ab9a714e5d664b9e4f5cd6d Mon Sep 17 00:00:00 2001 From: alex289 Date: Wed, 22 Mar 2023 19:06:01 +0100 Subject: [PATCH 08/10] Full Cleanup --- .../CleanArchitecture.Api.csproj | 22 +++--- .../Controllers/ApiController.cs | 2 +- .../Controllers/UserController.cs | 4 +- CleanArchitecture.Api/Program.cs | 62 +++++++-------- ...CleanArchitecture.Application.Tests.csproj | 14 ++-- .../Queries/QueryHandlerBaseFixture.cs | 2 +- .../Queries/Users/GetAllUsersTestFixture.cs | 44 +++++------ .../Queries/Users/GetUserByIdTestFixture.cs | 22 +++--- .../Users/GetAllUsersQueryHandlerTests.cs | 9 ++- .../Users/GetUserByIdQueryHandlerTests.cs | 10 +-- .../CleanArchitecture.Application.csproj | 4 +- .../Extensions/ServiceCollectionExtension.cs | 6 +- .../Interfaces/IUserService.cs | 2 +- .../Queries/Users/GetAll/GetAllUsersQuery.cs | 2 +- .../Users/GetUserById/GetUserByIdQuery.cs | 2 +- .../GetUserById/GetUserByIdQueryHandler.cs | 6 +- .../Services/UserService.cs | 8 +- .../Users/ChangePasswordViewModel.cs | 2 +- .../ViewModels/Users/LoginUserViewModel.cs | 2 +- .../CleanArchitecture.Domain.Tests.csproj | 12 +-- .../ChangePasswordCommandHandlerTests.cs | 2 +- .../ChangePasswordCommandTestFixture.cs | 10 +-- .../ChangePasswordCommandValidationTests.cs | 41 +++++----- .../CreateUserCommandHandlerTests.cs | 12 +-- .../CreateUserCommandTestFixture.cs | 12 +-- .../CreateUserCommandValidationTests.cs | 76 ++++++++++--------- .../DeleteUserCommandHandlerTests.cs | 12 +-- .../DeleteUserCommandTestFixture.cs | 10 +-- .../DeleteUserCommandValidationTests.cs | 18 +++-- .../LoginUser/LoginUserCommandHandlerTests.cs | 2 +- .../LoginUser/LoginUserCommandTestFixture.cs | 16 ++-- .../LoginUserCommandValidationTests.cs | 60 ++++++++------- .../UpdateUserCommandHandlerTests.cs | 12 +-- .../UpdateUserCommandTestFixture.cs | 12 +-- .../UpdateUserCommandValidationTests.cs | 48 ++++++------ .../CommandHandlerFixtureBase.cs | 12 +-- .../ValidationTestBase.cs | 8 +- CleanArchitecture.Domain/ApiUser.cs | 28 +++---- .../CleanArchitecture.Domain.csproj | 12 +-- .../Commands/CommandBase.cs | 12 +-- .../Commands/CommandHandlerBase.cs | 12 +-- .../ChangePassword/ChangePasswordCommand.cs | 6 +- .../ChangePasswordCommandHandler.cs | 4 +- .../ChangePasswordCommandValidation.cs | 2 +- .../Users/CreateUser/CreateUserCommand.cs | 14 ++-- .../CreateUser/CreateUserCommandHandler.cs | 12 +-- .../CreateUser/CreateUserCommandValidation.cs | 8 +- .../Users/DeleteUser/DeleteUserCommand.cs | 8 +- .../DeleteUser/DeleteUserCommandHandler.cs | 6 +- .../Users/LoginUser/LoginUserCommand.cs | 6 +- .../LoginUser/LoginUserCommandHandler.cs | 22 +++--- .../LoginUser/LoginUserCommandValidation.cs | 2 +- .../Users/UpdateUser/UpdateUserCommand.cs | 12 +-- .../UpdateUser/UpdateUserCommandHandler.cs | 7 +- .../UpdateUser/UpdateUserCommandValidation.cs | 6 +- CleanArchitecture.Domain/Entities/User.cs | 20 ++--- CleanArchitecture.Domain/Enums/UserRole.cs | 2 +- .../Errors/DomainErrorCodes.cs | 2 +- .../EventHandler/UserEventHandler.cs | 8 +- .../Events/User/PasswordChangedEvent.cs | 6 +- .../Events/User/UserCreatedEvent.cs | 4 +- .../Events/User/UserDeletedEvent.cs | 4 +- .../Events/User/UserUpdatedEvent.cs | 4 +- .../Extensions/ServiceCollectionExtension.cs | 4 +- .../Extensions/Validation/CustomValidator.cs | 5 +- CleanArchitecture.Domain/Interfaces/IUser.cs | 5 +- .../Notifications/DomainNotification.cs | 10 +-- .../DomainNotificationHandler.cs | 12 +-- ...anArchitecture.Infrastructure.Tests.csproj | 10 +-- .../DomainNotificationHandlerTests.cs | 20 ++--- .../DomainNotificationTests.cs | 14 ++-- .../UnitOfWorkTests.cs | 10 +-- .../CleanArchitecture.Infrastructure.csproj | 18 ++--- .../Configurations/UserConfiguration.cs | 2 +- .../Database/ApplicationDbContext.cs | 4 +- .../Extensions/DbContextExtension.cs | 2 +- .../Extensions/ServiceCollectionExtensions.cs | 2 +- .../InMemoryBus.cs | 2 +- .../Repositories/BaseRepository.cs | 27 ++++--- .../UnitOfWork.cs | 2 +- .../CleanArchitecture.IntegrationTests.csproj | 20 ++--- .../Controller/UserControllerTests.cs | 55 ++++++++------ ...ctionalTestsServiceCollectionExtensions.cs | 21 ++--- .../Extensions/HttpExtensions.cs | 4 +- .../Fixtures/TestFixtureBase.cs | 6 +- .../Fixtures/UserTestFixture.cs | 4 +- .../CleanArchitectureWebApplicationFactory.cs | 10 +-- .../CleanArchitecture.Proto.csproj | 14 ++-- .../CleanArchitecture.gRPC.Tests.csproj | 14 ++-- .../Fixtures/UserTestsFixture.cs | 40 +++++----- .../CleanArchitecture.gRPC.csproj | 6 +- 91 files changed, 593 insertions(+), 579 deletions(-) diff --git a/CleanArchitecture.Api/CleanArchitecture.Api.csproj b/CleanArchitecture.Api/CleanArchitecture.Api.csproj index 3662059..3ffe10b 100644 --- a/CleanArchitecture.Api/CleanArchitecture.Api.csproj +++ b/CleanArchitecture.Api/CleanArchitecture.Api.csproj @@ -7,22 +7,22 @@ - - + + - all - runtime; build; native; contentfiles; analyzers; buildtransitive + all + runtime; build; native; contentfiles; analyzers; buildtransitive - - - + + + - - - - + + + + diff --git a/CleanArchitecture.Api/Controllers/ApiController.cs b/CleanArchitecture.Api/Controllers/ApiController.cs index a191d22..c092d04 100644 --- a/CleanArchitecture.Api/Controllers/ApiController.cs +++ b/CleanArchitecture.Api/Controllers/ApiController.cs @@ -62,7 +62,7 @@ public class ApiController : ControllerBase { return HttpStatusCode.NotFound; } - + if (_notifications.GetNotifications().Any(n => n.Code == ErrorCodes.InsufficientPermissions)) { return HttpStatusCode.Forbidden; diff --git a/CleanArchitecture.Api/Controllers/UserController.cs b/CleanArchitecture.Api/Controllers/UserController.cs index 3d822b4..7df8108 100644 --- a/CleanArchitecture.Api/Controllers/UserController.cs +++ b/CleanArchitecture.Api/Controllers/UserController.cs @@ -46,7 +46,7 @@ public class UserController : ApiController var user = await _userService.GetUserByUserIdAsync(id, isDeleted); return Response(user); } - + [Authorize] [HttpGet("me")] [SwaggerOperation("Get the current active user")] @@ -56,7 +56,7 @@ public class UserController : ApiController var user = await _userService.GetCurrentUserAsync(); return Response(user); } - + [HttpPost] [SwaggerOperation("Create a new user")] [SwaggerResponse(200, "Request successful", typeof(ResponseMessage))] diff --git a/CleanArchitecture.Api/Program.cs b/CleanArchitecture.Api/Program.cs index 9a76f47..6bfc87b 100644 --- a/CleanArchitecture.Api/Program.cs +++ b/CleanArchitecture.Api/Program.cs @@ -28,37 +28,36 @@ builder.Services.AddSwaggerGen(c => { Title = "CleanArchitecture", Version = "v1", - Description = "A clean architecture API", + Description = "A clean architecture API" }); c.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme { Description = "JWT Authorization header using the Bearer scheme. " + - "Use the /auth/azureLogin endpoint to generate a token (use the id_token here), " + - "or create a personal access token in centralhub.", + "Use the /api/v1/user/login endpoint to generate a token", Name = "Authorization", In = ParameterLocation.Header, Type = SecuritySchemeType.Http, Scheme = "bearer" }); - c.AddSecurityRequirement(new OpenApiSecurityRequirement() + c.AddSecurityRequirement(new OpenApiSecurityRequirement + { + { + new OpenApiSecurityScheme { + Reference = new OpenApiReference { - new OpenApiSecurityScheme - { - Reference = new OpenApiReference - { - Type = ReferenceType.SecurityScheme, - Id = "Bearer" - }, - Scheme = "oauth2", - Name = "Bearer", - In = ParameterLocation.Header, - }, - new List() - } - }); + Type = ReferenceType.SecurityScheme, + Id = "Bearer" + }, + Scheme = "oauth2", + Name = "Bearer", + In = ParameterLocation.Header + }, + new List() + } + }); }); builder.Services.AddHealthChecks(); @@ -72,15 +71,9 @@ builder.Services.AddDbContext(options => }); builder.Services.AddAuthentication( - options => - { - options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme; - }) + options => { options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme; }) .AddJwtBearer( - jwtOptions => - { - jwtOptions.TokenValidationParameters = CreateTokenValidationParameters(); - }); + jwtOptions => { jwtOptions.TokenValidationParameters = CreateTokenValidationParameters(); }); builder.Services.AddInfrastructure(); builder.Services.AddQueryHandlers(); @@ -94,10 +87,7 @@ builder.Services .Bind(builder.Configuration.GetSection("Auth")) .ValidateOnStart(); -builder.Services.AddMediatR(cfg => -{ - cfg.RegisterServicesFromAssemblies(typeof(Program).Assembly); -}); +builder.Services.AddMediatR(cfg => { cfg.RegisterServicesFromAssemblies(typeof(Program).Assembly); }); var app = builder.Build(); @@ -116,10 +106,10 @@ app.MapControllers(); app.MapHealthChecks("/health"); app.MapGrpcService(); -using (IServiceScope scope = app.Services.CreateScope()) +using (var scope = app.Services.CreateScope()) { var services = scope.ServiceProvider; - ApplicationDbContext appDbContext = services.GetRequiredService(); + var appDbContext = services.GetRequiredService(); appDbContext.EnsureMigrationsApplied(); } @@ -137,8 +127,8 @@ TokenValidationParameters CreateTokenValidationParameters() ValidIssuer = builder.Configuration["Auth:Issuer"], ValidAudience = builder.Configuration["Auth:Audience"], IssuerSigningKey = new SymmetricSecurityKey( - Encoding.UTF8.GetBytes( - builder.Configuration["Auth:Secret"]!)), + Encoding.UTF8.GetBytes( + builder.Configuration["Auth:Secret"]!)), RequireSignedTokens = false }; @@ -146,4 +136,6 @@ TokenValidationParameters CreateTokenValidationParameters() } // Needed for integration tests webapplication factory -public partial class Program { } \ No newline at end of file +public partial class Program +{ +} \ No newline at end of file diff --git a/CleanArchitecture.Application.Tests/CleanArchitecture.Application.Tests.csproj b/CleanArchitecture.Application.Tests/CleanArchitecture.Application.Tests.csproj index 082265f..9af11da 100644 --- a/CleanArchitecture.Application.Tests/CleanArchitecture.Application.Tests.csproj +++ b/CleanArchitecture.Application.Tests/CleanArchitecture.Application.Tests.csproj @@ -8,11 +8,11 @@ - - - - - + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive all @@ -24,8 +24,8 @@ - - + + diff --git a/CleanArchitecture.Application.Tests/Fixtures/Queries/QueryHandlerBaseFixture.cs b/CleanArchitecture.Application.Tests/Fixtures/Queries/QueryHandlerBaseFixture.cs index 204e29c..2d995f8 100644 --- a/CleanArchitecture.Application.Tests/Fixtures/Queries/QueryHandlerBaseFixture.cs +++ b/CleanArchitecture.Application.Tests/Fixtures/Queries/QueryHandlerBaseFixture.cs @@ -7,7 +7,7 @@ namespace CleanArchitecture.Application.Tests.Fixtures.Queries; public class QueryHandlerBaseFixture { public Mock Bus { get; } = new(); - + public QueryHandlerBaseFixture VerifyExistingNotification(string key, string errorCode, string message) { Bus.Verify( diff --git a/CleanArchitecture.Application.Tests/Fixtures/Queries/Users/GetAllUsersTestFixture.cs b/CleanArchitecture.Application.Tests/Fixtures/Queries/Users/GetAllUsersTestFixture.cs index e53901c..9dde37d 100644 --- a/CleanArchitecture.Application.Tests/Fixtures/Queries/Users/GetAllUsersTestFixture.cs +++ b/CleanArchitecture.Application.Tests/Fixtures/Queries/Users/GetAllUsersTestFixture.cs @@ -11,30 +11,30 @@ namespace CleanArchitecture.Application.Tests.Fixtures.Queries.Users; public sealed class GetAllUsersTestFixture : QueryHandlerBaseFixture { + public GetAllUsersTestFixture() + { + UserRepository = new Mock(); + + Handler = new GetAllUsersQueryHandler(UserRepository.Object); + } + private Mock UserRepository { get; } public GetAllUsersQueryHandler Handler { get; } public Guid ExistingUserId { get; } = Guid.NewGuid(); - public GetAllUsersTestFixture() - { - UserRepository = new(); - - Handler = new(UserRepository.Object); - } - public void SetupUserAsync() { var user = new Mock(() => - new User( - ExistingUserId, - "max@mustermann.com", - "Max", - "Mustermann", - "Password", - UserRole.User)); + new User( + ExistingUserId, + "max@mustermann.com", + "Max", + "Mustermann", + "Password", + UserRole.User)); var query = new[] { user.Object }.AsQueryable().BuildMock(); - + UserRepository .Setup(x => x.GetAllNoTracking()) .Returns(query); @@ -43,13 +43,13 @@ public sealed class GetAllUsersTestFixture : QueryHandlerBaseFixture public void SetupDeletedUserAsync() { var user = new Mock(() => - new User( - ExistingUserId, - "max@mustermann.com", - "Max", - "Mustermann", - "Password", - UserRole.User)); + new User( + ExistingUserId, + "max@mustermann.com", + "Max", + "Mustermann", + "Password", + UserRole.User)); user.Object.Delete(); diff --git a/CleanArchitecture.Application.Tests/Fixtures/Queries/Users/GetUserByIdTestFixture.cs b/CleanArchitecture.Application.Tests/Fixtures/Queries/Users/GetUserByIdTestFixture.cs index b7c0201..e175faa 100644 --- a/CleanArchitecture.Application.Tests/Fixtures/Queries/Users/GetUserByIdTestFixture.cs +++ b/CleanArchitecture.Application.Tests/Fixtures/Queries/Users/GetUserByIdTestFixture.cs @@ -11,30 +11,30 @@ namespace CleanArchitecture.Application.Tests.Fixtures.Queries.Users; public sealed class GetUserByIdTestFixture : QueryHandlerBaseFixture { + public GetUserByIdTestFixture() + { + UserRepository = new Mock(); + + Handler = new GetUserByIdQueryHandler(UserRepository.Object, Bus.Object); + } + private Mock UserRepository { get; } public GetUserByIdQueryHandler Handler { get; } public Guid ExistingUserId { get; } = Guid.NewGuid(); - public GetUserByIdTestFixture() - { - UserRepository = new(); - - Handler = new(UserRepository.Object, Bus.Object); - } - public void SetupUserAsync() { var user = new Mock(() => new User( - ExistingUserId, - "max@mustermann.com", - "Max", + ExistingUserId, + "max@mustermann.com", + "Max", "Mustermann", "Password", UserRole.User)); var query = new[] { user.Object }.AsQueryable().BuildMock(); - + UserRepository .Setup(x => x.GetAllNoTracking()) .Returns(query); diff --git a/CleanArchitecture.Application.Tests/Queries/Users/GetAllUsersQueryHandlerTests.cs b/CleanArchitecture.Application.Tests/Queries/Users/GetAllUsersQueryHandlerTests.cs index f1ec70e..7f2f3a5 100644 --- a/CleanArchitecture.Application.Tests/Queries/Users/GetAllUsersQueryHandlerTests.cs +++ b/CleanArchitecture.Application.Tests/Queries/Users/GetAllUsersQueryHandlerTests.cs @@ -1,5 +1,6 @@ using System.Linq; using System.Threading.Tasks; +using CleanArchitecture.Application.Queries.Users.GetAll; using CleanArchitecture.Application.Tests.Fixtures.Queries.Users; using FluentAssertions; using Xunit; @@ -16,11 +17,11 @@ public sealed class GetAllUsersQueryHandlerTests _fixture.SetupUserAsync(); var result = await _fixture.Handler.Handle( - new(), + new GetAllUsersQuery(), default); - + _fixture.VerifyNoDomainNotification(); - + result.Should().NotBeNull(); result.Should().ContainSingle(); result.FirstOrDefault()!.Id.Should().Be(_fixture.ExistingUserId); @@ -32,7 +33,7 @@ public sealed class GetAllUsersQueryHandlerTests _fixture.SetupDeletedUserAsync(); var result = await _fixture.Handler.Handle( - new(), + new GetAllUsersQuery(), default); _fixture.VerifyNoDomainNotification(); diff --git a/CleanArchitecture.Application.Tests/Queries/Users/GetUserByIdQueryHandlerTests.cs b/CleanArchitecture.Application.Tests/Queries/Users/GetUserByIdQueryHandlerTests.cs index a65da4c..4b3aec0 100644 --- a/CleanArchitecture.Application.Tests/Queries/Users/GetUserByIdQueryHandlerTests.cs +++ b/CleanArchitecture.Application.Tests/Queries/Users/GetUserByIdQueryHandlerTests.cs @@ -18,15 +18,15 @@ public sealed class GetUserByIdQueryHandlerTests _fixture.SetupUserAsync(); var result = await _fixture.Handler.Handle( - new(_fixture.ExistingUserId, false), + new GetUserByIdQuery(_fixture.ExistingUserId, false), default); - + _fixture.VerifyNoDomainNotification(); result.Should().NotBeNull(); result!.Id.Should().Be(_fixture.ExistingUserId); } - + [Fact] public async Task Should_Raise_Notification_For_No_User() { @@ -36,7 +36,7 @@ public sealed class GetUserByIdQueryHandlerTests var result = await _fixture.Handler.Handle( request, default); - + _fixture.VerifyExistingNotification( nameof(GetUserByIdQuery), ErrorCodes.ObjectNotFound, @@ -51,7 +51,7 @@ public sealed class GetUserByIdQueryHandlerTests _fixture.SetupDeletedUserAsync(); var result = await _fixture.Handler.Handle( - new(_fixture.ExistingUserId, false), + new GetUserByIdQuery(_fixture.ExistingUserId, false), default); _fixture.VerifyExistingNotification( diff --git a/CleanArchitecture.Application/CleanArchitecture.Application.csproj b/CleanArchitecture.Application/CleanArchitecture.Application.csproj index 2f39ab9..2b5f8ef 100644 --- a/CleanArchitecture.Application/CleanArchitecture.Application.csproj +++ b/CleanArchitecture.Application/CleanArchitecture.Application.csproj @@ -6,11 +6,11 @@ - + - + diff --git a/CleanArchitecture.Application/Extensions/ServiceCollectionExtension.cs b/CleanArchitecture.Application/Extensions/ServiceCollectionExtension.cs index beaec9f..784380f 100644 --- a/CleanArchitecture.Application/Extensions/ServiceCollectionExtension.cs +++ b/CleanArchitecture.Application/Extensions/ServiceCollectionExtension.cs @@ -14,15 +14,15 @@ public static class ServiceCollectionExtension public static IServiceCollection AddServices(this IServiceCollection services) { services.AddScoped(); - + return services; } - + public static IServiceCollection AddQueryHandlers(this IServiceCollection services) { services.AddScoped, GetUserByIdQueryHandler>(); services.AddScoped>, GetAllUsersQueryHandler>(); - + return services; } } \ No newline at end of file diff --git a/CleanArchitecture.Application/Interfaces/IUserService.cs b/CleanArchitecture.Application/Interfaces/IUserService.cs index a04dfcb..1fbca3e 100644 --- a/CleanArchitecture.Application/Interfaces/IUserService.cs +++ b/CleanArchitecture.Application/Interfaces/IUserService.cs @@ -1,6 +1,6 @@ -using System.Threading.Tasks; using System; using System.Collections.Generic; +using System.Threading.Tasks; using CleanArchitecture.Application.ViewModels.Users; namespace CleanArchitecture.Application.Interfaces; diff --git a/CleanArchitecture.Application/Queries/Users/GetAll/GetAllUsersQuery.cs b/CleanArchitecture.Application/Queries/Users/GetAll/GetAllUsersQuery.cs index 40e8571..9e59d7f 100644 --- a/CleanArchitecture.Application/Queries/Users/GetAll/GetAllUsersQuery.cs +++ b/CleanArchitecture.Application/Queries/Users/GetAll/GetAllUsersQuery.cs @@ -4,4 +4,4 @@ using MediatR; namespace CleanArchitecture.Application.Queries.Users.GetAll; -public sealed record GetAllUsersQuery : IRequest>; +public sealed record GetAllUsersQuery : IRequest>; \ No newline at end of file diff --git a/CleanArchitecture.Application/Queries/Users/GetUserById/GetUserByIdQuery.cs b/CleanArchitecture.Application/Queries/Users/GetUserById/GetUserByIdQuery.cs index e394336..b4edf1e 100644 --- a/CleanArchitecture.Application/Queries/Users/GetUserById/GetUserByIdQuery.cs +++ b/CleanArchitecture.Application/Queries/Users/GetUserById/GetUserByIdQuery.cs @@ -4,4 +4,4 @@ using MediatR; namespace CleanArchitecture.Application.Queries.Users.GetUserById; -public sealed record GetUserByIdQuery(Guid UserId, bool IsDeleted) : IRequest; +public sealed record GetUserByIdQuery(Guid UserId, bool IsDeleted) : IRequest; \ No newline at end of file diff --git a/CleanArchitecture.Application/Queries/Users/GetUserById/GetUserByIdQueryHandler.cs b/CleanArchitecture.Application/Queries/Users/GetUserById/GetUserByIdQueryHandler.cs index 2c166a8..f057731 100644 --- a/CleanArchitecture.Application/Queries/Users/GetUserById/GetUserByIdQueryHandler.cs +++ b/CleanArchitecture.Application/Queries/Users/GetUserById/GetUserByIdQueryHandler.cs @@ -13,8 +13,8 @@ namespace CleanArchitecture.Application.Queries.Users.GetUserById; public sealed class GetUserByIdQueryHandler : IRequestHandler { - private readonly IUserRepository _userRepository; private readonly IMediatorHandler _bus; + private readonly IUserRepository _userRepository; public GetUserByIdQueryHandler(IUserRepository userRepository, IMediatorHandler bus) { @@ -26,7 +26,7 @@ public sealed class GetUserByIdQueryHandler : { var user = _userRepository .GetAllNoTracking() - .FirstOrDefault(x => + .FirstOrDefault(x => x.Id == request.UserId && x.Deleted == request.IsDeleted); @@ -42,4 +42,4 @@ public sealed class GetUserByIdQueryHandler : return UserViewModel.FromUser(user); } -} +} \ No newline at end of file diff --git a/CleanArchitecture.Application/Services/UserService.cs b/CleanArchitecture.Application/Services/UserService.cs index ee726a5..da20411 100644 --- a/CleanArchitecture.Application/Services/UserService.cs +++ b/CleanArchitecture.Application/Services/UserService.cs @@ -29,7 +29,7 @@ public sealed class UserService : IUserService { return await _bus.QueryAsync(new GetUserByIdQuery(userId, isDeleted)); } - + public async Task GetCurrentUserAsync() { return await _bus.QueryAsync(new GetUserByIdQuery(_user.GetUserId(), false)); @@ -39,7 +39,7 @@ public sealed class UserService : IUserService { return await _bus.QueryAsync(new GetAllUsersQuery()); } - + public async Task CreateUserAsync(CreateUserViewModel user) { var userId = Guid.NewGuid(); @@ -53,7 +53,7 @@ public sealed class UserService : IUserService return userId; } - + public async Task UpdateUserAsync(UpdateUserViewModel user) { await _bus.SendCommandAsync(new UpdateUserCommand( @@ -63,7 +63,7 @@ public sealed class UserService : IUserService user.GivenName, user.Role)); } - + public async Task DeleteUserAsync(Guid userId) { await _bus.SendCommandAsync(new DeleteUserCommand(userId)); diff --git a/CleanArchitecture.Application/ViewModels/Users/ChangePasswordViewModel.cs b/CleanArchitecture.Application/ViewModels/Users/ChangePasswordViewModel.cs index fb02751..bc35cbb 100644 --- a/CleanArchitecture.Application/ViewModels/Users/ChangePasswordViewModel.cs +++ b/CleanArchitecture.Application/ViewModels/Users/ChangePasswordViewModel.cs @@ -1,3 +1,3 @@ namespace CleanArchitecture.Application.ViewModels.Users; -public sealed record ChangePasswordViewModel(string Password, string NewPassword); +public sealed record ChangePasswordViewModel(string Password, string NewPassword); \ No newline at end of file diff --git a/CleanArchitecture.Application/ViewModels/Users/LoginUserViewModel.cs b/CleanArchitecture.Application/ViewModels/Users/LoginUserViewModel.cs index 3883c05..ac5e7de 100644 --- a/CleanArchitecture.Application/ViewModels/Users/LoginUserViewModel.cs +++ b/CleanArchitecture.Application/ViewModels/Users/LoginUserViewModel.cs @@ -1,3 +1,3 @@ namespace CleanArchitecture.Application.ViewModels.Users; -public sealed record LoginUserViewModel(string Email, string Password); +public sealed record LoginUserViewModel(string Email, string Password); \ No newline at end of file diff --git a/CleanArchitecture.Domain.Tests/CleanArchitecture.Domain.Tests.csproj b/CleanArchitecture.Domain.Tests/CleanArchitecture.Domain.Tests.csproj index 72a5f62..dfa6f77 100644 --- a/CleanArchitecture.Domain.Tests/CleanArchitecture.Domain.Tests.csproj +++ b/CleanArchitecture.Domain.Tests/CleanArchitecture.Domain.Tests.csproj @@ -8,11 +8,11 @@ - - - - - + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive all @@ -24,7 +24,7 @@ - + diff --git a/CleanArchitecture.Domain.Tests/CommandHandler/User/ChangePassword/ChangePasswordCommandHandlerTests.cs b/CleanArchitecture.Domain.Tests/CommandHandler/User/ChangePassword/ChangePasswordCommandHandlerTests.cs index a6fc1e9..08d16ca 100644 --- a/CleanArchitecture.Domain.Tests/CommandHandler/User/ChangePassword/ChangePasswordCommandHandlerTests.cs +++ b/CleanArchitecture.Domain.Tests/CommandHandler/User/ChangePassword/ChangePasswordCommandHandlerTests.cs @@ -60,4 +60,4 @@ public sealed class ChangePasswordCommandHandlerTests DomainErrorCodes.UserPasswordIncorrect, "The password is incorrect"); } -} +} \ No newline at end of file diff --git a/CleanArchitecture.Domain.Tests/CommandHandler/User/ChangePassword/ChangePasswordCommandTestFixture.cs b/CleanArchitecture.Domain.Tests/CommandHandler/User/ChangePassword/ChangePasswordCommandTestFixture.cs index 6162a21..77e8e55 100644 --- a/CleanArchitecture.Domain.Tests/CommandHandler/User/ChangePassword/ChangePasswordCommandTestFixture.cs +++ b/CleanArchitecture.Domain.Tests/CommandHandler/User/ChangePassword/ChangePasswordCommandTestFixture.cs @@ -9,14 +9,11 @@ namespace CleanArchitecture.Domain.Tests.CommandHandler.User.ChangePassword; public sealed class ChangePasswordCommandTestFixture : CommandHandlerFixtureBase { - public ChangePasswordCommandHandler CommandHandler { get; set; } - public Mock UserRepository { get; set; } - public ChangePasswordCommandTestFixture() { UserRepository = new Mock(); - CommandHandler = new( + CommandHandler = new ChangePasswordCommandHandler( Bus.Object, UnitOfWork.Object, NotificationHandler.Object, @@ -24,6 +21,9 @@ public sealed class ChangePasswordCommandTestFixture : CommandHandlerFixtureBase User.Object); } + public ChangePasswordCommandHandler CommandHandler { get; set; } + public Mock UserRepository { get; set; } + public Entities.User SetupUser() { var user = new Entities.User( @@ -49,4 +49,4 @@ public sealed class ChangePasswordCommandTestFixture : CommandHandlerFixtureBase User.Setup(x => x.GetUserId()).Returns(id); return id; } -} +} \ No newline at end of file diff --git a/CleanArchitecture.Domain.Tests/CommandHandler/User/ChangePassword/ChangePasswordCommandValidationTests.cs b/CleanArchitecture.Domain.Tests/CommandHandler/User/ChangePassword/ChangePasswordCommandValidationTests.cs index d68220c..3b0339e 100644 --- a/CleanArchitecture.Domain.Tests/CommandHandler/User/ChangePassword/ChangePasswordCommandValidationTests.cs +++ b/CleanArchitecture.Domain.Tests/CommandHandler/User/ChangePassword/ChangePasswordCommandValidationTests.cs @@ -12,15 +12,15 @@ public sealed class ChangePasswordCommandValidationTests : public ChangePasswordCommandValidationTests() : base(new ChangePasswordCommandValidation()) { } - + [Fact] public void Should_Be_Valid() { var command = CreateTestCommand(); - + ShouldBeValid(command); } - + [Fact] public void Should_Be_Invalid_For_Empty_Password() { @@ -35,60 +35,63 @@ public sealed class ChangePasswordCommandValidationTests : DomainErrorCodes.UserUppercaseLetterPassword, DomainErrorCodes.UserShortPassword }; - + ShouldHaveExpectedErrors(command, errors.ToArray()); } - + [Fact] public void Should_Be_Invalid_For_Missing_Special_Character() { var command = CreateTestCommand("z8tnayvd5FNLU9AQm"); - + ShouldHaveSingleError(command, DomainErrorCodes.UserSpecialCharPassword); } - + [Fact] public void Should_Be_Invalid_For_Missing_Number() { var command = CreateTestCommand("z]tnayvdFNLU:]AQm"); - + ShouldHaveSingleError(command, DomainErrorCodes.UserNumberPassword); } - + [Fact] public void Should_Be_Invalid_For_Missing_Lowercase_Character() { var command = CreateTestCommand("Z8]TNAYVDFNLU:]AQM"); - + ShouldHaveSingleError(command, DomainErrorCodes.UserLowercaseLetterPassword); } - + [Fact] public void Should_Be_Invalid_For_Missing_Uppercase_Character() { var command = CreateTestCommand("z8]tnayvd5fnlu9:]aqm"); - + ShouldHaveSingleError(command, DomainErrorCodes.UserUppercaseLetterPassword); } - + [Fact] public void Should_Be_Invalid_For_Password_Too_Short() { var command = CreateTestCommand("zA6{"); - + ShouldHaveSingleError(command, DomainErrorCodes.UserShortPassword); } - + [Fact] public void Should_Be_Invalid_For_Password_Too_Long() { var command = CreateTestCommand(string.Concat(Enumerable.Repeat("zA6{", 12), 12)); - + ShouldHaveSingleError(command, DomainErrorCodes.UserLongPassword); } private ChangePasswordCommand CreateTestCommand( - string? password = null, string? newPassword = null) => new( - password ?? "z8]tnayvd5FNLU9:]AQm", + string? password = null, string? newPassword = null) + { + return new( + password ?? "z8]tnayvd5FNLU9:]AQm", newPassword ?? "z8]tnayvd5FNLU9:]AQw"); -} + } +} \ No newline at end of file diff --git a/CleanArchitecture.Domain.Tests/CommandHandler/User/CreateUser/CreateUserCommandHandlerTests.cs b/CleanArchitecture.Domain.Tests/CommandHandler/User/CreateUser/CreateUserCommandHandlerTests.cs index 50eaa79..9319274 100644 --- a/CleanArchitecture.Domain.Tests/CommandHandler/User/CreateUser/CreateUserCommandHandlerTests.cs +++ b/CleanArchitecture.Domain.Tests/CommandHandler/User/CreateUser/CreateUserCommandHandlerTests.cs @@ -9,19 +9,19 @@ namespace CleanArchitecture.Domain.Tests.CommandHandler.User.CreateUser; public sealed class CreateUserCommandHandlerTests { private readonly CreateUserCommandTestFixture _fixture = new(); - + [Fact] public void Should_Create_User() { _fixture.SetupUser(); - + var command = new CreateUserCommand( Guid.NewGuid(), "test@email.com", "Test", "Email", "Po=PF]PC6t.?8?ks)A6W"); - + _fixture.CommandHandler.Handle(command, default).Wait(); _fixture @@ -29,19 +29,19 @@ public sealed class CreateUserCommandHandlerTests .VerifyCommit() .VerifyRaisedEvent(x => x.UserId == command.UserId); } - + [Fact] public void Should_Not_Create_Already_Existing_User() { var user = _fixture.SetupUser(); - + var command = new CreateUserCommand( user.Id, "test@email.com", "Test", "Email", "Po=PF]PC6t.?8?ks)A6W"); - + _fixture.CommandHandler.Handle(command, default).Wait(); _fixture diff --git a/CleanArchitecture.Domain.Tests/CommandHandler/User/CreateUser/CreateUserCommandTestFixture.cs b/CleanArchitecture.Domain.Tests/CommandHandler/User/CreateUser/CreateUserCommandTestFixture.cs index c7ba05a..f6879b3 100644 --- a/CleanArchitecture.Domain.Tests/CommandHandler/User/CreateUser/CreateUserCommandTestFixture.cs +++ b/CleanArchitecture.Domain.Tests/CommandHandler/User/CreateUser/CreateUserCommandTestFixture.cs @@ -8,20 +8,20 @@ namespace CleanArchitecture.Domain.Tests.CommandHandler.User.CreateUser; public sealed class CreateUserCommandTestFixture : CommandHandlerFixtureBase { - public CreateUserCommandHandler CommandHandler { get; } - private Mock UserRepository { get; } - public CreateUserCommandTestFixture() { UserRepository = new Mock(); - - CommandHandler = new( + + CommandHandler = new CreateUserCommandHandler( Bus.Object, UnitOfWork.Object, NotificationHandler.Object, UserRepository.Object); } - + + public CreateUserCommandHandler CommandHandler { get; } + private Mock UserRepository { get; } + public Entities.User SetupUser() { var user = new Entities.User( diff --git a/CleanArchitecture.Domain.Tests/CommandHandler/User/CreateUser/CreateUserCommandValidationTests.cs b/CleanArchitecture.Domain.Tests/CommandHandler/User/CreateUser/CreateUserCommandValidationTests.cs index 61a00ad..f3bbe6d 100644 --- a/CleanArchitecture.Domain.Tests/CommandHandler/User/CreateUser/CreateUserCommandValidationTests.cs +++ b/CleanArchitecture.Domain.Tests/CommandHandler/User/CreateUser/CreateUserCommandValidationTests.cs @@ -13,103 +13,103 @@ public sealed class CreateUserCommandValidationTests : public CreateUserCommandValidationTests() : base(new CreateUserCommandValidation()) { } - + [Fact] public void Should_Be_Valid() { var command = CreateTestCommand(); - + ShouldBeValid(command); } - + [Fact] public void Should_Be_Invalid_For_Empty_User_Id() { - var command = CreateTestCommand(userId: Guid.Empty); - + var command = CreateTestCommand(Guid.Empty); + ShouldHaveSingleError( - command, + command, DomainErrorCodes.UserEmptyId, "User id may not be empty"); } - + [Fact] public void Should_Be_Invalid_For_Empty_Email() { var command = CreateTestCommand(email: string.Empty); - + ShouldHaveSingleError( command, DomainErrorCodes.UserInvalidEmail, "Email is not a valid email address"); } - + [Fact] public void Should_Be_Invalid_For_Invalid_Email() { var command = CreateTestCommand(email: "not a email"); - + ShouldHaveSingleError( command, DomainErrorCodes.UserInvalidEmail, "Email is not a valid email address"); } - + [Fact] public void Should_Be_Invalid_For_Email_Exceeds_Max_Length() { var command = CreateTestCommand(email: new string('a', 320) + "@test.com"); - + ShouldHaveSingleError( command, DomainErrorCodes.UserEmailExceedsMaxLength, "Email may not be longer than 320 characters"); } - + [Fact] public void Should_Be_Invalid_For_Empty_Surname() { var command = CreateTestCommand(surName: ""); - + ShouldHaveSingleError( command, DomainErrorCodes.UserEmptySurname, "Surname may not be empty"); } - + [Fact] public void Should_Be_Invalid_For_Surname_Exceeds_Max_Length() { var command = CreateTestCommand(surName: new string('a', 101)); - + ShouldHaveSingleError( command, DomainErrorCodes.UserSurnameExceedsMaxLength, "Surname may not be longer than 100 characters"); } - + [Fact] public void Should_Be_Invalid_For_Empty_Given_Name() { var command = CreateTestCommand(givenName: ""); - + ShouldHaveSingleError( command, DomainErrorCodes.UserEmptyGivenName, "Given name may not be empty"); } - + [Fact] public void Should_Be_Invalid_For_Given_Name_Exceeds_Max_Length() { var command = CreateTestCommand(givenName: new string('a', 101)); - + ShouldHaveSingleError( command, DomainErrorCodes.UserGivenNameExceedsMaxLength, "Given name may not be longer than 100 characters"); } - + [Fact] public void Should_Be_Invalid_For_Empty_Password() { @@ -124,68 +124,70 @@ public sealed class CreateUserCommandValidationTests : DomainErrorCodes.UserUppercaseLetterPassword, DomainErrorCodes.UserShortPassword }; - + ShouldHaveExpectedErrors(command, errors.ToArray()); } - + [Fact] public void Should_Be_Invalid_For_Missing_Special_Character() { var command = CreateTestCommand(password: "z8tnayvd5FNLU9AQm"); - + ShouldHaveSingleError(command, DomainErrorCodes.UserSpecialCharPassword); } - + [Fact] public void Should_Be_Invalid_For_Missing_Number() { var command = CreateTestCommand(password: "z]tnayvdFNLU:]AQm"); - + ShouldHaveSingleError(command, DomainErrorCodes.UserNumberPassword); } - + [Fact] public void Should_Be_Invalid_For_Missing_Lowercase_Character() { var command = CreateTestCommand(password: "Z8]TNAYVDFNLU:]AQM"); - + ShouldHaveSingleError(command, DomainErrorCodes.UserLowercaseLetterPassword); } - + [Fact] public void Should_Be_Invalid_For_Missing_Uppercase_Character() { var command = CreateTestCommand(password: "z8]tnayvd5fnlu9:]aqm"); - + ShouldHaveSingleError(command, DomainErrorCodes.UserUppercaseLetterPassword); } - + [Fact] public void Should_Be_Invalid_For_Password_Too_Short() { var command = CreateTestCommand(password: "zA6{"); - + ShouldHaveSingleError(command, DomainErrorCodes.UserShortPassword); } - + [Fact] public void Should_Be_Invalid_For_Password_Too_Long() { var command = CreateTestCommand(password: string.Concat(Enumerable.Repeat("zA6{", 12), 12)); - + ShouldHaveSingleError(command, DomainErrorCodes.UserLongPassword); } - + private CreateUserCommand CreateTestCommand( Guid? userId = null, string? email = null, string? surName = null, string? givenName = null, - string? password = null) => - new ( + string? password = null) + { + return new( userId ?? Guid.NewGuid(), email ?? "test@email.com", surName ?? "test", givenName ?? "email", password ?? "Po=PF]PC6t.?8?ks)A6W"); + } } \ No newline at end of file diff --git a/CleanArchitecture.Domain.Tests/CommandHandler/User/DeleteUser/DeleteUserCommandHandlerTests.cs b/CleanArchitecture.Domain.Tests/CommandHandler/User/DeleteUser/DeleteUserCommandHandlerTests.cs index dc23bd6..fabfd29 100644 --- a/CleanArchitecture.Domain.Tests/CommandHandler/User/DeleteUser/DeleteUserCommandHandlerTests.cs +++ b/CleanArchitecture.Domain.Tests/CommandHandler/User/DeleteUser/DeleteUserCommandHandlerTests.cs @@ -9,14 +9,14 @@ namespace CleanArchitecture.Domain.Tests.CommandHandler.User.DeleteUser; public sealed class DeleteUserCommandHandlerTests { private readonly DeleteUserCommandTestFixture _fixture = new(); - + [Fact] public void Should_Delete_User() { var user = _fixture.SetupUser(); - + var command = new DeleteUserCommand(user.Id); - + _fixture.CommandHandler.Handle(command, default).Wait(); _fixture @@ -24,14 +24,14 @@ public sealed class DeleteUserCommandHandlerTests .VerifyCommit() .VerifyRaisedEvent(x => x.UserId == user.Id); } - + [Fact] public void Should_Not_Delete_Non_Existing_User() { _fixture.SetupUser(); - + var command = new DeleteUserCommand(Guid.NewGuid()); - + _fixture.CommandHandler.Handle(command, default).Wait(); _fixture diff --git a/CleanArchitecture.Domain.Tests/CommandHandler/User/DeleteUser/DeleteUserCommandTestFixture.cs b/CleanArchitecture.Domain.Tests/CommandHandler/User/DeleteUser/DeleteUserCommandTestFixture.cs index 149480d..aaf2e66 100644 --- a/CleanArchitecture.Domain.Tests/CommandHandler/User/DeleteUser/DeleteUserCommandTestFixture.cs +++ b/CleanArchitecture.Domain.Tests/CommandHandler/User/DeleteUser/DeleteUserCommandTestFixture.cs @@ -8,14 +8,11 @@ namespace CleanArchitecture.Domain.Tests.CommandHandler.User.DeleteUser; public sealed class DeleteUserCommandTestFixture : CommandHandlerFixtureBase { - public DeleteUserCommandHandler CommandHandler { get; } - private Mock UserRepository { get; } - public DeleteUserCommandTestFixture() { UserRepository = new Mock(); - - CommandHandler = new ( + + CommandHandler = new DeleteUserCommandHandler( Bus.Object, UnitOfWork.Object, NotificationHandler.Object, @@ -23,6 +20,9 @@ public sealed class DeleteUserCommandTestFixture : CommandHandlerFixtureBase User.Object); } + public DeleteUserCommandHandler CommandHandler { get; } + private Mock UserRepository { get; } + public Entities.User SetupUser() { var user = new Entities.User( diff --git a/CleanArchitecture.Domain.Tests/CommandHandler/User/DeleteUser/DeleteUserCommandValidationTests.cs b/CleanArchitecture.Domain.Tests/CommandHandler/User/DeleteUser/DeleteUserCommandValidationTests.cs index 50106e5..4ace62f 100644 --- a/CleanArchitecture.Domain.Tests/CommandHandler/User/DeleteUser/DeleteUserCommandValidationTests.cs +++ b/CleanArchitecture.Domain.Tests/CommandHandler/User/DeleteUser/DeleteUserCommandValidationTests.cs @@ -16,21 +16,23 @@ public sealed class DeleteUserCommandValidationTests : public void Should_Be_Valid() { var command = CreateTestCommand(); - + ShouldBeValid(command); } - + [Fact] public void Should_Be_Invalid_For_Empty_User_Id() { - var command = CreateTestCommand(userId: Guid.Empty); - + var command = CreateTestCommand(Guid.Empty); + ShouldHaveSingleError( - command, + command, DomainErrorCodes.UserEmptyId, "User id may not be empty"); } - - private DeleteUserCommand CreateTestCommand(Guid? userId = null) => - new (userId ?? Guid.NewGuid()); + + private DeleteUserCommand CreateTestCommand(Guid? userId = null) + { + return new(userId ?? Guid.NewGuid()); + } } \ No newline at end of file diff --git a/CleanArchitecture.Domain.Tests/CommandHandler/User/LoginUser/LoginUserCommandHandlerTests.cs b/CleanArchitecture.Domain.Tests/CommandHandler/User/LoginUser/LoginUserCommandHandlerTests.cs index 700da13..6137b67 100644 --- a/CleanArchitecture.Domain.Tests/CommandHandler/User/LoginUser/LoginUserCommandHandlerTests.cs +++ b/CleanArchitecture.Domain.Tests/CommandHandler/User/LoginUser/LoginUserCommandHandlerTests.cs @@ -79,4 +79,4 @@ public sealed class LoginUserCommandHandlerTests token.Should().BeEmpty(); } -} +} \ No newline at end of file diff --git a/CleanArchitecture.Domain.Tests/CommandHandler/User/LoginUser/LoginUserCommandTestFixture.cs b/CleanArchitecture.Domain.Tests/CommandHandler/User/LoginUser/LoginUserCommandTestFixture.cs index 8010194..71ee339 100644 --- a/CleanArchitecture.Domain.Tests/CommandHandler/User/LoginUser/LoginUserCommandTestFixture.cs +++ b/CleanArchitecture.Domain.Tests/CommandHandler/User/LoginUser/LoginUserCommandTestFixture.cs @@ -1,20 +1,16 @@ -using CleanArchitecture.Domain.Commands.Users.LoginUser; +using System; +using CleanArchitecture.Domain.Commands.Users.LoginUser; using CleanArchitecture.Domain.Enums; using CleanArchitecture.Domain.Interfaces.Repositories; using CleanArchitecture.Domain.Settings; using Microsoft.Extensions.Options; using Moq; -using System; using BC = BCrypt.Net.BCrypt; namespace CleanArchitecture.Domain.Tests.CommandHandler.User.LoginUser; public sealed class LoginUserCommandTestFixture : CommandHandlerFixtureBase { - public LoginUserCommandHandler CommandHandler { get; set; } - public Mock UserRepository { get; set; } - public IOptions TokenSettings { get; set; } - public LoginUserCommandTestFixture() { UserRepository = new Mock(); @@ -26,7 +22,7 @@ public sealed class LoginUserCommandTestFixture : CommandHandlerFixtureBase Secret = "asjdlkasjd87439284)@#(*" }); - CommandHandler = new( + CommandHandler = new LoginUserCommandHandler( Bus.Object, UnitOfWork.Object, NotificationHandler.Object, @@ -34,6 +30,10 @@ public sealed class LoginUserCommandTestFixture : CommandHandlerFixtureBase TokenSettings); } + public LoginUserCommandHandler CommandHandler { get; set; } + public Mock UserRepository { get; set; } + public IOptions TokenSettings { get; set; } + public Entities.User SetupUser() { var user = new Entities.User( @@ -52,4 +52,4 @@ public sealed class LoginUserCommandTestFixture : CommandHandlerFixtureBase return user; } -} +} \ No newline at end of file diff --git a/CleanArchitecture.Domain.Tests/CommandHandler/User/LoginUser/LoginUserCommandValidationTests.cs b/CleanArchitecture.Domain.Tests/CommandHandler/User/LoginUser/LoginUserCommandValidationTests.cs index 223f711..96fa7c3 100644 --- a/CleanArchitecture.Domain.Tests/CommandHandler/User/LoginUser/LoginUserCommandValidationTests.cs +++ b/CleanArchitecture.Domain.Tests/CommandHandler/User/LoginUser/LoginUserCommandValidationTests.cs @@ -12,48 +12,48 @@ public sealed class LoginUserCommandValidationTests : public LoginUserCommandValidationTests() : base(new LoginUserCommandValidation()) { } - + [Fact] public void Should_Be_Valid() { var command = CreateTestCommand(); - + ShouldBeValid(command); } - + [Fact] public void Should_Be_Invalid_For_Empty_Email() { - var command = CreateTestCommand(email: string.Empty); - + var command = CreateTestCommand(string.Empty); + ShouldHaveSingleError( command, DomainErrorCodes.UserInvalidEmail, "Email is not a valid email address"); } - + [Fact] public void Should_Be_Invalid_For_Invalid_Email() { - var command = CreateTestCommand(email: "not a email"); - + var command = CreateTestCommand("not a email"); + ShouldHaveSingleError( command, DomainErrorCodes.UserInvalidEmail, "Email is not a valid email address"); } - + [Fact] public void Should_Be_Invalid_For_Email_Exceeds_Max_Length() { - var command = CreateTestCommand(email: new string('a', 320) + "@test.com"); - + var command = CreateTestCommand(new string('a', 320) + "@test.com"); + ShouldHaveSingleError( command, DomainErrorCodes.UserEmailExceedsMaxLength, "Email may not be longer than 320 characters"); } - + [Fact] public void Should_Be_Invalid_For_Empty_Password() { @@ -68,62 +68,64 @@ public sealed class LoginUserCommandValidationTests : DomainErrorCodes.UserUppercaseLetterPassword, DomainErrorCodes.UserShortPassword }; - + ShouldHaveExpectedErrors(command, errors.ToArray()); } - + [Fact] public void Should_Be_Invalid_For_Missing_Special_Character() { var command = CreateTestCommand(password: "z8tnayvd5FNLU9AQm"); - + ShouldHaveSingleError(command, DomainErrorCodes.UserSpecialCharPassword); } - + [Fact] public void Should_Be_Invalid_For_Missing_Number() { var command = CreateTestCommand(password: "z]tnayvdFNLU:]AQm"); - + ShouldHaveSingleError(command, DomainErrorCodes.UserNumberPassword); } - + [Fact] public void Should_Be_Invalid_For_Missing_Lowercase_Character() { var command = CreateTestCommand(password: "Z8]TNAYVDFNLU:]AQM"); - + ShouldHaveSingleError(command, DomainErrorCodes.UserLowercaseLetterPassword); } - + [Fact] public void Should_Be_Invalid_For_Missing_Uppercase_Character() { var command = CreateTestCommand(password: "z8]tnayvd5fnlu9:]aqm"); - + ShouldHaveSingleError(command, DomainErrorCodes.UserUppercaseLetterPassword); } - + [Fact] public void Should_Be_Invalid_For_Password_Too_Short() { var command = CreateTestCommand(password: "zA6{"); - + ShouldHaveSingleError(command, DomainErrorCodes.UserShortPassword); } - + [Fact] public void Should_Be_Invalid_For_Password_Too_Long() { var command = CreateTestCommand(password: string.Concat(Enumerable.Repeat("zA6{", 12), 12)); - + ShouldHaveSingleError(command, DomainErrorCodes.UserLongPassword); } - + private LoginUserCommand CreateTestCommand( string? email = null, - string? password = null) => - new ( + string? password = null) + { + return new( email ?? "test@email.com", password ?? "Po=PF]PC6t.?8?ks)A6W"); -} + } +} \ No newline at end of file diff --git a/CleanArchitecture.Domain.Tests/CommandHandler/User/UpdateUser/UpdateUserCommandHandlerTests.cs b/CleanArchitecture.Domain.Tests/CommandHandler/User/UpdateUser/UpdateUserCommandHandlerTests.cs index 3fcb757..98a13df 100644 --- a/CleanArchitecture.Domain.Tests/CommandHandler/User/UpdateUser/UpdateUserCommandHandlerTests.cs +++ b/CleanArchitecture.Domain.Tests/CommandHandler/User/UpdateUser/UpdateUserCommandHandlerTests.cs @@ -11,19 +11,19 @@ namespace CleanArchitecture.Domain.Tests.CommandHandler.User.UpdateUser; public sealed class UpdateUserCommandHandlerTests { private readonly UpdateUserCommandTestFixture _fixture = new(); - + [Fact] public async Task Should_Update_User() { var user = _fixture.SetupUser(); - + var command = new UpdateUserCommand( user.Id, "test@email.com", "Test", "Email", UserRole.User); - + await _fixture.CommandHandler.Handle(command, default); _fixture @@ -31,19 +31,19 @@ public sealed class UpdateUserCommandHandlerTests .VerifyCommit() .VerifyRaisedEvent(x => x.UserId == command.UserId); } - + [Fact] public async Task Should_Not_Update_Non_Existing_User() { _fixture.SetupUser(); - + var command = new UpdateUserCommand( Guid.NewGuid(), "test@email.com", "Test", "Email", UserRole.User); - + await _fixture.CommandHandler.Handle(command, default); _fixture diff --git a/CleanArchitecture.Domain.Tests/CommandHandler/User/UpdateUser/UpdateUserCommandTestFixture.cs b/CleanArchitecture.Domain.Tests/CommandHandler/User/UpdateUser/UpdateUserCommandTestFixture.cs index bb4e581..69bb3ca 100644 --- a/CleanArchitecture.Domain.Tests/CommandHandler/User/UpdateUser/UpdateUserCommandTestFixture.cs +++ b/CleanArchitecture.Domain.Tests/CommandHandler/User/UpdateUser/UpdateUserCommandTestFixture.cs @@ -8,21 +8,21 @@ namespace CleanArchitecture.Domain.Tests.CommandHandler.User.UpdateUser; public sealed class UpdateUserCommandTestFixture : CommandHandlerFixtureBase { - public UpdateUserCommandHandler CommandHandler { get; } - private Mock UserRepository { get; } - public UpdateUserCommandTestFixture() { UserRepository = new Mock(); - - CommandHandler = new( + + CommandHandler = new UpdateUserCommandHandler( Bus.Object, UnitOfWork.Object, NotificationHandler.Object, UserRepository.Object, User.Object); } - + + public UpdateUserCommandHandler CommandHandler { get; } + private Mock UserRepository { get; } + public Entities.User SetupUser() { var user = new Entities.User( diff --git a/CleanArchitecture.Domain.Tests/CommandHandler/User/UpdateUser/UpdateUserCommandValidationTests.cs b/CleanArchitecture.Domain.Tests/CommandHandler/User/UpdateUser/UpdateUserCommandValidationTests.cs index dcf9d11..2fe4e21 100644 --- a/CleanArchitecture.Domain.Tests/CommandHandler/User/UpdateUser/UpdateUserCommandValidationTests.cs +++ b/CleanArchitecture.Domain.Tests/CommandHandler/User/UpdateUser/UpdateUserCommandValidationTests.cs @@ -12,113 +12,115 @@ public sealed class UpdateUserCommandValidationTests : public UpdateUserCommandValidationTests() : base(new UpdateUserCommandValidation()) { } - + [Fact] public void Should_Be_Valid() { var command = CreateTestCommand(); - + ShouldBeValid(command); } - + [Fact] public void Should_Be_Invalid_For_Empty_User_Id() { - var command = CreateTestCommand(userId: Guid.Empty); - + var command = CreateTestCommand(Guid.Empty); + ShouldHaveSingleError( - command, + command, DomainErrorCodes.UserEmptyId, "User id may not be empty"); } - + [Fact] public void Should_Be_Invalid_For_Empty_Email() { var command = CreateTestCommand(email: string.Empty); - + ShouldHaveSingleError( command, DomainErrorCodes.UserInvalidEmail, "Email is not a valid email address"); } - + [Fact] public void Should_Be_Invalid_For_Invalid_Email() { var command = CreateTestCommand(email: "not a email"); - + ShouldHaveSingleError( command, DomainErrorCodes.UserInvalidEmail, "Email is not a valid email address"); } - + [Fact] public void Should_Be_Invalid_For_Email_Exceeds_Max_Length() { var command = CreateTestCommand(email: new string('a', 320) + "@test.com"); - + ShouldHaveSingleError( command, DomainErrorCodes.UserEmailExceedsMaxLength, "Email may not be longer than 320 characters"); } - + [Fact] public void Should_Be_Invalid_For_Empty_Surname() { var command = CreateTestCommand(surName: ""); - + ShouldHaveSingleError( command, DomainErrorCodes.UserEmptySurname, "Surname may not be empty"); } - + [Fact] public void Should_Be_Invalid_For_Surname_Exceeds_Max_Length() { var command = CreateTestCommand(surName: new string('a', 101)); - + ShouldHaveSingleError( command, DomainErrorCodes.UserSurnameExceedsMaxLength, "Surname may not be longer than 100 characters"); } - + [Fact] public void Should_Be_Invalid_For_Empty_Given_Name() { var command = CreateTestCommand(givenName: ""); - + ShouldHaveSingleError( command, DomainErrorCodes.UserEmptyGivenName, "Given name may not be empty"); } - + [Fact] public void Should_Be_Invalid_For_Given_Name_Exceeds_Max_Length() { var command = CreateTestCommand(givenName: new string('a', 101)); - + ShouldHaveSingleError( command, DomainErrorCodes.UserGivenNameExceedsMaxLength, "Given name may not be longer than 100 characters"); } - + private static UpdateUserCommand CreateTestCommand( Guid? userId = null, string? email = null, string? surName = null, string? givenName = null, - UserRole? role = null) => - new ( + UserRole? role = null) + { + return new( userId ?? Guid.NewGuid(), email ?? "test@email.com", surName ?? "test", givenName ?? "email", role ?? UserRole.User); + } } \ No newline at end of file diff --git a/CleanArchitecture.Domain.Tests/CommandHandlerFixtureBase.cs b/CleanArchitecture.Domain.Tests/CommandHandlerFixtureBase.cs index a8e464c..ce8a493 100644 --- a/CleanArchitecture.Domain.Tests/CommandHandlerFixtureBase.cs +++ b/CleanArchitecture.Domain.Tests/CommandHandlerFixtureBase.cs @@ -9,24 +9,24 @@ namespace CleanArchitecture.Domain.Tests; public class CommandHandlerFixtureBase { - protected Mock Bus { get; } - protected Mock UnitOfWork { get; } - protected Mock NotificationHandler { get; } - protected Mock User { get; } - protected CommandHandlerFixtureBase() { Bus = new Mock(); UnitOfWork = new Mock(); NotificationHandler = new Mock(); User = new Mock(); - + User.Setup(x => x.GetUserId()).Returns(Guid.NewGuid()); User.Setup(x => x.GetUserRole()).Returns(UserRole.Admin); UnitOfWork.Setup(unit => unit.CommitAsync()).ReturnsAsync(true); } + protected Mock Bus { get; } + protected Mock UnitOfWork { get; } + protected Mock NotificationHandler { get; } + protected Mock User { get; } + public CommandHandlerFixtureBase VerifyExistingNotification(string errorCode, string message) { Bus.Verify( diff --git a/CleanArchitecture.Domain.Tests/ValidationTestBase.cs b/CleanArchitecture.Domain.Tests/ValidationTestBase.cs index cbb3692..37391ee 100644 --- a/CleanArchitecture.Domain.Tests/ValidationTestBase.cs +++ b/CleanArchitecture.Domain.Tests/ValidationTestBase.cs @@ -8,7 +8,7 @@ namespace CleanArchitecture.Domain.Tests; public class ValidationTestBase where TCommand : CommandBase - where TValidation: AbstractValidator + where TValidation : AbstractValidator { private readonly TValidation _validation; @@ -54,7 +54,7 @@ public class ValidationTestBase } protected void ShouldHaveExpectedErrors( - TCommand command, + TCommand command, params KeyValuePair[] expectedErrors) { var result = _validation.Validate(command); @@ -70,9 +70,9 @@ public class ValidationTestBase .Be(1); } } - + protected void ShouldHaveExpectedErrors( - TCommand command, + TCommand command, params string[] expectedErrors) { var result = _validation.Validate(command); diff --git a/CleanArchitecture.Domain/ApiUser.cs b/CleanArchitecture.Domain/ApiUser.cs index c991c79..15912af 100644 --- a/CleanArchitecture.Domain/ApiUser.cs +++ b/CleanArchitecture.Domain/ApiUser.cs @@ -29,19 +29,6 @@ public sealed class ApiUser : IUser throw new ArgumentException("Could not parse user id to guid"); } - public string GetUserEmail() - { - var claim = _httpContextAccessor.HttpContext?.User.Claims - .FirstOrDefault(x => string.Equals(x.Type, ClaimTypes.Email)); - - if (!string.IsNullOrWhiteSpace(claim?.Value)) - { - return claim?.Value!; - } - - throw new ArgumentException("Could not parse user email"); - } - public UserRole GetUserRole() { var claim = _httpContextAccessor.HttpContext?.User.Claims @@ -56,4 +43,17 @@ public sealed class ApiUser : IUser } public string Name => _httpContextAccessor.HttpContext?.User.Identity?.Name ?? string.Empty; -} + + public string GetUserEmail() + { + var claim = _httpContextAccessor.HttpContext?.User.Claims + .FirstOrDefault(x => string.Equals(x.Type, ClaimTypes.Email)); + + if (!string.IsNullOrWhiteSpace(claim?.Value)) + { + return claim?.Value!; + } + + throw new ArgumentException("Could not parse user email"); + } +} \ No newline at end of file diff --git a/CleanArchitecture.Domain/CleanArchitecture.Domain.csproj b/CleanArchitecture.Domain/CleanArchitecture.Domain.csproj index bd7f84a..3774cae 100644 --- a/CleanArchitecture.Domain/CleanArchitecture.Domain.csproj +++ b/CleanArchitecture.Domain/CleanArchitecture.Domain.csproj @@ -6,12 +6,12 @@ - - - - - - + + + + + + diff --git a/CleanArchitecture.Domain/Commands/CommandBase.cs b/CleanArchitecture.Domain/Commands/CommandBase.cs index 25f84f6..02d78d1 100644 --- a/CleanArchitecture.Domain/Commands/CommandBase.cs +++ b/CleanArchitecture.Domain/Commands/CommandBase.cs @@ -6,17 +6,17 @@ namespace CleanArchitecture.Domain.Commands; public abstract class CommandBase : IRequest { - public Guid AggregateId { get; } - public string MessageType { get; } - public DateTime Timestamp { get; } - public ValidationResult? ValidationResult { get; protected set; } - protected CommandBase(Guid aggregateId) { MessageType = GetType().Name; Timestamp = DateTime.Now; AggregateId = aggregateId; } - + + public Guid AggregateId { get; } + public string MessageType { get; } + public DateTime Timestamp { get; } + public ValidationResult? ValidationResult { get; protected set; } + public abstract bool IsValid(); } \ No newline at end of file diff --git a/CleanArchitecture.Domain/Commands/CommandHandlerBase.cs b/CleanArchitecture.Domain/Commands/CommandHandlerBase.cs index fe9b36c..14d6877 100644 --- a/CleanArchitecture.Domain/Commands/CommandHandlerBase.cs +++ b/CleanArchitecture.Domain/Commands/CommandHandlerBase.cs @@ -10,8 +10,8 @@ namespace CleanArchitecture.Domain.Commands; public abstract class CommandHandlerBase { protected readonly IMediatorHandler _bus; - private readonly IUnitOfWork _unitOfWork; private readonly DomainNotificationHandler _notifications; + private readonly IUnitOfWork _unitOfWork; protected CommandHandlerBase( IMediatorHandler bus, @@ -22,7 +22,7 @@ public abstract class CommandHandlerBase _unitOfWork = unitOfWork; _notifications = (DomainNotificationHandler)notifications; } - + public async Task CommitAsync() { if (_notifications.HasNotifications()) @@ -43,7 +43,7 @@ public abstract class CommandHandlerBase return false; } - + protected async Task NotifyAsync(string key, string message, string code) { await _bus.RaiseEventAsync( @@ -54,7 +54,7 @@ public abstract class CommandHandlerBase { await _bus.RaiseEventAsync(notification); } - + protected async ValueTask TestValidityAsync(CommandBase command) { if (command.IsValid()) @@ -71,8 +71,8 @@ public abstract class CommandHandlerBase { await NotifyAsync( new DomainNotification( - command.MessageType, - error.ErrorMessage, + command.MessageType, + error.ErrorMessage, error.ErrorCode, error.FormattedMessagePlaceholderValues)); } diff --git a/CleanArchitecture.Domain/Commands/Users/ChangePassword/ChangePasswordCommand.cs b/CleanArchitecture.Domain/Commands/Users/ChangePassword/ChangePasswordCommand.cs index 44f0d86..4a27534 100644 --- a/CleanArchitecture.Domain/Commands/Users/ChangePassword/ChangePasswordCommand.cs +++ b/CleanArchitecture.Domain/Commands/Users/ChangePassword/ChangePasswordCommand.cs @@ -6,15 +6,15 @@ public sealed class ChangePasswordCommand : CommandBase { private readonly ChangePasswordCommandValidation _validation = new(); - public string Password { get; } - public string NewPassword { get; } - public ChangePasswordCommand(string password, string newPassword) : base(Guid.NewGuid()) { Password = password; NewPassword = newPassword; } + public string Password { get; } + public string NewPassword { get; } + public override bool IsValid() { ValidationResult = _validation.Validate(this); diff --git a/CleanArchitecture.Domain/Commands/Users/ChangePassword/ChangePasswordCommandHandler.cs b/CleanArchitecture.Domain/Commands/Users/ChangePassword/ChangePasswordCommandHandler.cs index 4bd41e0..8b5ef24 100644 --- a/CleanArchitecture.Domain/Commands/Users/ChangePassword/ChangePasswordCommandHandler.cs +++ b/CleanArchitecture.Domain/Commands/Users/ChangePassword/ChangePasswordCommandHandler.cs @@ -13,8 +13,8 @@ namespace CleanArchitecture.Domain.Commands.Users.ChangePassword; public sealed class ChangePasswordCommandHandler : CommandHandlerBase, IRequestHandler { - private readonly IUserRepository _userRepository; private readonly IUser _user; + private readonly IUserRepository _userRepository; public ChangePasswordCommandHandler( IMediatorHandler bus, @@ -68,4 +68,4 @@ public sealed class ChangePasswordCommandHandler : CommandHandlerBase, await _bus.RaiseEventAsync(new PasswordChangedEvent(user.Id)); } } -} +} \ No newline at end of file diff --git a/CleanArchitecture.Domain/Commands/Users/ChangePassword/ChangePasswordCommandValidation.cs b/CleanArchitecture.Domain/Commands/Users/ChangePassword/ChangePasswordCommandValidation.cs index e90e964..83ea0d9 100644 --- a/CleanArchitecture.Domain/Commands/Users/ChangePassword/ChangePasswordCommandValidation.cs +++ b/CleanArchitecture.Domain/Commands/Users/ChangePassword/ChangePasswordCommandValidation.cs @@ -22,4 +22,4 @@ public sealed class ChangePasswordCommandValidation : AbstractValidator cmd.NewPassword) .Password(); } -} +} \ No newline at end of file diff --git a/CleanArchitecture.Domain/Commands/Users/CreateUser/CreateUserCommand.cs b/CleanArchitecture.Domain/Commands/Users/CreateUser/CreateUserCommand.cs index 7185a16..bafd286 100644 --- a/CleanArchitecture.Domain/Commands/Users/CreateUser/CreateUserCommand.cs +++ b/CleanArchitecture.Domain/Commands/Users/CreateUser/CreateUserCommand.cs @@ -4,13 +4,7 @@ namespace CleanArchitecture.Domain.Commands.Users.CreateUser; public sealed class CreateUserCommand : CommandBase { - private readonly CreateUserCommandValidation _validation = new(); - - public Guid UserId { get; } - public string Email { get; } - public string Surname { get; } - public string GivenName { get; } - public string Password { get; } + private readonly CreateUserCommandValidation _validation = new(); public CreateUserCommand( Guid userId, @@ -26,6 +20,12 @@ public sealed class CreateUserCommand : CommandBase Password = password; } + public Guid UserId { get; } + public string Email { get; } + public string Surname { get; } + public string GivenName { get; } + public string Password { get; } + public override bool IsValid() { ValidationResult = _validation.Validate(this); diff --git a/CleanArchitecture.Domain/Commands/Users/CreateUser/CreateUserCommandHandler.cs b/CleanArchitecture.Domain/Commands/Users/CreateUser/CreateUserCommandHandler.cs index 2f7f511..382a529 100644 --- a/CleanArchitecture.Domain/Commands/Users/CreateUser/CreateUserCommandHandler.cs +++ b/CleanArchitecture.Domain/Commands/Users/CreateUser/CreateUserCommandHandler.cs @@ -16,7 +16,7 @@ public sealed class CreateUserCommandHandler : CommandHandlerBase, IRequestHandler { private readonly IUserRepository _userRepository; - + public CreateUserCommandHandler( IMediatorHandler bus, IUnitOfWork unitOfWork, @@ -44,9 +44,9 @@ public sealed class CreateUserCommandHandler : CommandHandlerBase, DomainErrorCodes.UserAlreadyExists)); return; } - + existingUser = await _userRepository.GetByEmailAsync(request.Email); - + if (existingUser != null) { await _bus.RaiseEventAsync( @@ -60,15 +60,15 @@ public sealed class CreateUserCommandHandler : CommandHandlerBase, var passwordHash = BC.HashPassword(request.Password); var user = new User( - request.UserId, + request.UserId, request.Email, request.Surname, request.GivenName, passwordHash, UserRole.User); - + _userRepository.Add(user); - + if (await CommitAsync()) { await _bus.RaiseEventAsync(new UserCreatedEvent(user.Id)); diff --git a/CleanArchitecture.Domain/Commands/Users/CreateUser/CreateUserCommandValidation.cs b/CleanArchitecture.Domain/Commands/Users/CreateUser/CreateUserCommandValidation.cs index e1b7ed9..23be1aa 100644 --- a/CleanArchitecture.Domain/Commands/Users/CreateUser/CreateUserCommandValidation.cs +++ b/CleanArchitecture.Domain/Commands/Users/CreateUser/CreateUserCommandValidation.cs @@ -22,7 +22,7 @@ public sealed class CreateUserCommandValidation : AbstractValidator cmd.Email) @@ -33,7 +33,7 @@ public sealed class CreateUserCommandValidation : AbstractValidator cmd.Surname) @@ -44,7 +44,7 @@ public sealed class CreateUserCommandValidation : AbstractValidator cmd.GivenName) @@ -55,7 +55,7 @@ public sealed class CreateUserCommandValidation : AbstractValidator cmd.Password) diff --git a/CleanArchitecture.Domain/Commands/Users/DeleteUser/DeleteUserCommand.cs b/CleanArchitecture.Domain/Commands/Users/DeleteUser/DeleteUserCommand.cs index a15d7d6..48c42f5 100644 --- a/CleanArchitecture.Domain/Commands/Users/DeleteUser/DeleteUserCommand.cs +++ b/CleanArchitecture.Domain/Commands/Users/DeleteUser/DeleteUserCommand.cs @@ -4,15 +4,15 @@ namespace CleanArchitecture.Domain.Commands.Users.DeleteUser; public sealed class DeleteUserCommand : CommandBase { - private readonly DeleteUserCommandValidation _validation = new(); - - public Guid UserId { get; } - + private readonly DeleteUserCommandValidation _validation = new(); + public DeleteUserCommand(Guid userId) : base(userId) { UserId = userId; } + public Guid UserId { get; } + public override bool IsValid() { ValidationResult = _validation.Validate(this); diff --git a/CleanArchitecture.Domain/Commands/Users/DeleteUser/DeleteUserCommandHandler.cs b/CleanArchitecture.Domain/Commands/Users/DeleteUser/DeleteUserCommandHandler.cs index 366c240..d40495f 100644 --- a/CleanArchitecture.Domain/Commands/Users/DeleteUser/DeleteUserCommandHandler.cs +++ b/CleanArchitecture.Domain/Commands/Users/DeleteUser/DeleteUserCommandHandler.cs @@ -13,8 +13,8 @@ namespace CleanArchitecture.Domain.Commands.Users.DeleteUser; public sealed class DeleteUserCommandHandler : CommandHandlerBase, IRequestHandler { - private readonly IUserRepository _userRepository; private readonly IUser _user; + private readonly IUserRepository _userRepository; public DeleteUserCommandHandler( IMediatorHandler bus, @@ -35,7 +35,7 @@ public sealed class DeleteUserCommandHandler : CommandHandlerBase, } var user = await _userRepository.GetByIdAsync(request.UserId); - + if (user == null) { await NotifyAsync( @@ -54,7 +54,7 @@ public sealed class DeleteUserCommandHandler : CommandHandlerBase, request.MessageType, $"No permission to delete user {request.UserId}", ErrorCodes.InsufficientPermissions)); - + return; } diff --git a/CleanArchitecture.Domain/Commands/Users/LoginUser/LoginUserCommand.cs b/CleanArchitecture.Domain/Commands/Users/LoginUser/LoginUserCommand.cs index b0a7999..2fcdc00 100644 --- a/CleanArchitecture.Domain/Commands/Users/LoginUser/LoginUserCommand.cs +++ b/CleanArchitecture.Domain/Commands/Users/LoginUser/LoginUserCommand.cs @@ -8,9 +8,6 @@ public sealed class LoginUserCommand : CommandBase, { private readonly LoginUserCommandValidation _validation = new(); - public string Email { get; set; } - public string Password { get; set; } - public LoginUserCommand( string email, @@ -20,6 +17,9 @@ public sealed class LoginUserCommand : CommandBase, Password = password; } + public string Email { get; set; } + public string Password { get; set; } + public override bool IsValid() { ValidationResult = _validation.Validate(this); diff --git a/CleanArchitecture.Domain/Commands/Users/LoginUser/LoginUserCommandHandler.cs b/CleanArchitecture.Domain/Commands/Users/LoginUser/LoginUserCommandHandler.cs index 9998952..c93e2c1 100644 --- a/CleanArchitecture.Domain/Commands/Users/LoginUser/LoginUserCommandHandler.cs +++ b/CleanArchitecture.Domain/Commands/Users/LoginUser/LoginUserCommandHandler.cs @@ -1,6 +1,7 @@ -using System.Security.Claims; +using System; +using System.IdentityModel.Tokens.Jwt; +using System.Security.Claims; using System.Text; -using System; using System.Threading; using System.Threading.Tasks; using CleanArchitecture.Domain.Enums; @@ -10,10 +11,9 @@ using CleanArchitecture.Domain.Interfaces.Repositories; using CleanArchitecture.Domain.Notifications; using CleanArchitecture.Domain.Settings; using MediatR; -using BC = BCrypt.Net.BCrypt; -using System.IdentityModel.Tokens.Jwt; -using Microsoft.IdentityModel.Tokens; using Microsoft.Extensions.Options; +using Microsoft.IdentityModel.Tokens; +using BC = BCrypt.Net.BCrypt; namespace CleanArchitecture.Domain.Commands.Users.LoginUser; @@ -21,9 +21,9 @@ public sealed class LoginUserCommandHandler : CommandHandlerBase, IRequestHandler { private const double ExpiryDurationMinutes = 30; + private readonly TokenSettings _tokenSettings; private readonly IUserRepository _userRepository; - private readonly TokenSettings _tokenSettings; public LoginUserCommandHandler( IMediatorHandler bus, @@ -80,10 +80,10 @@ public sealed class LoginUserCommandHandler : CommandHandlerBase, { var claims = new[] { - new Claim(ClaimTypes.Email, email), - new Claim(ClaimTypes.Role, role.ToString()), - new Claim(ClaimTypes.NameIdentifier, id.ToString()) - }; + new Claim(ClaimTypes.Email, email), + new Claim(ClaimTypes.Role, role.ToString()), + new Claim(ClaimTypes.NameIdentifier, id.ToString()) + }; var securityKey = new SymmetricSecurityKey( Encoding.UTF8.GetBytes(tokenSettings.Secret)); @@ -101,4 +101,4 @@ public sealed class LoginUserCommandHandler : CommandHandlerBase, return new JwtSecurityTokenHandler().WriteToken(tokenDescriptor); } -} +} \ No newline at end of file diff --git a/CleanArchitecture.Domain/Commands/Users/LoginUser/LoginUserCommandValidation.cs b/CleanArchitecture.Domain/Commands/Users/LoginUser/LoginUserCommandValidation.cs index 64bc452..9f668cd 100644 --- a/CleanArchitecture.Domain/Commands/Users/LoginUser/LoginUserCommandValidation.cs +++ b/CleanArchitecture.Domain/Commands/Users/LoginUser/LoginUserCommandValidation.cs @@ -28,4 +28,4 @@ public sealed class LoginUserCommandValidation : AbstractValidator cmd.Password) .Password(); } -} +} \ No newline at end of file diff --git a/CleanArchitecture.Domain/Commands/Users/UpdateUser/UpdateUserCommand.cs b/CleanArchitecture.Domain/Commands/Users/UpdateUser/UpdateUserCommand.cs index d561e47..918bec9 100644 --- a/CleanArchitecture.Domain/Commands/Users/UpdateUser/UpdateUserCommand.cs +++ b/CleanArchitecture.Domain/Commands/Users/UpdateUser/UpdateUserCommand.cs @@ -6,12 +6,6 @@ namespace CleanArchitecture.Domain.Commands.Users.UpdateUser; public sealed class UpdateUserCommand : CommandBase { private readonly UpdateUserCommandValidation _validation = new(); - - public Guid UserId { get; } - public string Email { get; } - public string Surname { get; } - public string GivenName { get; } - public UserRole Role { get; } public UpdateUserCommand( Guid userId, @@ -27,6 +21,12 @@ public sealed class UpdateUserCommand : CommandBase Role = role; } + public Guid UserId { get; } + public string Email { get; } + public string Surname { get; } + public string GivenName { get; } + public UserRole Role { get; } + public override bool IsValid() { ValidationResult = _validation.Validate(this); diff --git a/CleanArchitecture.Domain/Commands/Users/UpdateUser/UpdateUserCommandHandler.cs b/CleanArchitecture.Domain/Commands/Users/UpdateUser/UpdateUserCommandHandler.cs index bbf140c..02d87cd 100644 --- a/CleanArchitecture.Domain/Commands/Users/UpdateUser/UpdateUserCommandHandler.cs +++ b/CleanArchitecture.Domain/Commands/Users/UpdateUser/UpdateUserCommandHandler.cs @@ -1,6 +1,5 @@ using System.Threading; using System.Threading.Tasks; -using CleanArchitecture.Domain.Entities; using CleanArchitecture.Domain.Enums; using CleanArchitecture.Domain.Errors; using CleanArchitecture.Domain.Events.User; @@ -14,8 +13,8 @@ namespace CleanArchitecture.Domain.Commands.Users.UpdateUser; public sealed class UpdateUserCommandHandler : CommandHandlerBase, IRequestHandler { - private readonly IUserRepository _userRepository; private readonly IUser _user; + private readonly IUserRepository _userRepository; public UpdateUserCommandHandler( IMediatorHandler bus, @@ -54,7 +53,7 @@ public sealed class UpdateUserCommandHandler : CommandHandlerBase, request.MessageType, $"No permission to update user {request.UserId}", ErrorCodes.InsufficientPermissions)); - + return; } @@ -68,7 +67,7 @@ public sealed class UpdateUserCommandHandler : CommandHandlerBase, user.SetGivenName(request.GivenName); _userRepository.Update(user); - + if (await CommitAsync()) { await _bus.RaiseEventAsync(new UserUpdatedEvent(user.Id)); diff --git a/CleanArchitecture.Domain/Commands/Users/UpdateUser/UpdateUserCommandValidation.cs b/CleanArchitecture.Domain/Commands/Users/UpdateUser/UpdateUserCommandValidation.cs index 58a901f..f0afc67 100644 --- a/CleanArchitecture.Domain/Commands/Users/UpdateUser/UpdateUserCommandValidation.cs +++ b/CleanArchitecture.Domain/Commands/Users/UpdateUser/UpdateUserCommandValidation.cs @@ -21,7 +21,7 @@ public sealed class UpdateUserCommandValidation : AbstractValidator cmd.Email) @@ -32,7 +32,7 @@ public sealed class UpdateUserCommandValidation : AbstractValidator cmd.Surname) @@ -43,7 +43,7 @@ public sealed class UpdateUserCommandValidation : AbstractValidator cmd.GivenName) diff --git a/CleanArchitecture.Domain/Entities/User.cs b/CleanArchitecture.Domain/Entities/User.cs index 2d14858..b92cd3e 100644 --- a/CleanArchitecture.Domain/Entities/User.cs +++ b/CleanArchitecture.Domain/Entities/User.cs @@ -6,14 +6,6 @@ namespace CleanArchitecture.Domain.Entities; public class User : Entity { - public string Email { get; private set; } - public string GivenName { get; private set; } - public string Surname { get; private set; } - public string Password { get; private set; } - public UserRole Role { get; private set; } - - public string FullName => $"{Surname}, {GivenName}"; - public User( Guid id, string email, @@ -29,6 +21,14 @@ public class User : Entity Role = role; } + public string Email { get; private set; } + public string GivenName { get; private set; } + public string Surname { get; private set; } + public string Password { get; private set; } + public UserRole Role { get; private set; } + + public string FullName => $"{Surname}, {GivenName}"; + [MemberNotNull(nameof(Email))] public void SetEmail(string email) { @@ -45,7 +45,7 @@ public class User : Entity Email = email; } - + [MemberNotNull(nameof(GivenName))] public void SetGivenName(string givenName) { @@ -62,7 +62,7 @@ public class User : Entity GivenName = givenName; } - + [MemberNotNull(nameof(Surname))] public void SetSurname(string surname) { diff --git a/CleanArchitecture.Domain/Enums/UserRole.cs b/CleanArchitecture.Domain/Enums/UserRole.cs index 95c9259..f95d8e1 100644 --- a/CleanArchitecture.Domain/Enums/UserRole.cs +++ b/CleanArchitecture.Domain/Enums/UserRole.cs @@ -4,4 +4,4 @@ public enum UserRole { Admin, User -} +} \ No newline at end of file diff --git a/CleanArchitecture.Domain/Errors/DomainErrorCodes.cs b/CleanArchitecture.Domain/Errors/DomainErrorCodes.cs index 76925e2..6c486cf 100644 --- a/CleanArchitecture.Domain/Errors/DomainErrorCodes.cs +++ b/CleanArchitecture.Domain/Errors/DomainErrorCodes.cs @@ -20,7 +20,7 @@ public static class DomainErrorCodes public const string UserLowercaseLetterPassword = "USER_PASSWORD_MUST_CONTAIN_A_LOWERCASE_LETTER"; public const string UserNumberPassword = "USER_PASSWORD_MUST_CONTAIN_A_NUMBER"; public const string UserSpecialCharPassword = "USER_PASSWORD_MUST_CONTAIN_A_SPECIAL_CHARACTER"; - + // User public const string UserAlreadyExists = "USER_ALREADY_EXISTS"; public const string UserPasswordIncorrect = "USER_PASSWORD_INCORRECT"; diff --git a/CleanArchitecture.Domain/EventHandler/UserEventHandler.cs b/CleanArchitecture.Domain/EventHandler/UserEventHandler.cs index a6f7e47..3e29e28 100644 --- a/CleanArchitecture.Domain/EventHandler/UserEventHandler.cs +++ b/CleanArchitecture.Domain/EventHandler/UserEventHandler.cs @@ -11,7 +11,7 @@ public sealed class UserEventHandler : INotificationHandler, INotificationHandler { - public Task Handle(UserDeletedEvent notification, CancellationToken cancellationToken) + public Task Handle(PasswordChangedEvent notification, CancellationToken cancellationToken) { return Task.CompletedTask; } @@ -20,13 +20,13 @@ public sealed class UserEventHandler : { return Task.CompletedTask; } - - public Task Handle(UserUpdatedEvent notification, CancellationToken cancellationToken) + + public Task Handle(UserDeletedEvent notification, CancellationToken cancellationToken) { return Task.CompletedTask; } - public Task Handle(PasswordChangedEvent notification, CancellationToken cancellationToken) + public Task Handle(UserUpdatedEvent notification, CancellationToken cancellationToken) { return Task.CompletedTask; } diff --git a/CleanArchitecture.Domain/Events/User/PasswordChangedEvent.cs b/CleanArchitecture.Domain/Events/User/PasswordChangedEvent.cs index 32e7a53..7ebb021 100644 --- a/CleanArchitecture.Domain/Events/User/PasswordChangedEvent.cs +++ b/CleanArchitecture.Domain/Events/User/PasswordChangedEvent.cs @@ -4,10 +4,10 @@ namespace CleanArchitecture.Domain.Events.User; public sealed class PasswordChangedEvent : DomainEvent { - public Guid UserId { get; } - public PasswordChangedEvent(Guid userId) : base(userId) { UserId = userId; } -} + + public Guid UserId { get; } +} \ No newline at end of file diff --git a/CleanArchitecture.Domain/Events/User/UserCreatedEvent.cs b/CleanArchitecture.Domain/Events/User/UserCreatedEvent.cs index 19f3658..2a67c9b 100644 --- a/CleanArchitecture.Domain/Events/User/UserCreatedEvent.cs +++ b/CleanArchitecture.Domain/Events/User/UserCreatedEvent.cs @@ -4,10 +4,10 @@ namespace CleanArchitecture.Domain.Events.User; public sealed class UserCreatedEvent : DomainEvent { - public Guid UserId { get; } - public UserCreatedEvent(Guid userId) : base(userId) { UserId = userId; } + + public Guid UserId { get; } } \ No newline at end of file diff --git a/CleanArchitecture.Domain/Events/User/UserDeletedEvent.cs b/CleanArchitecture.Domain/Events/User/UserDeletedEvent.cs index 576bcbf..89fadc1 100644 --- a/CleanArchitecture.Domain/Events/User/UserDeletedEvent.cs +++ b/CleanArchitecture.Domain/Events/User/UserDeletedEvent.cs @@ -4,10 +4,10 @@ namespace CleanArchitecture.Domain.Events.User; public sealed class UserDeletedEvent : DomainEvent { - public Guid UserId { get; } - public UserDeletedEvent(Guid userId) : base(userId) { UserId = userId; } + + public Guid UserId { get; } } \ No newline at end of file diff --git a/CleanArchitecture.Domain/Events/User/UserUpdatedEvent.cs b/CleanArchitecture.Domain/Events/User/UserUpdatedEvent.cs index 996c7e6..92a09cf 100644 --- a/CleanArchitecture.Domain/Events/User/UserUpdatedEvent.cs +++ b/CleanArchitecture.Domain/Events/User/UserUpdatedEvent.cs @@ -4,10 +4,10 @@ namespace CleanArchitecture.Domain.Events.User; public sealed class UserUpdatedEvent : DomainEvent { - public Guid UserId { get; } - public UserUpdatedEvent(Guid userId) : base(userId) { UserId = userId; } + + public Guid UserId { get; } } \ No newline at end of file diff --git a/CleanArchitecture.Domain/Extensions/ServiceCollectionExtension.cs b/CleanArchitecture.Domain/Extensions/ServiceCollectionExtension.cs index d56c338..4c3b2b9 100644 --- a/CleanArchitecture.Domain/Extensions/ServiceCollectionExtension.cs +++ b/CleanArchitecture.Domain/Extensions/ServiceCollectionExtension.cs @@ -25,7 +25,7 @@ public static class ServiceCollectionExtension return services; } - + public static IServiceCollection AddNotificationHandlers(this IServiceCollection services) { // User @@ -41,7 +41,7 @@ public static class ServiceCollectionExtension { // User services.AddScoped(); - + return services; } } \ No newline at end of file diff --git a/CleanArchitecture.Domain/Extensions/Validation/CustomValidator.cs b/CleanArchitecture.Domain/Extensions/Validation/CustomValidator.cs index ecacf87..c94ff0d 100644 --- a/CleanArchitecture.Domain/Extensions/Validation/CustomValidator.cs +++ b/CleanArchitecture.Domain/Extensions/Validation/CustomValidator.cs @@ -17,7 +17,10 @@ public static class CustomValidator return base64.Length % 4 == 0 && Regex.IsMatch(base64, @"^[a-zA-Z0-9\+/]*={0,3}$", RegexOptions.None); } - public static IRuleBuilder Password(this IRuleBuilder ruleBuilder, int minLength = 8, int maxLength = 50) + public static IRuleBuilder Password( + this IRuleBuilder ruleBuilder, + int minLength = 8, + int maxLength = 50) { var options = ruleBuilder .NotEmpty().WithErrorCode(DomainErrorCodes.UserEmptyPassword) diff --git a/CleanArchitecture.Domain/Interfaces/IUser.cs b/CleanArchitecture.Domain/Interfaces/IUser.cs index 55e953c..26cbe1a 100644 --- a/CleanArchitecture.Domain/Interfaces/IUser.cs +++ b/CleanArchitecture.Domain/Interfaces/IUser.cs @@ -5,8 +5,7 @@ namespace CleanArchitecture.Domain.Interfaces; public interface IUser { + string Name { get; } Guid GetUserId(); UserRole GetUserRole(); - - string Name { get; } -} +} \ No newline at end of file diff --git a/CleanArchitecture.Domain/Notifications/DomainNotification.cs b/CleanArchitecture.Domain/Notifications/DomainNotification.cs index c9b2719..e57a891 100644 --- a/CleanArchitecture.Domain/Notifications/DomainNotification.cs +++ b/CleanArchitecture.Domain/Notifications/DomainNotification.cs @@ -4,11 +4,6 @@ namespace CleanArchitecture.Domain.Notifications; public sealed class DomainNotification : DomainEvent { - public string Key { get; private set; } - public string Value { get; private set; } - public string Code { get; private set; } - public object? Data { get; set; } - public DomainNotification( string key, string value, @@ -23,4 +18,9 @@ public sealed class DomainNotification : DomainEvent Data = data; } + + public string Key { get; } + public string Value { get; } + public string Code { get; } + public object? Data { get; set; } } \ No newline at end of file diff --git a/CleanArchitecture.Domain/Notifications/DomainNotificationHandler.cs b/CleanArchitecture.Domain/Notifications/DomainNotificationHandler.cs index d1e579d..59f9235 100644 --- a/CleanArchitecture.Domain/Notifications/DomainNotificationHandler.cs +++ b/CleanArchitecture.Domain/Notifications/DomainNotificationHandler.cs @@ -15,18 +15,18 @@ public class DomainNotificationHandler : INotificationHandler(); } - public virtual List GetNotifications() - { - return _notifications; - } - public Task Handle(DomainNotification notification, CancellationToken cancellationToken = default) { _notifications.Add(notification); return Task.CompletedTask; } - + + public virtual List GetNotifications() + { + return _notifications; + } + public virtual bool HasNotifications() { return GetNotifications().Any(); diff --git a/CleanArchitecture.Infrastructure.Tests/CleanArchitecture.Infrastructure.Tests.csproj b/CleanArchitecture.Infrastructure.Tests/CleanArchitecture.Infrastructure.Tests.csproj index 574a54c..45957b8 100644 --- a/CleanArchitecture.Infrastructure.Tests/CleanArchitecture.Infrastructure.Tests.csproj +++ b/CleanArchitecture.Infrastructure.Tests/CleanArchitecture.Infrastructure.Tests.csproj @@ -8,10 +8,10 @@ - - - - + + + + runtime; build; native; contentfiles; analyzers; buildtransitive all @@ -23,7 +23,7 @@ - + diff --git a/CleanArchitecture.Infrastructure.Tests/DomainNotificationHandlerTests.cs b/CleanArchitecture.Infrastructure.Tests/DomainNotificationHandlerTests.cs index 95148af..4ebf2a2 100644 --- a/CleanArchitecture.Infrastructure.Tests/DomainNotificationHandlerTests.cs +++ b/CleanArchitecture.Infrastructure.Tests/DomainNotificationHandlerTests.cs @@ -16,9 +16,9 @@ public sealed class DomainNotificationHandlerTests [Fact] public void Should_Handle_DomainNotification() { - string key = "Key"; - string value = "Value"; - string code = "Code"; + var key = "Key"; + var value = "Value"; + var code = "Code"; var domainNotification = new DomainNotification(key, value, code); var domainNotificationHandler = new DomainNotificationHandler(); @@ -29,9 +29,9 @@ public sealed class DomainNotificationHandlerTests [Fact] public void Should_Handle_DomainNotification_Overload() { - string key = "Key"; - string value = "Value"; - string code = "Code"; + var key = "Key"; + var value = "Value"; + var code = "Code"; var domainNotification = new DomainNotification(key, value, code); var domainNotificationHandler = new DomainNotificationHandler(); @@ -42,9 +42,9 @@ public sealed class DomainNotificationHandlerTests [Fact] public void DomainNotification_HasNotifications_After_Handling_One() { - string key = "Key"; - string value = "Value"; - string code = "Code"; + var key = "Key"; + var value = "Value"; + var code = "Code"; var domainNotification = new DomainNotification(key, value, code); var domainNotificationHandler = new DomainNotificationHandler(); @@ -60,4 +60,4 @@ public sealed class DomainNotificationHandlerTests domainNotificationHandler.HasNotifications().Should().BeFalse(); } -} +} \ No newline at end of file diff --git a/CleanArchitecture.Infrastructure.Tests/DomainNotificationTests.cs b/CleanArchitecture.Infrastructure.Tests/DomainNotificationTests.cs index d942a92..f34d1c0 100644 --- a/CleanArchitecture.Infrastructure.Tests/DomainNotificationTests.cs +++ b/CleanArchitecture.Infrastructure.Tests/DomainNotificationTests.cs @@ -10,9 +10,9 @@ public sealed class DomainNotificationTests [Fact] public void Should_Create_DomainNotification_Instance() { - string key = "Key"; - string value = "Value"; - string code = "Code"; + var key = "Key"; + var value = "Value"; + var code = "Code"; var domainNotification = new DomainNotification( key, value, code); @@ -26,9 +26,9 @@ public sealed class DomainNotificationTests [Fact] public void Should_Create_DomainNotification_Overload_Instance() { - string key = "Key"; - string value = "Value"; - string code = "Code"; + var key = "Key"; + var value = "Value"; + var code = "Code"; var domainNotification = new DomainNotification( key, value, code); @@ -38,4 +38,4 @@ public sealed class DomainNotificationTests domainNotification.Code.Should().Be(code); domainNotification.Should().NotBe(default(Guid)); } -} +} \ No newline at end of file diff --git a/CleanArchitecture.Infrastructure.Tests/UnitOfWorkTests.cs b/CleanArchitecture.Infrastructure.Tests/UnitOfWorkTests.cs index 2a0e51f..5700ae1 100644 --- a/CleanArchitecture.Infrastructure.Tests/UnitOfWorkTests.cs +++ b/CleanArchitecture.Infrastructure.Tests/UnitOfWorkTests.cs @@ -19,18 +19,18 @@ public sealed class UnitOfWorkTests var options = new DbContextOptionsBuilder(); var dbContextMock = new Mock(options.Options); var loggerMock = new Mock>>(); - + dbContextMock .Setup(x => x.SaveChangesAsync(CancellationToken.None)) .Returns(Task.FromResult(1)); - + var unitOfWork = UnitOfWorkTestFixture.GetUnitOfWork(dbContextMock.Object, loggerMock.Object); - + var result = await unitOfWork.CommitAsync(); result.Should().BeTrue(); } - + [Fact] public async Task Should_Commit_Async_Returns_False() { @@ -45,7 +45,7 @@ public sealed class UnitOfWorkTests var unitOfWork = UnitOfWorkTestFixture.GetUnitOfWork(dbContextMock.Object, loggerMock.Object); var result = await unitOfWork.CommitAsync(); - + result.Should().BeFalse(); } diff --git a/CleanArchitecture.Infrastructure/CleanArchitecture.Infrastructure.csproj b/CleanArchitecture.Infrastructure/CleanArchitecture.Infrastructure.csproj index 6b37ff6..01399f8 100644 --- a/CleanArchitecture.Infrastructure/CleanArchitecture.Infrastructure.csproj +++ b/CleanArchitecture.Infrastructure/CleanArchitecture.Infrastructure.csproj @@ -6,18 +6,18 @@ - + - - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/CleanArchitecture.Infrastructure/Configurations/UserConfiguration.cs b/CleanArchitecture.Infrastructure/Configurations/UserConfiguration.cs index 6fe7426..4763c9c 100644 --- a/CleanArchitecture.Infrastructure/Configurations/UserConfiguration.cs +++ b/CleanArchitecture.Infrastructure/Configurations/UserConfiguration.cs @@ -24,7 +24,7 @@ public sealed class UserConfiguration : IEntityTypeConfiguration .Property(user => user.Surname) .IsRequired() .HasMaxLength(100); - + builder .Property(user => user.Password) .IsRequired() diff --git a/CleanArchitecture.Infrastructure/Database/ApplicationDbContext.cs b/CleanArchitecture.Infrastructure/Database/ApplicationDbContext.cs index f4b4323..4280c4d 100644 --- a/CleanArchitecture.Infrastructure/Database/ApplicationDbContext.cs +++ b/CleanArchitecture.Infrastructure/Database/ApplicationDbContext.cs @@ -6,12 +6,12 @@ namespace CleanArchitecture.Infrastructure.Database; public class ApplicationDbContext : DbContext { - public DbSet Users { get; set; } = null!; - public ApplicationDbContext(DbContextOptions options) : base(options) { } + public DbSet Users { get; set; } = null!; + protected override void OnModelCreating(ModelBuilder builder) { builder.ApplyConfiguration(new UserConfiguration()); diff --git a/CleanArchitecture.Infrastructure/Extensions/DbContextExtension.cs b/CleanArchitecture.Infrastructure/Extensions/DbContextExtension.cs index 9192d48..869c1ea 100644 --- a/CleanArchitecture.Infrastructure/Extensions/DbContextExtension.cs +++ b/CleanArchitecture.Infrastructure/Extensions/DbContextExtension.cs @@ -18,4 +18,4 @@ public static class DbContextExtension context.Database.Migrate(); } } -} +} \ No newline at end of file diff --git a/CleanArchitecture.Infrastructure/Extensions/ServiceCollectionExtensions.cs b/CleanArchitecture.Infrastructure/Extensions/ServiceCollectionExtensions.cs index ff72bb8..a7a68a8 100644 --- a/CleanArchitecture.Infrastructure/Extensions/ServiceCollectionExtensions.cs +++ b/CleanArchitecture.Infrastructure/Extensions/ServiceCollectionExtensions.cs @@ -22,4 +22,4 @@ public static class ServiceCollectionExtensions return services; } -} +} \ No newline at end of file diff --git a/CleanArchitecture.Infrastructure/InMemoryBus.cs b/CleanArchitecture.Infrastructure/InMemoryBus.cs index 5896d83..2df646c 100644 --- a/CleanArchitecture.Infrastructure/InMemoryBus.cs +++ b/CleanArchitecture.Infrastructure/InMemoryBus.cs @@ -14,7 +14,7 @@ public sealed class InMemoryBus : IMediatorHandler { _mediator = mediator; } - + public Task QueryAsync(IRequest query) { return _mediator.Send(query); diff --git a/CleanArchitecture.Infrastructure/Repositories/BaseRepository.cs b/CleanArchitecture.Infrastructure/Repositories/BaseRepository.cs index 52b768d..db13d69 100644 --- a/CleanArchitecture.Infrastructure/Repositories/BaseRepository.cs +++ b/CleanArchitecture.Infrastructure/Repositories/BaseRepository.cs @@ -50,19 +50,6 @@ public class BaseRepository : IRepository where TEntity : Enti return await DbSet.FindAsync(id); } - public int SaveChanges() - { - return _dbContext.SaveChanges(); - } - - protected virtual void Dispose(bool disposing) - { - if (disposing) - { - _dbContext.Dispose(); - } - } - public virtual void Update(TEntity entity) { DbSet.Update(entity); @@ -72,7 +59,7 @@ public class BaseRepository : IRepository where TEntity : Enti { return DbSet.AnyAsync(entity => entity.Id == id); } - + public void Remove(TEntity entity, bool hardDelete = false) { if (hardDelete) @@ -86,4 +73,16 @@ public class BaseRepository : IRepository where TEntity : Enti } } + public int SaveChanges() + { + return _dbContext.SaveChanges(); + } + + protected virtual void Dispose(bool disposing) + { + if (disposing) + { + _dbContext.Dispose(); + } + } } \ No newline at end of file diff --git a/CleanArchitecture.Infrastructure/UnitOfWork.cs b/CleanArchitecture.Infrastructure/UnitOfWork.cs index 97d88a9..735590e 100644 --- a/CleanArchitecture.Infrastructure/UnitOfWork.cs +++ b/CleanArchitecture.Infrastructure/UnitOfWork.cs @@ -45,4 +45,4 @@ public sealed class UnitOfWork : IUnitOfWork where TContext : DbContex _context.Dispose(); } } -} +} \ No newline at end of file diff --git a/CleanArchitecture.IntegrationTests/CleanArchitecture.IntegrationTests.csproj b/CleanArchitecture.IntegrationTests/CleanArchitecture.IntegrationTests.csproj index 5f128a0..5768a6d 100644 --- a/CleanArchitecture.IntegrationTests/CleanArchitecture.IntegrationTests.csproj +++ b/CleanArchitecture.IntegrationTests/CleanArchitecture.IntegrationTests.csproj @@ -8,14 +8,14 @@ - - - - - - - - + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive all @@ -27,8 +27,8 @@ - - + + diff --git a/CleanArchitecture.IntegrationTests/Controller/UserControllerTests.cs b/CleanArchitecture.IntegrationTests/Controller/UserControllerTests.cs index 357dd35..a06ecfa 100644 --- a/CleanArchitecture.IntegrationTests/Controller/UserControllerTests.cs +++ b/CleanArchitecture.IntegrationTests/Controller/UserControllerTests.cs @@ -24,12 +24,13 @@ public sealed class UserControllerTests : IClassFixture _fixture = fixture; } - [Fact, Priority(0)] + [Fact] + [Priority(0)] public async Task Should_Create_User() { var user = new CreateUserViewModel( - _fixture.CreatedUserEmail, - "Test", + _fixture.CreatedUserEmail, + "Test", "Email", _fixture.CreatedUserPassword); @@ -43,12 +44,13 @@ public sealed class UserControllerTests : IClassFixture _fixture.CreatedUserId = message!.Data; } - - [Fact, Priority(5)] + + [Fact] + [Priority(5)] public async Task Should_Login_User() { var user = new LoginUserViewModel( - _fixture.CreatedUserEmail, + _fixture.CreatedUserEmail, _fixture.CreatedUserPassword); var response = await _fixture.ServerClient.PostAsJsonAsync("/api/v1/user/login", user); @@ -63,7 +65,8 @@ public sealed class UserControllerTests : IClassFixture _fixture.EnableAuthentication(); } - [Fact, Priority(10)] + [Fact] + [Priority(10)] public async Task Should_Get_Created_Users() { var response = await _fixture.ServerClient.GetAsync("/api/v1/user/" + _fixture.CreatedUserId); @@ -81,8 +84,9 @@ public sealed class UserControllerTests : IClassFixture content.Surname.Should().Be("Test"); content.GivenName.Should().Be("Email"); } - - [Fact, Priority(10)] + + [Fact] + [Priority(10)] public async Task Should_Get_The_Current_Active_Users() { var response = await _fixture.ServerClient.GetAsync("/api/v1/user/me"); @@ -101,7 +105,8 @@ public sealed class UserControllerTests : IClassFixture content.GivenName.Should().Be("Email"); } - [Fact, Priority(15)] + [Fact] + [Priority(15)] public async Task Should_Update_User() { var user = new UpdateUserViewModel( @@ -124,7 +129,8 @@ public sealed class UserControllerTests : IClassFixture content.Should().BeEquivalentTo(user); } - [Fact, Priority(20)] + [Fact] + [Priority(20)] public async Task Should_Get_Updated_Users() { var response = await _fixture.ServerClient.GetAsync("/api/v1/user/" + _fixture.CreatedUserId); @@ -141,11 +147,12 @@ public sealed class UserControllerTests : IClassFixture content.Email.Should().Be("newtest@email.com"); content.Surname.Should().Be("NewTest"); content.GivenName.Should().Be("NewEmail"); - + _fixture.CreatedUserEmail = content.Email; } - - [Fact, Priority(25)] + + [Fact] + [Priority(25)] public async Task Should_Change_User_Password() { var user = new ChangePasswordViewModel( @@ -163,10 +170,10 @@ public sealed class UserControllerTests : IClassFixture var content = message!.Data; content.Should().BeEquivalentTo(user); - + // Verify the user can login with the new password var login = new LoginUserViewModel( - _fixture.CreatedUserEmail, + _fixture.CreatedUserEmail, _fixture.CreatedUserPassword + "1"); var loginResponse = await _fixture.ServerClient.PostAsJsonAsync("/api/v1/user/login", login); @@ -178,7 +185,8 @@ public sealed class UserControllerTests : IClassFixture loginMessage?.Data.Should().NotBeEmpty(); } - [Fact, Priority(30)] + [Fact] + [Priority(30)] public async Task Should_Get_All_User() { var response = await _fixture.ServerClient.GetAsync("/api/v1/user"); @@ -192,23 +200,24 @@ public sealed class UserControllerTests : IClassFixture var content = message!.Data!.ToList(); content.Count.Should().Be(2); - + var currentUser = content.First(x => x.Id == _fixture.CreatedUserId); - + currentUser.Id.Should().Be(_fixture.CreatedUserId); currentUser.Role.Should().Be(UserRole.User); currentUser.Email.Should().Be("newtest@email.com"); currentUser.Surname.Should().Be("NewTest"); currentUser.GivenName.Should().Be("NewEmail"); - + var adminUser = content.First(x => x.Role == UserRole.Admin); - + adminUser.Email.Should().Be("admin@email.com"); adminUser.Surname.Should().Be("Admin"); adminUser.GivenName.Should().Be("User"); } - [Fact, Priority(35)] + [Fact] + [Priority(35)] public async Task Should_Delete_User() { var response = await _fixture.ServerClient.DeleteAsync("/api/v1/user/" + _fixture.CreatedUserId); @@ -222,4 +231,4 @@ public sealed class UserControllerTests : IClassFixture var content = message!.Data; content.Should().Be(_fixture.CreatedUserId); } -} +} \ No newline at end of file diff --git a/CleanArchitecture.IntegrationTests/Extensions/FunctionalTestsServiceCollectionExtensions.cs b/CleanArchitecture.IntegrationTests/Extensions/FunctionalTestsServiceCollectionExtensions.cs index 8cb7fad..d81e2c0 100644 --- a/CleanArchitecture.IntegrationTests/Extensions/FunctionalTestsServiceCollectionExtensions.cs +++ b/CleanArchitecture.IntegrationTests/Extensions/FunctionalTestsServiceCollectionExtensions.cs @@ -2,28 +2,29 @@ using System.Collections.Generic; using System.Data.Common; using System.Linq; -using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.DependencyInjection; using Microsoft.EntityFrameworkCore.Diagnostics; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.Extensions.DependencyInjection; namespace CleanArchitecture.IntegrationTests.Extensions; public static class FunctionalTestsServiceCollectionExtensions { - public static IServiceCollection SetupTestDatabase(this IServiceCollection services, DbConnection connection) where TContext : DbContext + public static IServiceCollection SetupTestDatabase(this IServiceCollection services, + DbConnection connection) where TContext : DbContext { var descriptor = services.SingleOrDefault(d => d.ServiceType == typeof(DbContextOptions)); if (descriptor != null) services.Remove(descriptor); services.AddScoped(p => - DbContextOptionsFactory( - p, - (_, options) => options - .ConfigureWarnings(b => b.Log(CoreEventId.ManyServiceProvidersCreatedWarning)) - .UseLazyLoadingProxies() - .UseSqlite(connection))); + DbContextOptionsFactory( + p, + (_, options) => options + .ConfigureWarnings(b => b.Log(CoreEventId.ManyServiceProvidersCreatedWarning)) + .UseLazyLoadingProxies() + .UseSqlite(connection))); return services; } @@ -42,4 +43,4 @@ public static class FunctionalTestsServiceCollectionExtensions return builder.Options; } -} +} \ No newline at end of file diff --git a/CleanArchitecture.IntegrationTests/Extensions/HttpExtensions.cs b/CleanArchitecture.IntegrationTests/Extensions/HttpExtensions.cs index f765a9c..3ac2378 100644 --- a/CleanArchitecture.IntegrationTests/Extensions/HttpExtensions.cs +++ b/CleanArchitecture.IntegrationTests/Extensions/HttpExtensions.cs @@ -10,7 +10,7 @@ public static class HttpExtensions { private static readonly JsonSerializerOptions JsonSerializerOptions = new() { - PropertyNameCaseInsensitive = true, + PropertyNameCaseInsensitive = true }; private static T? Deserialize(string json) @@ -55,4 +55,4 @@ public static class HttpExtensions return httpClient.PutAsync(url, content); } -} +} \ No newline at end of file diff --git a/CleanArchitecture.IntegrationTests/Fixtures/TestFixtureBase.cs b/CleanArchitecture.IntegrationTests/Fixtures/TestFixtureBase.cs index 2d4994e..cfe5a52 100644 --- a/CleanArchitecture.IntegrationTests/Fixtures/TestFixtureBase.cs +++ b/CleanArchitecture.IntegrationTests/Fixtures/TestFixtureBase.cs @@ -9,8 +9,6 @@ namespace CleanArchitecture.IntegrationTests.Fixtures; public class TestFixtureBase { - public HttpClient ServerClient { get; } - public TestFixtureBase() { var projectDir = Directory.GetCurrentDirectory(); @@ -25,6 +23,8 @@ public class TestFixtureBase ServerClient.Timeout = TimeSpan.FromMinutes(5); } + public HttpClient ServerClient { get; } + protected virtual void SeedTestData(ApplicationDbContext context) { } @@ -35,4 +35,4 @@ public class TestFixtureBase IServiceProvider scopedServices) { } -} +} \ No newline at end of file diff --git a/CleanArchitecture.IntegrationTests/Fixtures/UserTestFixture.cs b/CleanArchitecture.IntegrationTests/Fixtures/UserTestFixture.cs index 3d8fa2e..5769927 100644 --- a/CleanArchitecture.IntegrationTests/Fixtures/UserTestFixture.cs +++ b/CleanArchitecture.IntegrationTests/Fixtures/UserTestFixture.cs @@ -8,9 +8,9 @@ public sealed class UserTestFixture : TestFixtureBase public string CreatedUserEmail { get; set; } = "test@email.com"; public string CreatedUserPassword { get; set; } = "z8]tnayvd5FNLU9:]AQm"; public string CreatedUserToken { get; set; } = string.Empty; - + public void EnableAuthentication() { ServerClient.DefaultRequestHeaders.Add("Authorization", $"Bearer {CreatedUserToken}"); } -} +} \ No newline at end of file diff --git a/CleanArchitecture.IntegrationTests/Infrastructure/CleanArchitectureWebApplicationFactory.cs b/CleanArchitecture.IntegrationTests/Infrastructure/CleanArchitectureWebApplicationFactory.cs index 0e9f243..b35bbdf 100644 --- a/CleanArchitecture.IntegrationTests/Infrastructure/CleanArchitectureWebApplicationFactory.cs +++ b/CleanArchitecture.IntegrationTests/Infrastructure/CleanArchitectureWebApplicationFactory.cs @@ -18,11 +18,11 @@ public sealed class CleanArchitectureWebApplicationFactory : WebApplicationFacto ServiceProvider serviceProvider, IServiceProvider scopedServices); - private readonly SqliteConnection _connection = new($"DataSource=:memory:"); - private readonly AddCustomSeedDataHandler? _addCustomSeedDataHandler; - private readonly RegisterCustomServicesHandler? _registerCustomServicesHandler; + + private readonly SqliteConnection _connection = new("DataSource=:memory:"); private readonly string? _environment; + private readonly RegisterCustomServicesHandler? _registerCustomServicesHandler; public CleanArchitectureWebApplicationFactory( AddCustomSeedDataHandler? addCustomSeedDataHandler, @@ -51,7 +51,7 @@ public sealed class CleanArchitectureWebApplicationFactory : WebApplicationFacto var sp = services.BuildServiceProvider(); - using IServiceScope scope = sp.CreateScope(); + using var scope = sp.CreateScope(); var scopedServices = scope.ServiceProvider; var applicationDbContext = scopedServices.GetRequiredService(); @@ -62,4 +62,4 @@ public sealed class CleanArchitectureWebApplicationFactory : WebApplicationFacto _registerCustomServicesHandler?.Invoke(services, sp, scopedServices); }); } -} +} \ No newline at end of file diff --git a/CleanArchitecture.Proto/CleanArchitecture.Proto.csproj b/CleanArchitecture.Proto/CleanArchitecture.Proto.csproj index 1f914a1..de1ac9c 100644 --- a/CleanArchitecture.Proto/CleanArchitecture.Proto.csproj +++ b/CleanArchitecture.Proto/CleanArchitecture.Proto.csproj @@ -6,19 +6,19 @@ - - + + - - + + - - - + + + diff --git a/CleanArchitecture.gRPC.Tests/CleanArchitecture.gRPC.Tests.csproj b/CleanArchitecture.gRPC.Tests/CleanArchitecture.gRPC.Tests.csproj index 6d11a38..b6443c4 100644 --- a/CleanArchitecture.gRPC.Tests/CleanArchitecture.gRPC.Tests.csproj +++ b/CleanArchitecture.gRPC.Tests/CleanArchitecture.gRPC.Tests.csproj @@ -8,11 +8,11 @@ - - - - - + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive all @@ -24,8 +24,8 @@ - - + + diff --git a/CleanArchitecture.gRPC.Tests/Fixtures/UserTestsFixture.cs b/CleanArchitecture.gRPC.Tests/Fixtures/UserTestsFixture.cs index 9cac355..5b04bef 100644 --- a/CleanArchitecture.gRPC.Tests/Fixtures/UserTestsFixture.cs +++ b/CleanArchitecture.gRPC.Tests/Fixtures/UserTestsFixture.cs @@ -11,37 +11,31 @@ namespace CleanArchitecture.gRPC.Tests.Fixtures; public sealed class UserTestsFixture { - private Mock UserRepository { get; } = new (); - - public UsersApiImplementation UsersApiImplementation { get; } - - public IEnumerable ExistingUsers { get; } - public UserTestsFixture() { - ExistingUsers = new List() + ExistingUsers = new List { - new ( - Guid.NewGuid(), - "test@test.de", - "Test First Name", + new( + Guid.NewGuid(), + "test@test.de", + "Test First Name", "Test Last Name", "Test Password", UserRole.User), - new ( - Guid.NewGuid(), - "email@Email.de", - "Email First Name", + new( + Guid.NewGuid(), + "email@Email.de", + "Email First Name", "Email Last Name", "Email Password", UserRole.Admin), - new ( - Guid.NewGuid(), - "user@user.de", - "User First Name", + new( + Guid.NewGuid(), + "user@user.de", + "User First Name", "User Last Name", "User Password", - UserRole.User), + UserRole.User) }; var queryable = ExistingUsers.AsQueryable().BuildMock(); @@ -52,4 +46,10 @@ public sealed class UserTestsFixture UsersApiImplementation = new UsersApiImplementation(UserRepository.Object); } + + private Mock UserRepository { get; } = new(); + + public UsersApiImplementation UsersApiImplementation { get; } + + public IEnumerable ExistingUsers { get; } } \ No newline at end of file diff --git a/CleanArchitecture.gRPC/CleanArchitecture.gRPC.csproj b/CleanArchitecture.gRPC/CleanArchitecture.gRPC.csproj index 82b6f59..1268e7f 100644 --- a/CleanArchitecture.gRPC/CleanArchitecture.gRPC.csproj +++ b/CleanArchitecture.gRPC/CleanArchitecture.gRPC.csproj @@ -6,12 +6,12 @@ - - + + - + From 96b82e2d6fbd67e2dab1fe2514a21919749d5ecd Mon Sep 17 00:00:00 2001 From: alex289 Date: Wed, 22 Mar 2023 19:25:44 +0100 Subject: [PATCH 09/10] Apply suggestions and fix warnings --- .../Controllers/UserController.cs | 4 +-- CleanArchitecture.Api/Program.cs | 2 +- .../Users/GetAllUsersQueryHandlerTests.cs | 7 ++--- .../viewmodels/Users/UserViewModel.cs | 2 +- .../ChangePasswordCommandTestFixture.cs | 4 +-- .../ChangePasswordCommandValidationTests.cs | 2 +- .../CreateUserCommandValidationTests.cs | 2 +- .../DeleteUserCommandValidationTests.cs | 2 +- .../LoginUserCommandValidationTests.cs | 2 +- CleanArchitecture.Domain/ApiUser.cs | 2 +- .../Commands/CommandHandlerBase.cs | 12 ++++----- .../ChangePasswordCommandHandler.cs | 2 +- .../CreateUser/CreateUserCommandHandler.cs | 6 ++--- .../DeleteUser/DeleteUserCommandHandler.cs | 2 +- .../LoginUser/LoginUserCommandHandler.cs | 2 +- .../UpdateUser/UpdateUserCommandHandler.cs | 4 +-- CleanArchitecture.Domain/Entities/Entity.cs | 27 ------------------- .../DomainNotificationHandlerTests.cs | 18 ++++++------- .../DomainNotificationTests.cs | 12 ++++----- .../InMemoryBusTests.cs | 6 ++--- 20 files changed, 47 insertions(+), 73 deletions(-) diff --git a/CleanArchitecture.Api/Controllers/UserController.cs b/CleanArchitecture.Api/Controllers/UserController.cs index 7df8108..d3986dd 100644 --- a/CleanArchitecture.Api/Controllers/UserController.cs +++ b/CleanArchitecture.Api/Controllers/UserController.cs @@ -36,7 +36,7 @@ public class UserController : ApiController } [Authorize] - [HttpGet("{id}")] + [HttpGet("{id:guid}")] [SwaggerOperation("Get a user by id")] [SwaggerResponse(200, "Request successful", typeof(ResponseMessage))] public async Task GetUserByIdAsync( @@ -67,7 +67,7 @@ public class UserController : ApiController } [Authorize] - [HttpDelete("{id}")] + [HttpDelete("{id:guid}")] [SwaggerOperation("Delete a user")] [SwaggerResponse(200, "Request successful", typeof(ResponseMessage))] public async Task DeleteUserAsync([FromRoute] Guid id) diff --git a/CleanArchitecture.Api/Program.cs b/CleanArchitecture.Api/Program.cs index 6bfc87b..b828705 100644 --- a/CleanArchitecture.Api/Program.cs +++ b/CleanArchitecture.Api/Program.cs @@ -135,7 +135,7 @@ TokenValidationParameters CreateTokenValidationParameters() return result; } -// Needed for integration tests webapplication factory +// Needed for integration tests web application factory public partial class Program { } \ No newline at end of file diff --git a/CleanArchitecture.Application.Tests/Queries/Users/GetAllUsersQueryHandlerTests.cs b/CleanArchitecture.Application.Tests/Queries/Users/GetAllUsersQueryHandlerTests.cs index 7f2f3a5..816c374 100644 --- a/CleanArchitecture.Application.Tests/Queries/Users/GetAllUsersQueryHandlerTests.cs +++ b/CleanArchitecture.Application.Tests/Queries/Users/GetAllUsersQueryHandlerTests.cs @@ -22,9 +22,10 @@ public sealed class GetAllUsersQueryHandlerTests _fixture.VerifyNoDomainNotification(); - result.Should().NotBeNull(); - result.Should().ContainSingle(); - result.FirstOrDefault()!.Id.Should().Be(_fixture.ExistingUserId); + var userViewModels = result.ToArray(); + userViewModels.Should().NotBeNull(); + userViewModels.Should().ContainSingle(); + userViewModels.FirstOrDefault()!.Id.Should().Be(_fixture.ExistingUserId); } [Fact] diff --git a/CleanArchitecture.Application/viewmodels/Users/UserViewModel.cs b/CleanArchitecture.Application/viewmodels/Users/UserViewModel.cs index cc0ac67..53f2aa9 100644 --- a/CleanArchitecture.Application/viewmodels/Users/UserViewModel.cs +++ b/CleanArchitecture.Application/viewmodels/Users/UserViewModel.cs @@ -23,4 +23,4 @@ public sealed class UserViewModel Role = user.Role }; } -} +} \ No newline at end of file diff --git a/CleanArchitecture.Domain.Tests/CommandHandler/User/ChangePassword/ChangePasswordCommandTestFixture.cs b/CleanArchitecture.Domain.Tests/CommandHandler/User/ChangePassword/ChangePasswordCommandTestFixture.cs index 77e8e55..74f24c7 100644 --- a/CleanArchitecture.Domain.Tests/CommandHandler/User/ChangePassword/ChangePasswordCommandTestFixture.cs +++ b/CleanArchitecture.Domain.Tests/CommandHandler/User/ChangePassword/ChangePasswordCommandTestFixture.cs @@ -21,8 +21,8 @@ public sealed class ChangePasswordCommandTestFixture : CommandHandlerFixtureBase User.Object); } - public ChangePasswordCommandHandler CommandHandler { get; set; } - public Mock UserRepository { get; set; } + public ChangePasswordCommandHandler CommandHandler { get; } + private Mock UserRepository { get; } public Entities.User SetupUser() { diff --git a/CleanArchitecture.Domain.Tests/CommandHandler/User/ChangePassword/ChangePasswordCommandValidationTests.cs b/CleanArchitecture.Domain.Tests/CommandHandler/User/ChangePassword/ChangePasswordCommandValidationTests.cs index 3b0339e..39c8186 100644 --- a/CleanArchitecture.Domain.Tests/CommandHandler/User/ChangePassword/ChangePasswordCommandValidationTests.cs +++ b/CleanArchitecture.Domain.Tests/CommandHandler/User/ChangePassword/ChangePasswordCommandValidationTests.cs @@ -87,7 +87,7 @@ public sealed class ChangePasswordCommandValidationTests : ShouldHaveSingleError(command, DomainErrorCodes.UserLongPassword); } - private ChangePasswordCommand CreateTestCommand( + private static ChangePasswordCommand CreateTestCommand( string? password = null, string? newPassword = null) { return new( diff --git a/CleanArchitecture.Domain.Tests/CommandHandler/User/CreateUser/CreateUserCommandValidationTests.cs b/CleanArchitecture.Domain.Tests/CommandHandler/User/CreateUser/CreateUserCommandValidationTests.cs index f3bbe6d..05a8d1b 100644 --- a/CleanArchitecture.Domain.Tests/CommandHandler/User/CreateUser/CreateUserCommandValidationTests.cs +++ b/CleanArchitecture.Domain.Tests/CommandHandler/User/CreateUser/CreateUserCommandValidationTests.cs @@ -176,7 +176,7 @@ public sealed class CreateUserCommandValidationTests : ShouldHaveSingleError(command, DomainErrorCodes.UserLongPassword); } - private CreateUserCommand CreateTestCommand( + private static CreateUserCommand CreateTestCommand( Guid? userId = null, string? email = null, string? surName = null, diff --git a/CleanArchitecture.Domain.Tests/CommandHandler/User/DeleteUser/DeleteUserCommandValidationTests.cs b/CleanArchitecture.Domain.Tests/CommandHandler/User/DeleteUser/DeleteUserCommandValidationTests.cs index 4ace62f..a2e6e45 100644 --- a/CleanArchitecture.Domain.Tests/CommandHandler/User/DeleteUser/DeleteUserCommandValidationTests.cs +++ b/CleanArchitecture.Domain.Tests/CommandHandler/User/DeleteUser/DeleteUserCommandValidationTests.cs @@ -31,7 +31,7 @@ public sealed class DeleteUserCommandValidationTests : "User id may not be empty"); } - private DeleteUserCommand CreateTestCommand(Guid? userId = null) + private static DeleteUserCommand CreateTestCommand(Guid? userId = null) { return new(userId ?? Guid.NewGuid()); } diff --git a/CleanArchitecture.Domain.Tests/CommandHandler/User/LoginUser/LoginUserCommandValidationTests.cs b/CleanArchitecture.Domain.Tests/CommandHandler/User/LoginUser/LoginUserCommandValidationTests.cs index 96fa7c3..fd85ce8 100644 --- a/CleanArchitecture.Domain.Tests/CommandHandler/User/LoginUser/LoginUserCommandValidationTests.cs +++ b/CleanArchitecture.Domain.Tests/CommandHandler/User/LoginUser/LoginUserCommandValidationTests.cs @@ -120,7 +120,7 @@ public sealed class LoginUserCommandValidationTests : ShouldHaveSingleError(command, DomainErrorCodes.UserLongPassword); } - private LoginUserCommand CreateTestCommand( + private static LoginUserCommand CreateTestCommand( string? email = null, string? password = null) { diff --git a/CleanArchitecture.Domain/ApiUser.cs b/CleanArchitecture.Domain/ApiUser.cs index 15912af..285bfd6 100644 --- a/CleanArchitecture.Domain/ApiUser.cs +++ b/CleanArchitecture.Domain/ApiUser.cs @@ -51,7 +51,7 @@ public sealed class ApiUser : IUser if (!string.IsNullOrWhiteSpace(claim?.Value)) { - return claim?.Value!; + return claim.Value; } throw new ArgumentException("Could not parse user email"); diff --git a/CleanArchitecture.Domain/Commands/CommandHandlerBase.cs b/CleanArchitecture.Domain/Commands/CommandHandlerBase.cs index 14d6877..54a557b 100644 --- a/CleanArchitecture.Domain/Commands/CommandHandlerBase.cs +++ b/CleanArchitecture.Domain/Commands/CommandHandlerBase.cs @@ -9,7 +9,7 @@ namespace CleanArchitecture.Domain.Commands; public abstract class CommandHandlerBase { - protected readonly IMediatorHandler _bus; + protected readonly IMediatorHandler Bus; private readonly DomainNotificationHandler _notifications; private readonly IUnitOfWork _unitOfWork; @@ -18,12 +18,12 @@ public abstract class CommandHandlerBase IUnitOfWork unitOfWork, INotificationHandler notifications) { - _bus = bus; + Bus = bus; _unitOfWork = unitOfWork; _notifications = (DomainNotificationHandler)notifications; } - public async Task CommitAsync() + protected async Task CommitAsync() { if (_notifications.HasNotifications()) { @@ -35,7 +35,7 @@ public abstract class CommandHandlerBase return true; } - await _bus.RaiseEventAsync( + await Bus.RaiseEventAsync( new DomainNotification( "Commit", "Problem occured while saving the data. Please try again.", @@ -46,13 +46,13 @@ public abstract class CommandHandlerBase protected async Task NotifyAsync(string key, string message, string code) { - await _bus.RaiseEventAsync( + await Bus.RaiseEventAsync( new DomainNotification(key, message, code)); } protected async Task NotifyAsync(DomainNotification notification) { - await _bus.RaiseEventAsync(notification); + await Bus.RaiseEventAsync(notification); } protected async ValueTask TestValidityAsync(CommandBase command) diff --git a/CleanArchitecture.Domain/Commands/Users/ChangePassword/ChangePasswordCommandHandler.cs b/CleanArchitecture.Domain/Commands/Users/ChangePassword/ChangePasswordCommandHandler.cs index 8b5ef24..38a85dd 100644 --- a/CleanArchitecture.Domain/Commands/Users/ChangePassword/ChangePasswordCommandHandler.cs +++ b/CleanArchitecture.Domain/Commands/Users/ChangePassword/ChangePasswordCommandHandler.cs @@ -65,7 +65,7 @@ public sealed class ChangePasswordCommandHandler : CommandHandlerBase, if (await CommitAsync()) { - await _bus.RaiseEventAsync(new PasswordChangedEvent(user.Id)); + await Bus.RaiseEventAsync(new PasswordChangedEvent(user.Id)); } } } \ No newline at end of file diff --git a/CleanArchitecture.Domain/Commands/Users/CreateUser/CreateUserCommandHandler.cs b/CleanArchitecture.Domain/Commands/Users/CreateUser/CreateUserCommandHandler.cs index 382a529..a639591 100644 --- a/CleanArchitecture.Domain/Commands/Users/CreateUser/CreateUserCommandHandler.cs +++ b/CleanArchitecture.Domain/Commands/Users/CreateUser/CreateUserCommandHandler.cs @@ -37,7 +37,7 @@ public sealed class CreateUserCommandHandler : CommandHandlerBase, if (existingUser != null) { - await _bus.RaiseEventAsync( + await Bus.RaiseEventAsync( new DomainNotification( request.MessageType, $"There is already a User with Id {request.UserId}", @@ -49,7 +49,7 @@ public sealed class CreateUserCommandHandler : CommandHandlerBase, if (existingUser != null) { - await _bus.RaiseEventAsync( + await Bus.RaiseEventAsync( new DomainNotification( request.MessageType, $"There is already a User with Email {request.Email}", @@ -71,7 +71,7 @@ public sealed class CreateUserCommandHandler : CommandHandlerBase, if (await CommitAsync()) { - await _bus.RaiseEventAsync(new UserCreatedEvent(user.Id)); + await Bus.RaiseEventAsync(new UserCreatedEvent(user.Id)); } } } \ No newline at end of file diff --git a/CleanArchitecture.Domain/Commands/Users/DeleteUser/DeleteUserCommandHandler.cs b/CleanArchitecture.Domain/Commands/Users/DeleteUser/DeleteUserCommandHandler.cs index d40495f..1fc4970 100644 --- a/CleanArchitecture.Domain/Commands/Users/DeleteUser/DeleteUserCommandHandler.cs +++ b/CleanArchitecture.Domain/Commands/Users/DeleteUser/DeleteUserCommandHandler.cs @@ -62,7 +62,7 @@ public sealed class DeleteUserCommandHandler : CommandHandlerBase, if (await CommitAsync()) { - await _bus.RaiseEventAsync(new UserDeletedEvent(request.UserId)); + await Bus.RaiseEventAsync(new UserDeletedEvent(request.UserId)); } } } \ No newline at end of file diff --git a/CleanArchitecture.Domain/Commands/Users/LoginUser/LoginUserCommandHandler.cs b/CleanArchitecture.Domain/Commands/Users/LoginUser/LoginUserCommandHandler.cs index c93e2c1..06cb8d7 100644 --- a/CleanArchitecture.Domain/Commands/Users/LoginUser/LoginUserCommandHandler.cs +++ b/CleanArchitecture.Domain/Commands/Users/LoginUser/LoginUserCommandHandler.cs @@ -76,7 +76,7 @@ public sealed class LoginUserCommandHandler : CommandHandlerBase, _tokenSettings); } - public static string BuildToken(string email, UserRole role, Guid id, TokenSettings tokenSettings) + private static string BuildToken(string email, UserRole role, Guid id, TokenSettings tokenSettings) { var claims = new[] { diff --git a/CleanArchitecture.Domain/Commands/Users/UpdateUser/UpdateUserCommandHandler.cs b/CleanArchitecture.Domain/Commands/Users/UpdateUser/UpdateUserCommandHandler.cs index 02d87cd..6f882e3 100644 --- a/CleanArchitecture.Domain/Commands/Users/UpdateUser/UpdateUserCommandHandler.cs +++ b/CleanArchitecture.Domain/Commands/Users/UpdateUser/UpdateUserCommandHandler.cs @@ -38,7 +38,7 @@ public sealed class UpdateUserCommandHandler : CommandHandlerBase, if (user == null) { - await _bus.RaiseEventAsync( + await Bus.RaiseEventAsync( new DomainNotification( request.MessageType, $"There is no User with Id {request.UserId}", @@ -70,7 +70,7 @@ public sealed class UpdateUserCommandHandler : CommandHandlerBase, if (await CommitAsync()) { - await _bus.RaiseEventAsync(new UserUpdatedEvent(user.Id)); + await Bus.RaiseEventAsync(new UserUpdatedEvent(user.Id)); } } } \ No newline at end of file diff --git a/CleanArchitecture.Domain/Entities/Entity.cs b/CleanArchitecture.Domain/Entities/Entity.cs index 395e70e..99a272a 100644 --- a/CleanArchitecture.Domain/Entities/Entity.cs +++ b/CleanArchitecture.Domain/Entities/Entity.cs @@ -31,31 +31,4 @@ public abstract class Entity { Deleted = false; } - - public override bool Equals(object? obj) - { - var compareTo = obj as Entity; - - if (ReferenceEquals(this, compareTo)) - { - return true; - } - - if (compareTo is null) - { - return false; - } - - return Id == compareTo.Id; - } - - public override int GetHashCode() - { - return GetType().GetHashCode() * 907 + Id.GetHashCode(); - } - - public override string ToString() - { - return GetType().Name + " [Id=" + Id + "]"; - } } \ No newline at end of file diff --git a/CleanArchitecture.Infrastructure.Tests/DomainNotificationHandlerTests.cs b/CleanArchitecture.Infrastructure.Tests/DomainNotificationHandlerTests.cs index 4ebf2a2..15a24ed 100644 --- a/CleanArchitecture.Infrastructure.Tests/DomainNotificationHandlerTests.cs +++ b/CleanArchitecture.Infrastructure.Tests/DomainNotificationHandlerTests.cs @@ -16,9 +16,9 @@ public sealed class DomainNotificationHandlerTests [Fact] public void Should_Handle_DomainNotification() { - var key = "Key"; - var value = "Value"; - var code = "Code"; + const string key = "Key"; + const string value = "Value"; + const string code = "Code"; var domainNotification = new DomainNotification(key, value, code); var domainNotificationHandler = new DomainNotificationHandler(); @@ -29,9 +29,9 @@ public sealed class DomainNotificationHandlerTests [Fact] public void Should_Handle_DomainNotification_Overload() { - var key = "Key"; - var value = "Value"; - var code = "Code"; + const string key = "Key"; + const string value = "Value"; + const string code = "Code"; var domainNotification = new DomainNotification(key, value, code); var domainNotificationHandler = new DomainNotificationHandler(); @@ -42,9 +42,9 @@ public sealed class DomainNotificationHandlerTests [Fact] public void DomainNotification_HasNotifications_After_Handling_One() { - var key = "Key"; - var value = "Value"; - var code = "Code"; + const string key = "Key"; + const string value = "Value"; + const string code = "Code"; var domainNotification = new DomainNotification(key, value, code); var domainNotificationHandler = new DomainNotificationHandler(); diff --git a/CleanArchitecture.Infrastructure.Tests/DomainNotificationTests.cs b/CleanArchitecture.Infrastructure.Tests/DomainNotificationTests.cs index f34d1c0..14d90b6 100644 --- a/CleanArchitecture.Infrastructure.Tests/DomainNotificationTests.cs +++ b/CleanArchitecture.Infrastructure.Tests/DomainNotificationTests.cs @@ -10,9 +10,9 @@ public sealed class DomainNotificationTests [Fact] public void Should_Create_DomainNotification_Instance() { - var key = "Key"; - var value = "Value"; - var code = "Code"; + const string key = "Key"; + const string value = "Value"; + const string code = "Code"; var domainNotification = new DomainNotification( key, value, code); @@ -26,9 +26,9 @@ public sealed class DomainNotificationTests [Fact] public void Should_Create_DomainNotification_Overload_Instance() { - var key = "Key"; - var value = "Value"; - var code = "Code"; + const string key = "Key"; + const string value = "Value"; + const string code = "Code"; var domainNotification = new DomainNotification( key, value, code); diff --git a/CleanArchitecture.Infrastructure.Tests/InMemoryBusTests.cs b/CleanArchitecture.Infrastructure.Tests/InMemoryBusTests.cs index f3ed4d8..382ccc2 100644 --- a/CleanArchitecture.Infrastructure.Tests/InMemoryBusTests.cs +++ b/CleanArchitecture.Infrastructure.Tests/InMemoryBusTests.cs @@ -19,9 +19,9 @@ public sealed class InMemoryBusTests var inMemoryBus = new InMemoryBus(mediator.Object); - var key = "Key"; - var value = "Value"; - var code = "Code"; + const string key = "Key"; + const string value = "Value"; + const string code = "Code"; var domainEvent = new DomainNotification(key, value, code); From 598949371f97cb6c2f99d9024d36eefc1744e5ce Mon Sep 17 00:00:00 2001 From: alex289 Date: Wed, 22 Mar 2023 19:40:53 +0100 Subject: [PATCH 10/10] Add build status to Readme.md --- Readme.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Readme.md b/Readme.md index 2455294..7f36628 100644 --- a/Readme.md +++ b/Readme.md @@ -1,4 +1,7 @@ # Clean Architecture Dotnet 7 API Project + +![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/alex289/CleanArchitecture/dotnet.yml) + This repository contains a sample API project built using the Clean Architecture principles, Onion Architecture, MediatR, and Entity Framework. The project also includes unit tests for all layers and integration tests using xUnit. ## Project Structure