From b0d96d8b4d240f7e3b4bc0f1f2b3fe9a23101fbc Mon Sep 17 00:00:00 2001 From: alex289 Date: Sun, 27 Aug 2023 19:09:23 +0200 Subject: [PATCH 01/10] Add tenant entity --- .../Queries/Users/GetAllUsersTestFixture.cs | 2 + .../Queries/Users/GetUserByIdTestFixture.cs | 2 + .../Services/UserService.cs | 1 + .../ChangePasswordCommandTestFixture.cs | 1 + .../CreateUserCommandHandlerTests.cs | 2 + .../CreateUserCommandTestFixture.cs | 1 + .../CreateUserCommandValidationTests.cs | 2 + .../DeleteUserCommandTestFixture.cs | 1 + .../LoginUser/LoginUserCommandTestFixture.cs | 1 + .../UpdateUserCommandHandlerTests.cs | 1 + .../UpdateUserCommandTestFixture.cs | 1 + .../Users/CreateUser/CreateUserCommand.cs | 3 + .../CreateUser/CreateUserCommandHandler.cs | 1 + .../CreateUser/CreateUserCommandValidation.cs | 7 +- .../LoginUser/LoginUserCommandValidation.cs | 5 +- .../UpdateUser/UpdateUserCommandValidation.cs | 7 +- CleanArchitecture.Domain/Constants/Ids.cs | 12 +++ .../Constants/MaxLengths.cs | 17 ++++ CleanArchitecture.Domain/Entities/Tenant.cs | 18 ++++ CleanArchitecture.Domain/Entities/User.cs | 83 +++++-------------- .../Configurations/TenantConfiguration.cs | 21 +++++ .../Configurations/UserConfiguration.cs | 12 +-- .../Database/ApplicationDbContext.cs | 2 + .../Controller/UserControllerTests.cs | 3 +- .../Fixtures/gRPC/GetUsersByIdsTestFixture.cs | 1 + .../Fixtures/UserTestsFixture.cs | 3 + 26 files changed, 133 insertions(+), 77 deletions(-) create mode 100644 CleanArchitecture.Domain/Constants/Ids.cs create mode 100644 CleanArchitecture.Domain/Constants/MaxLengths.cs create mode 100644 CleanArchitecture.Domain/Entities/Tenant.cs create mode 100644 CleanArchitecture.Infrastructure/Configurations/TenantConfiguration.cs diff --git a/CleanArchitecture.Application.Tests/Fixtures/Queries/Users/GetAllUsersTestFixture.cs b/CleanArchitecture.Application.Tests/Fixtures/Queries/Users/GetAllUsersTestFixture.cs index 0e23086..027e8ff 100644 --- a/CleanArchitecture.Application.Tests/Fixtures/Queries/Users/GetAllUsersTestFixture.cs +++ b/CleanArchitecture.Application.Tests/Fixtures/Queries/Users/GetAllUsersTestFixture.cs @@ -25,6 +25,7 @@ public sealed class GetAllUsersTestFixture : QueryHandlerBaseFixture { var user = new User( ExistingUserId, + Guid.NewGuid(), "max@mustermann.com", "Max", "Mustermann", @@ -40,6 +41,7 @@ public sealed class GetAllUsersTestFixture : QueryHandlerBaseFixture { var user = new User( ExistingUserId, + Guid.NewGuid(), "max@mustermann.com", "Max", "Mustermann", diff --git a/CleanArchitecture.Application.Tests/Fixtures/Queries/Users/GetUserByIdTestFixture.cs b/CleanArchitecture.Application.Tests/Fixtures/Queries/Users/GetUserByIdTestFixture.cs index 741faac..6f306d6 100644 --- a/CleanArchitecture.Application.Tests/Fixtures/Queries/Users/GetUserByIdTestFixture.cs +++ b/CleanArchitecture.Application.Tests/Fixtures/Queries/Users/GetUserByIdTestFixture.cs @@ -26,6 +26,7 @@ public sealed class GetUserByIdTestFixture : QueryHandlerBaseFixture { var user = new User( ExistingUserId, + Guid.NewGuid(), "max@mustermann.com", "Max", "Mustermann", @@ -41,6 +42,7 @@ public sealed class GetUserByIdTestFixture : QueryHandlerBaseFixture { var user = new User( ExistingUserId, + Guid.NewGuid(), "max@mustermann.com", "Max", "Mustermann", diff --git a/CleanArchitecture.Application/Services/UserService.cs b/CleanArchitecture.Application/Services/UserService.cs index 8ab4600..13920c7 100644 --- a/CleanArchitecture.Application/Services/UserService.cs +++ b/CleanArchitecture.Application/Services/UserService.cs @@ -46,6 +46,7 @@ public sealed class UserService : IUserService await _bus.SendCommandAsync(new CreateUserCommand( userId, + user.TenantId, user.Email, user.FirstName, user.LastName, diff --git a/CleanArchitecture.Domain.Tests/CommandHandler/User/ChangePassword/ChangePasswordCommandTestFixture.cs b/CleanArchitecture.Domain.Tests/CommandHandler/User/ChangePassword/ChangePasswordCommandTestFixture.cs index 20e2189..314716b 100644 --- a/CleanArchitecture.Domain.Tests/CommandHandler/User/ChangePassword/ChangePasswordCommandTestFixture.cs +++ b/CleanArchitecture.Domain.Tests/CommandHandler/User/ChangePassword/ChangePasswordCommandTestFixture.cs @@ -27,6 +27,7 @@ public sealed class ChangePasswordCommandTestFixture : CommandHandlerFixtureBase public Entities.User SetupUser() { var user = new Entities.User( + Guid.NewGuid(), Guid.NewGuid(), "max@mustermann.com", "Max", diff --git a/CleanArchitecture.Domain.Tests/CommandHandler/User/CreateUser/CreateUserCommandHandlerTests.cs b/CleanArchitecture.Domain.Tests/CommandHandler/User/CreateUser/CreateUserCommandHandlerTests.cs index 9319274..b902a98 100644 --- a/CleanArchitecture.Domain.Tests/CommandHandler/User/CreateUser/CreateUserCommandHandlerTests.cs +++ b/CleanArchitecture.Domain.Tests/CommandHandler/User/CreateUser/CreateUserCommandHandlerTests.cs @@ -16,6 +16,7 @@ public sealed class CreateUserCommandHandlerTests _fixture.SetupUser(); var command = new CreateUserCommand( + Guid.NewGuid(), Guid.NewGuid(), "test@email.com", "Test", @@ -37,6 +38,7 @@ public sealed class CreateUserCommandHandlerTests var command = new CreateUserCommand( user.Id, + Guid.NewGuid(), "test@email.com", "Test", "Email", diff --git a/CleanArchitecture.Domain.Tests/CommandHandler/User/CreateUser/CreateUserCommandTestFixture.cs b/CleanArchitecture.Domain.Tests/CommandHandler/User/CreateUser/CreateUserCommandTestFixture.cs index 3662b65..31ea40e 100644 --- a/CleanArchitecture.Domain.Tests/CommandHandler/User/CreateUser/CreateUserCommandTestFixture.cs +++ b/CleanArchitecture.Domain.Tests/CommandHandler/User/CreateUser/CreateUserCommandTestFixture.cs @@ -25,6 +25,7 @@ public sealed class CreateUserCommandTestFixture : CommandHandlerFixtureBase public Entities.User SetupUser() { var user = new Entities.User( + Guid.NewGuid(), Guid.NewGuid(), "max@mustermann.com", "Max", diff --git a/CleanArchitecture.Domain.Tests/CommandHandler/User/CreateUser/CreateUserCommandValidationTests.cs b/CleanArchitecture.Domain.Tests/CommandHandler/User/CreateUser/CreateUserCommandValidationTests.cs index f4635df..7906a1f 100644 --- a/CleanArchitecture.Domain.Tests/CommandHandler/User/CreateUser/CreateUserCommandValidationTests.cs +++ b/CleanArchitecture.Domain.Tests/CommandHandler/User/CreateUser/CreateUserCommandValidationTests.cs @@ -178,6 +178,7 @@ public sealed class CreateUserCommandValidationTests : private static CreateUserCommand CreateTestCommand( Guid? userId = null, + Guid? tenantId = null, string? email = null, string? firstName = null, string? lastName = null, @@ -185,6 +186,7 @@ public sealed class CreateUserCommandValidationTests : { return new( userId ?? Guid.NewGuid(), + tenantId ?? Guid.NewGuid(), email ?? "test@email.com", firstName ?? "test", lastName ?? "email", diff --git a/CleanArchitecture.Domain.Tests/CommandHandler/User/DeleteUser/DeleteUserCommandTestFixture.cs b/CleanArchitecture.Domain.Tests/CommandHandler/User/DeleteUser/DeleteUserCommandTestFixture.cs index 92af9a4..89c4eee 100644 --- a/CleanArchitecture.Domain.Tests/CommandHandler/User/DeleteUser/DeleteUserCommandTestFixture.cs +++ b/CleanArchitecture.Domain.Tests/CommandHandler/User/DeleteUser/DeleteUserCommandTestFixture.cs @@ -26,6 +26,7 @@ public sealed class DeleteUserCommandTestFixture : CommandHandlerFixtureBase public Entities.User SetupUser() { var user = new Entities.User( + Guid.NewGuid(), Guid.NewGuid(), "max@mustermann.com", "Max", diff --git a/CleanArchitecture.Domain.Tests/CommandHandler/User/LoginUser/LoginUserCommandTestFixture.cs b/CleanArchitecture.Domain.Tests/CommandHandler/User/LoginUser/LoginUserCommandTestFixture.cs index c8f91cd..7a08da8 100644 --- a/CleanArchitecture.Domain.Tests/CommandHandler/User/LoginUser/LoginUserCommandTestFixture.cs +++ b/CleanArchitecture.Domain.Tests/CommandHandler/User/LoginUser/LoginUserCommandTestFixture.cs @@ -37,6 +37,7 @@ public sealed class LoginUserCommandTestFixture : CommandHandlerFixtureBase public Entities.User SetupUser() { var user = new Entities.User( + Guid.NewGuid(), Guid.NewGuid(), "max@mustermann.com", "Max", diff --git a/CleanArchitecture.Domain.Tests/CommandHandler/User/UpdateUser/UpdateUserCommandHandlerTests.cs b/CleanArchitecture.Domain.Tests/CommandHandler/User/UpdateUser/UpdateUserCommandHandlerTests.cs index 841c796..66281d3 100644 --- a/CleanArchitecture.Domain.Tests/CommandHandler/User/UpdateUser/UpdateUserCommandHandlerTests.cs +++ b/CleanArchitecture.Domain.Tests/CommandHandler/User/UpdateUser/UpdateUserCommandHandlerTests.cs @@ -71,6 +71,7 @@ public sealed class UpdateUserCommandHandlerTests _fixture.UserRepository .GetByEmailAsync(command.Email) .Returns(new Entities.User( + Guid.NewGuid(), Guid.NewGuid(), command.Email, "Some", diff --git a/CleanArchitecture.Domain.Tests/CommandHandler/User/UpdateUser/UpdateUserCommandTestFixture.cs b/CleanArchitecture.Domain.Tests/CommandHandler/User/UpdateUser/UpdateUserCommandTestFixture.cs index cd21e9f..aaf9faf 100644 --- a/CleanArchitecture.Domain.Tests/CommandHandler/User/UpdateUser/UpdateUserCommandTestFixture.cs +++ b/CleanArchitecture.Domain.Tests/CommandHandler/User/UpdateUser/UpdateUserCommandTestFixture.cs @@ -26,6 +26,7 @@ public sealed class UpdateUserCommandTestFixture : CommandHandlerFixtureBase public Entities.User SetupUser() { var user = new Entities.User( + Guid.NewGuid(), Guid.NewGuid(), "max@mustermann.com", "Max", diff --git a/CleanArchitecture.Domain/Commands/Users/CreateUser/CreateUserCommand.cs b/CleanArchitecture.Domain/Commands/Users/CreateUser/CreateUserCommand.cs index c9e9e79..62406ad 100644 --- a/CleanArchitecture.Domain/Commands/Users/CreateUser/CreateUserCommand.cs +++ b/CleanArchitecture.Domain/Commands/Users/CreateUser/CreateUserCommand.cs @@ -8,12 +8,14 @@ public sealed class CreateUserCommand : CommandBase public CreateUserCommand( Guid userId, + Guid tenantId, string email, string firstName, string lastName, string password) : base(userId) { UserId = userId; + TenantId = tenantId; Email = email; FirstName = firstName; LastName = lastName; @@ -21,6 +23,7 @@ public sealed class CreateUserCommand : CommandBase } public Guid UserId { get; } + public Guid TenantId { get; } public string Email { get; } public string FirstName { get; } public string LastName { get; } diff --git a/CleanArchitecture.Domain/Commands/Users/CreateUser/CreateUserCommandHandler.cs b/CleanArchitecture.Domain/Commands/Users/CreateUser/CreateUserCommandHandler.cs index 98e1b2b..464aad4 100644 --- a/CleanArchitecture.Domain/Commands/Users/CreateUser/CreateUserCommandHandler.cs +++ b/CleanArchitecture.Domain/Commands/Users/CreateUser/CreateUserCommandHandler.cs @@ -61,6 +61,7 @@ public sealed class CreateUserCommandHandler : CommandHandlerBase, var user = new User( request.UserId, + request.TenantId, request.Email, request.FirstName, request.LastName, diff --git a/CleanArchitecture.Domain/Commands/Users/CreateUser/CreateUserCommandValidation.cs b/CleanArchitecture.Domain/Commands/Users/CreateUser/CreateUserCommandValidation.cs index 196db14..5a51947 100644 --- a/CleanArchitecture.Domain/Commands/Users/CreateUser/CreateUserCommandValidation.cs +++ b/CleanArchitecture.Domain/Commands/Users/CreateUser/CreateUserCommandValidation.cs @@ -1,3 +1,4 @@ +using CleanArchitecture.Domain.Constants; using CleanArchitecture.Domain.Errors; using CleanArchitecture.Domain.Extensions.Validation; using FluentValidation; @@ -29,7 +30,7 @@ public sealed class CreateUserCommandValidation : AbstractValidator Users { get; private set; } = new HashSet(); + + public Tenant( + Guid id, + string name) : base(id) + { + Name = name; + } +} \ No newline at end of file diff --git a/CleanArchitecture.Domain/Entities/User.cs b/CleanArchitecture.Domain/Entities/User.cs index 186913e..916269c 100644 --- a/CleanArchitecture.Domain/Entities/User.cs +++ b/CleanArchitecture.Domain/Entities/User.cs @@ -6,21 +6,6 @@ namespace CleanArchitecture.Domain.Entities; public class User : Entity { - public User( - Guid id, - string email, - string firstName, - string lastName, - string password, - UserRole role) : base(id) - { - Email = email; - FirstName = firstName; - LastName = lastName; - Password = password; - Role = role; - } - public string Email { get; private set; } public string FirstName { get; private set; } public string LastName { get; private set; } @@ -29,71 +14,43 @@ public class User : Entity public string FullName => $"{FirstName}, {LastName}"; - [MemberNotNull(nameof(Email))] + public Guid TenantId { get; private set; } + public Tenant Tenant { get; private set; } = null!; + + public User( + Guid id, + Guid tenantId, + string email, + string firstName, + string lastName, + string password, + UserRole role) : base(id) + { + Email = email; + TenantId = tenantId; + FirstName = firstName; + LastName = lastName; + Password = password; + Role = role; + } + public void SetEmail(string email) { - if (email == null) - { - throw new ArgumentNullException(nameof(email)); - } - - if (email.Length > 320) - { - throw new ArgumentException( - "Email may not be longer than 320 characters."); - } - Email = email; } - [MemberNotNull(nameof(FirstName))] public void SetFirstName(string firstName) { - if (firstName == null) - { - throw new ArgumentNullException(nameof(firstName)); - } - - if (firstName.Length > 100) - { - throw new ArgumentException( - "First name may not be longer than 100 characters"); - } - FirstName = firstName; } - [MemberNotNull(nameof(LastName))] public void SetLastName(string lastName) { - if (lastName == null) - { - throw new ArgumentNullException(nameof(lastName)); - } - - if (lastName.Length > 100) - { - throw new ArgumentException( - "Last name may not be longer than 100 characters"); - } - LastName = lastName; } - [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; } diff --git a/CleanArchitecture.Infrastructure/Configurations/TenantConfiguration.cs b/CleanArchitecture.Infrastructure/Configurations/TenantConfiguration.cs new file mode 100644 index 0000000..783c4a7 --- /dev/null +++ b/CleanArchitecture.Infrastructure/Configurations/TenantConfiguration.cs @@ -0,0 +1,21 @@ +using CleanArchitecture.Domain.Constants; +using CleanArchitecture.Domain.Entities; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace CleanArchitecture.Infrastructure.Configurations; + +public sealed class TenantConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder + .Property(user => user.Name) + .IsRequired() + .HasMaxLength(MaxLengths.Tenant.Name); + + builder.HasData(new Tenant( + Ids.Seed.TenantId, + "Admin Tenant")); + } +} \ No newline at end of file diff --git a/CleanArchitecture.Infrastructure/Configurations/UserConfiguration.cs b/CleanArchitecture.Infrastructure/Configurations/UserConfiguration.cs index ecd3505..abe3cef 100644 --- a/CleanArchitecture.Infrastructure/Configurations/UserConfiguration.cs +++ b/CleanArchitecture.Infrastructure/Configurations/UserConfiguration.cs @@ -1,4 +1,5 @@ using System; +using CleanArchitecture.Domain.Constants; using CleanArchitecture.Domain.Entities; using CleanArchitecture.Domain.Enums; using Microsoft.EntityFrameworkCore; @@ -13,25 +14,26 @@ public sealed class UserConfiguration : IEntityTypeConfiguration builder .Property(user => user.Email) .IsRequired() - .HasMaxLength(320); + .HasMaxLength(MaxLengths.User.Email); builder .Property(user => user.FirstName) .IsRequired() - .HasMaxLength(100); + .HasMaxLength(MaxLengths.User.FirstName); builder .Property(user => user.LastName) .IsRequired() - .HasMaxLength(100); + .HasMaxLength(MaxLengths.User.LastName); builder .Property(user => user.Password) .IsRequired() - .HasMaxLength(128); + .HasMaxLength(MaxLengths.User.Password); builder.HasData(new User( - Guid.NewGuid(), + Ids.Seed.UserId, + Ids.Seed.TenantId, "admin@email.com", "Admin", "User", diff --git a/CleanArchitecture.Infrastructure/Database/ApplicationDbContext.cs b/CleanArchitecture.Infrastructure/Database/ApplicationDbContext.cs index 4280c4d..f2dea27 100644 --- a/CleanArchitecture.Infrastructure/Database/ApplicationDbContext.cs +++ b/CleanArchitecture.Infrastructure/Database/ApplicationDbContext.cs @@ -11,9 +11,11 @@ public class ApplicationDbContext : DbContext } public DbSet Users { get; set; } = null!; + public DbSet Tenants { get; set; } = null!; protected override void OnModelCreating(ModelBuilder builder) { builder.ApplyConfiguration(new UserConfiguration()); + builder.ApplyConfiguration(new TenantConfiguration()); } } \ No newline at end of file diff --git a/CleanArchitecture.IntegrationTests/Controller/UserControllerTests.cs b/CleanArchitecture.IntegrationTests/Controller/UserControllerTests.cs index ad2cb37..8a90362 100644 --- a/CleanArchitecture.IntegrationTests/Controller/UserControllerTests.cs +++ b/CleanArchitecture.IntegrationTests/Controller/UserControllerTests.cs @@ -32,7 +32,8 @@ public sealed class UserControllerTests : IClassFixture _fixture.CreatedUserEmail, "Test", "Email", - _fixture.CreatedUserPassword); + _fixture.CreatedUserPassword, + Guid.NewGuid()); var response = await _fixture.ServerClient.PostAsJsonAsync("/api/v1/user", user); diff --git a/CleanArchitecture.IntegrationTests/Fixtures/gRPC/GetUsersByIdsTestFixture.cs b/CleanArchitecture.IntegrationTests/Fixtures/gRPC/GetUsersByIdsTestFixture.cs index df677db..b06aecd 100644 --- a/CleanArchitecture.IntegrationTests/Fixtures/gRPC/GetUsersByIdsTestFixture.cs +++ b/CleanArchitecture.IntegrationTests/Fixtures/gRPC/GetUsersByIdsTestFixture.cs @@ -33,6 +33,7 @@ public sealed class GetUsersByIdsTestFixture : TestFixtureBase { return new User( CreatedUserId, + Guid.NewGuid(), "user@user.de", "User First Name", "User Last Name", diff --git a/CleanArchitecture.gRPC.Tests/Fixtures/UserTestsFixture.cs b/CleanArchitecture.gRPC.Tests/Fixtures/UserTestsFixture.cs index 6ecba91..d0616f2 100644 --- a/CleanArchitecture.gRPC.Tests/Fixtures/UserTestsFixture.cs +++ b/CleanArchitecture.gRPC.Tests/Fixtures/UserTestsFixture.cs @@ -16,6 +16,7 @@ public sealed class UserTestsFixture ExistingUsers = new List { new( + Guid.NewGuid(), Guid.NewGuid(), "test@test.de", "Test First Name", @@ -23,6 +24,7 @@ public sealed class UserTestsFixture "Test Password", UserRole.User), new( + Guid.NewGuid(), Guid.NewGuid(), "email@Email.de", "Email First Name", @@ -30,6 +32,7 @@ public sealed class UserTestsFixture "Email Password", UserRole.Admin), new( + Guid.NewGuid(), Guid.NewGuid(), "user@user.de", "User First Name", From 6d1f0b1e5f15b7f5421b8e895fc4d23fd47e628f Mon Sep 17 00:00:00 2001 From: Alexander Konietzko Date: Sun, 27 Aug 2023 19:17:05 +0200 Subject: [PATCH 02/10] chore: Add migration --- .../viewmodels/Users/CreateUserViewModel.cs | 5 +- CleanArchitecture.Domain/Entities/Tenant.cs | 4 +- CleanArchitecture.Domain/Entities/User.cs | 4 +- .../20230827171448_AddTenants.Designer.cs | 131 ++++++++++++++++++ .../Migrations/20230827171448_AddTenants.cs | 92 ++++++++++++ .../ApplicationDbContextModelSnapshot.cs | 55 +++++++- 6 files changed, 283 insertions(+), 8 deletions(-) create mode 100644 CleanArchitecture.Infrastructure/Migrations/20230827171448_AddTenants.Designer.cs create mode 100644 CleanArchitecture.Infrastructure/Migrations/20230827171448_AddTenants.cs diff --git a/CleanArchitecture.Application/viewmodels/Users/CreateUserViewModel.cs b/CleanArchitecture.Application/viewmodels/Users/CreateUserViewModel.cs index 2197419..2be45ca 100644 --- a/CleanArchitecture.Application/viewmodels/Users/CreateUserViewModel.cs +++ b/CleanArchitecture.Application/viewmodels/Users/CreateUserViewModel.cs @@ -1,7 +1,10 @@ +using System; + namespace CleanArchitecture.Application.ViewModels.Users; public sealed record CreateUserViewModel( string Email, string FirstName, string LastName, - string Password); \ No newline at end of file + string Password, + Guid TenantId); \ No newline at end of file diff --git a/CleanArchitecture.Domain/Entities/Tenant.cs b/CleanArchitecture.Domain/Entities/Tenant.cs index 897738c..555b095 100644 --- a/CleanArchitecture.Domain/Entities/Tenant.cs +++ b/CleanArchitecture.Domain/Entities/Tenant.cs @@ -7,8 +7,8 @@ public class Tenant : Entity { public string Name { get; private set; } - public ICollection Users { get; private set; } = new HashSet(); - + public virtual ICollection Users { get; private set; } = new HashSet(); + public Tenant( Guid id, string name) : base(id) diff --git a/CleanArchitecture.Domain/Entities/User.cs b/CleanArchitecture.Domain/Entities/User.cs index 916269c..5c50209 100644 --- a/CleanArchitecture.Domain/Entities/User.cs +++ b/CleanArchitecture.Domain/Entities/User.cs @@ -15,8 +15,8 @@ public class User : Entity public string FullName => $"{FirstName}, {LastName}"; public Guid TenantId { get; private set; } - public Tenant Tenant { get; private set; } = null!; - + public virtual Tenant Tenant { get; private set; } = null!; + public User( Guid id, Guid tenantId, diff --git a/CleanArchitecture.Infrastructure/Migrations/20230827171448_AddTenants.Designer.cs b/CleanArchitecture.Infrastructure/Migrations/20230827171448_AddTenants.Designer.cs new file mode 100644 index 0000000..dfb2c12 --- /dev/null +++ b/CleanArchitecture.Infrastructure/Migrations/20230827171448_AddTenants.Designer.cs @@ -0,0 +1,131 @@ +// +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("20230827171448_AddTenants")] + partial class AddTenants + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "7.0.10") + .HasAnnotation("Proxies:ChangeTracking", false) + .HasAnnotation("Proxies:CheckEquality", false) + .HasAnnotation("Proxies:LazyLoading", true) + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("CleanArchitecture.Domain.Entities.Tenant", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("Deleted") + .HasColumnType("bit"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("nvarchar(255)"); + + b.HasKey("Id"); + + b.ToTable("Tenants"); + + b.HasData( + new + { + Id = new Guid("b542bf25-134c-47a2-a0df-84ed14d03c4a"), + Deleted = false, + Name = "Admin Tenant" + }); + }); + + 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("FirstName") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("LastName") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("Password") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("nvarchar(128)"); + + b.Property("Role") + .HasColumnType("int"); + + b.Property("TenantId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("TenantId"); + + b.ToTable("Users"); + + b.HasData( + new + { + Id = new Guid("7e3892c0-9374-49fa-a3fd-53db637a40ae"), + Deleted = false, + Email = "admin@email.com", + FirstName = "Admin", + LastName = "User", + Password = "$2a$12$Blal/uiFIJdYsCLTMUik/egLbfg3XhbnxBC6Sb5IKz2ZYhiU/MzL2", + Role = 0, + TenantId = new Guid("b542bf25-134c-47a2-a0df-84ed14d03c4a") + }); + }); + + modelBuilder.Entity("CleanArchitecture.Domain.Entities.User", b => + { + b.HasOne("CleanArchitecture.Domain.Entities.Tenant", "Tenant") + .WithMany("Users") + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Tenant"); + }); + + modelBuilder.Entity("CleanArchitecture.Domain.Entities.Tenant", b => + { + b.Navigation("Users"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/CleanArchitecture.Infrastructure/Migrations/20230827171448_AddTenants.cs b/CleanArchitecture.Infrastructure/Migrations/20230827171448_AddTenants.cs new file mode 100644 index 0000000..7af855e --- /dev/null +++ b/CleanArchitecture.Infrastructure/Migrations/20230827171448_AddTenants.cs @@ -0,0 +1,92 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace CleanArchitecture.Infrastructure.Migrations +{ + /// + public partial class AddTenants : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DeleteData( + table: "Users", + keyColumn: "Id", + keyValue: new Guid("28fc3d91-6a15-448e-b0b5-0c91a3948961")); + + migrationBuilder.AddColumn( + name: "TenantId", + table: "Users", + type: "uniqueidentifier", + nullable: false, + defaultValue: new Guid("00000000-0000-0000-0000-000000000000")); + + migrationBuilder.CreateTable( + name: "Tenants", + columns: table => new + { + Id = table.Column(type: "uniqueidentifier", nullable: false), + Name = table.Column(type: "nvarchar(255)", maxLength: 255, nullable: false), + Deleted = table.Column(type: "bit", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Tenants", x => x.Id); + }); + + migrationBuilder.InsertData( + table: "Tenants", + columns: new[] { "Id", "Deleted", "Name" }, + values: new object[] { new Guid("b542bf25-134c-47a2-a0df-84ed14d03c4a"), false, "Admin Tenant" }); + + migrationBuilder.InsertData( + table: "Users", + columns: new[] { "Id", "Deleted", "Email", "FirstName", "LastName", "Password", "Role", "TenantId" }, + values: new object[] { new Guid("7e3892c0-9374-49fa-a3fd-53db637a40ae"), false, "admin@email.com", "Admin", "User", "$2a$12$Blal/uiFIJdYsCLTMUik/egLbfg3XhbnxBC6Sb5IKz2ZYhiU/MzL2", 0, new Guid("b542bf25-134c-47a2-a0df-84ed14d03c4a") }); + + migrationBuilder.CreateIndex( + name: "IX_Users_TenantId", + table: "Users", + column: "TenantId"); + + migrationBuilder.AddForeignKey( + name: "FK_Users_Tenants_TenantId", + table: "Users", + column: "TenantId", + principalTable: "Tenants", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_Users_Tenants_TenantId", + table: "Users"); + + migrationBuilder.DropTable( + name: "Tenants"); + + migrationBuilder.DropIndex( + name: "IX_Users_TenantId", + table: "Users"); + + migrationBuilder.DeleteData( + table: "Users", + keyColumn: "Id", + keyValue: new Guid("7e3892c0-9374-49fa-a3fd-53db637a40ae")); + + migrationBuilder.DropColumn( + name: "TenantId", + table: "Users"); + + migrationBuilder.InsertData( + table: "Users", + columns: new[] { "Id", "Deleted", "Email", "FirstName", "LastName", "Password", "Role" }, + values: new object[] { new Guid("28fc3d91-6a15-448e-b0b5-0c91a3948961"), false, "admin@email.com", "Admin", "User", "$2a$12$Blal/uiFIJdYsCLTMUik/egLbfg3XhbnxBC6Sb5IKz2ZYhiU/MzL2", 0 }); + } + } +} diff --git a/CleanArchitecture.Infrastructure/Migrations/ApplicationDbContextModelSnapshot.cs b/CleanArchitecture.Infrastructure/Migrations/ApplicationDbContextModelSnapshot.cs index 6a2eca6..2c15819 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.5") + .HasAnnotation("ProductVersion", "7.0.10") .HasAnnotation("Proxies:ChangeTracking", false) .HasAnnotation("Proxies:CheckEquality", false) .HasAnnotation("Proxies:LazyLoading", true) @@ -25,6 +25,33 @@ namespace CleanArchitecture.Infrastructure.Migrations SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + modelBuilder.Entity("CleanArchitecture.Domain.Entities.Tenant", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("Deleted") + .HasColumnType("bit"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("nvarchar(255)"); + + b.HasKey("Id"); + + b.ToTable("Tenants"); + + b.HasData( + new + { + Id = new Guid("b542bf25-134c-47a2-a0df-84ed14d03c4a"), + Deleted = false, + Name = "Admin Tenant" + }); + }); + modelBuilder.Entity("CleanArchitecture.Domain.Entities.User", b => { b.Property("Id") @@ -57,22 +84,44 @@ namespace CleanArchitecture.Infrastructure.Migrations b.Property("Role") .HasColumnType("int"); + b.Property("TenantId") + .HasColumnType("uniqueidentifier"); + b.HasKey("Id"); + b.HasIndex("TenantId"); + b.ToTable("Users"); b.HasData( new { - Id = new Guid("28fc3d91-6a15-448e-b0b5-0c91a3948961"), + Id = new Guid("7e3892c0-9374-49fa-a3fd-53db637a40ae"), Deleted = false, Email = "admin@email.com", FirstName = "Admin", LastName = "User", Password = "$2a$12$Blal/uiFIJdYsCLTMUik/egLbfg3XhbnxBC6Sb5IKz2ZYhiU/MzL2", - Role = 0 + Role = 0, + TenantId = new Guid("b542bf25-134c-47a2-a0df-84ed14d03c4a") }); }); + + modelBuilder.Entity("CleanArchitecture.Domain.Entities.User", b => + { + b.HasOne("CleanArchitecture.Domain.Entities.Tenant", "Tenant") + .WithMany("Users") + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Tenant"); + }); + + modelBuilder.Entity("CleanArchitecture.Domain.Entities.Tenant", b => + { + b.Navigation("Users"); + }); #pragma warning restore 612, 618 } } From 64fb1067e0334545dc7032802ed1ec5aed5a3079 Mon Sep 17 00:00:00 2001 From: alex289 Date: Mon, 28 Aug 2023 18:03:23 +0200 Subject: [PATCH 03/10] fix: Use existing tenant id --- .../Controller/UserControllerTests.cs | 3 ++- .../Fixtures/gRPC/GetUsersByIdsTestFixture.cs | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/CleanArchitecture.IntegrationTests/Controller/UserControllerTests.cs b/CleanArchitecture.IntegrationTests/Controller/UserControllerTests.cs index 8a90362..ee176ec 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.Constants; using CleanArchitecture.Domain.Enums; using CleanArchitecture.IntegrationTests.Extensions; using CleanArchitecture.IntegrationTests.Fixtures; @@ -33,7 +34,7 @@ public sealed class UserControllerTests : IClassFixture "Test", "Email", _fixture.CreatedUserPassword, - Guid.NewGuid()); + Ids.Seed.TenantId); var response = await _fixture.ServerClient.PostAsJsonAsync("/api/v1/user", user); diff --git a/CleanArchitecture.IntegrationTests/Fixtures/gRPC/GetUsersByIdsTestFixture.cs b/CleanArchitecture.IntegrationTests/Fixtures/gRPC/GetUsersByIdsTestFixture.cs index b06aecd..22ce59b 100644 --- a/CleanArchitecture.IntegrationTests/Fixtures/gRPC/GetUsersByIdsTestFixture.cs +++ b/CleanArchitecture.IntegrationTests/Fixtures/gRPC/GetUsersByIdsTestFixture.cs @@ -1,4 +1,5 @@ using System; +using CleanArchitecture.Domain.Constants; using CleanArchitecture.Domain.Entities; using CleanArchitecture.Domain.Enums; using CleanArchitecture.Infrastructure.Database; @@ -33,7 +34,7 @@ public sealed class GetUsersByIdsTestFixture : TestFixtureBase { return new User( CreatedUserId, - Guid.NewGuid(), + Ids.Seed.TenantId, "user@user.de", "User First Name", "User Last Name", From 816d92fc854e6477248a72e8a44c4a6beebd44e1 Mon Sep 17 00:00:00 2001 From: alex289 Date: Mon, 28 Aug 2023 19:41:49 +0200 Subject: [PATCH 04/10] feat: Add endpoints for tenants --- .../Controllers/TenantController.cs | 75 +++++++++++++++++++ .../Controllers/UserController.cs | 9 +-- .../CleanArchitecture.Application.csproj | 2 + .../Extensions/ServiceCollectionExtension.cs | 9 +++ .../Interfaces/ITenantService.cs | 15 ++++ .../Tenants/GetAll/GetAllTenantsQuery.cs | 7 ++ .../GetAll/GetAllTenantsQueryHandler.cs | 32 ++++++++ .../GetTenantById/GetTenantByIdQuery.cs | 7 ++ .../GetTenantByIdQueryHandler.cs | 45 +++++++++++ .../GetUserById/GetUserByIdQueryHandler.cs | 2 +- .../Services/TenantService.cs | 56 ++++++++++++++ .../Tenants/CreateTenantViewModel.cs | 3 + .../ViewModels/Tenants/TenantViewModel.cs | 24 ++++++ .../Tenants/UpdateTenantViewModel.cs | 7 ++ .../gRPC/UsersApiImplementation.cs | 6 +- .../ChangePasswordCommandHandlerTests.cs | 6 +- .../ChangePasswordCommandValidationTests.cs | 24 +++--- .../CreateUserCommandHandlerTests.cs | 6 +- .../CreateUserCommandValidationTests.cs | 40 +++++----- .../DeleteUserCommandHandlerTests.cs | 4 +- .../DeleteUserCommandValidationTests.cs | 2 +- .../LoginUser/LoginUserCommandHandlerTests.cs | 4 +- .../LoginUserCommandValidationTests.cs | 30 ++++---- .../UpdateUserCommandHandlerTests.cs | 8 +- .../UpdateUserCommandValidationTests.cs | 16 ++-- CleanArchitecture.Domain/ApiUser.cs | 4 +- .../Commands/CommandHandlerBase.cs | 2 +- .../CreateTenant/CreateTenantCommand.cs | 21 ++++++ .../CreateTenantCommandHandler.cs | 58 ++++++++++++++ .../CreateTenantCommandValidation.cs | 33 ++++++++ .../DeleteTenant/DeleteTenantCommand.cs | 18 +++++ .../DeleteTenantCommandHandler.cs | 63 ++++++++++++++++ .../DeleteTenantCommandValidation.cs | 20 +++++ .../UpdateTenant/UpdateTenantCommand.cs | 21 ++++++ .../UpdateTenantCommandHandler.cs | 55 ++++++++++++++ .../UpdateTenantCommandValidation.cs | 33 ++++++++ .../ChangePassword/ChangePasswordCommand.cs | 4 +- .../ChangePasswordCommandHandler.cs | 6 +- .../Users/CreateUser/CreateUserCommand.cs | 4 +- .../CreateUser/CreateUserCommandHandler.cs | 12 +-- .../CreateUser/CreateUserCommandValidation.cs | 14 ++-- .../Users/DeleteUser/DeleteUserCommand.cs | 4 +- .../DeleteUser/DeleteUserCommandHandler.cs | 4 +- .../DeleteUser/DeleteUserCommandValidation.cs | 2 +- .../Users/LoginUser/LoginUserCommand.cs | 4 +- .../LoginUser/LoginUserCommandHandler.cs | 6 +- .../LoginUser/LoginUserCommandValidation.cs | 4 +- .../Users/UpdateUser/UpdateUserCommand.cs | 4 +- .../UpdateUser/UpdateUserCommandHandler.cs | 10 +-- .../UpdateUser/UpdateUserCommandValidation.cs | 16 ++-- CleanArchitecture.Domain/Entities/Tenant.cs | 5 ++ .../Errors/DomainErrorCodes.cs | 54 ++++++++----- .../EventHandler/TenantEventHandler.cs | 27 +++++++ .../Events/Tenant/TenantCreatedEvent.cs | 14 ++++ .../Events/Tenant/TenantDeletedEvent.cs | 11 +++ .../Events/Tenant/TenantUpdatedEvent.cs | 14 ++++ .../Events/User/PasswordChangedEvent.cs | 3 - .../Events/User/UserCreatedEvent.cs | 3 - .../Events/User/UserDeletedEvent.cs | 3 - .../Events/User/UserUpdatedEvent.cs | 3 - .../Extensions/ServiceCollectionExtension.cs | 13 ++++ .../Extensions/Validation/CustomValidator.cs | 14 ++-- .../Interfaces/Repositories/IRepository.cs | 1 + .../Repositories/ITenantRepository.cs | 7 ++ .../EventSourcing/EventStoreContext.cs | 2 +- .../Extensions/ServiceCollectionExtensions.cs | 1 + .../Repositories/BaseRepository.cs | 18 ++++- .../Repositories/TenantRepository.cs | 12 +++ ...ctionalTestsServiceCollectionExtensions.cs | 2 +- .../gRPC/GetUsersByIdsTests.cs | 2 +- .../CleanArchitecture.Proto.csproj | 7 +- CleanArchitecture.Proto/Tenants/Models.proto | 17 +++++ .../Tenants/TenantsApi.proto | 9 +++ CleanArchitecture.Proto/Users/Models.proto | 4 +- CleanArchitecture.Proto/Users/UsersApi.proto | 2 +- .../CleanArchitecture.Shared.csproj | 1 + .../Tenants/TenantViewModel.cs | 7 ++ .../Users/GetUsersByIdsTests.cs | 4 +- CleanArchitecture.gRPC/CleanArchitecture.cs | 8 +- .../Contexts/TenantsContext.cs | 32 ++++++++ .../Contexts/UsersContext.cs | 2 +- .../Extensions/ServiceCollectionExtensions.cs | 13 +++- CleanArchitecture.gRPC/ICleanArchitecture.cs | 1 + .../Interfaces/ITenantsContext.cs | 11 +++ 84 files changed, 998 insertions(+), 190 deletions(-) create mode 100644 CleanArchitecture.Api/Controllers/TenantController.cs create mode 100644 CleanArchitecture.Application/Interfaces/ITenantService.cs create mode 100644 CleanArchitecture.Application/Queries/Tenants/GetAll/GetAllTenantsQuery.cs create mode 100644 CleanArchitecture.Application/Queries/Tenants/GetAll/GetAllTenantsQueryHandler.cs create mode 100644 CleanArchitecture.Application/Queries/Tenants/GetTenantById/GetTenantByIdQuery.cs create mode 100644 CleanArchitecture.Application/Queries/Tenants/GetTenantById/GetTenantByIdQueryHandler.cs create mode 100644 CleanArchitecture.Application/Services/TenantService.cs create mode 100644 CleanArchitecture.Application/ViewModels/Tenants/CreateTenantViewModel.cs create mode 100644 CleanArchitecture.Application/ViewModels/Tenants/TenantViewModel.cs create mode 100644 CleanArchitecture.Application/ViewModels/Tenants/UpdateTenantViewModel.cs create mode 100644 CleanArchitecture.Domain/Commands/Tenants/CreateTenant/CreateTenantCommand.cs create mode 100644 CleanArchitecture.Domain/Commands/Tenants/CreateTenant/CreateTenantCommandHandler.cs create mode 100644 CleanArchitecture.Domain/Commands/Tenants/CreateTenant/CreateTenantCommandValidation.cs create mode 100644 CleanArchitecture.Domain/Commands/Tenants/DeleteTenant/DeleteTenantCommand.cs create mode 100644 CleanArchitecture.Domain/Commands/Tenants/DeleteTenant/DeleteTenantCommandHandler.cs create mode 100644 CleanArchitecture.Domain/Commands/Tenants/DeleteTenant/DeleteTenantCommandValidation.cs create mode 100644 CleanArchitecture.Domain/Commands/Tenants/UpdateTenant/UpdateTenantCommand.cs create mode 100644 CleanArchitecture.Domain/Commands/Tenants/UpdateTenant/UpdateTenantCommandHandler.cs create mode 100644 CleanArchitecture.Domain/Commands/Tenants/UpdateTenant/UpdateTenantCommandValidation.cs create mode 100644 CleanArchitecture.Domain/EventHandler/TenantEventHandler.cs create mode 100644 CleanArchitecture.Domain/Events/Tenant/TenantCreatedEvent.cs create mode 100644 CleanArchitecture.Domain/Events/Tenant/TenantDeletedEvent.cs create mode 100644 CleanArchitecture.Domain/Events/Tenant/TenantUpdatedEvent.cs create mode 100644 CleanArchitecture.Domain/Interfaces/Repositories/ITenantRepository.cs create mode 100644 CleanArchitecture.Infrastructure/Repositories/TenantRepository.cs create mode 100644 CleanArchitecture.Proto/Tenants/Models.proto create mode 100644 CleanArchitecture.Proto/Tenants/TenantsApi.proto create mode 100644 CleanArchitecture.Shared/Tenants/TenantViewModel.cs create mode 100644 CleanArchitecture.gRPC/Contexts/TenantsContext.cs create mode 100644 CleanArchitecture.gRPC/Interfaces/ITenantsContext.cs diff --git a/CleanArchitecture.Api/Controllers/TenantController.cs b/CleanArchitecture.Api/Controllers/TenantController.cs new file mode 100644 index 0000000..639e2c8 --- /dev/null +++ b/CleanArchitecture.Api/Controllers/TenantController.cs @@ -0,0 +1,75 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using CleanArchitecture.Api.Models; +using CleanArchitecture.Application.Interfaces; +using CleanArchitecture.Application.ViewModels.Tenants; +using CleanArchitecture.Domain.Notifications; +using MediatR; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Swashbuckle.AspNetCore.Annotations; + +namespace CleanArchitecture.Api.Controllers; + +[ApiController] +[Authorize] +[Route("/api/v1/[controller]")] +public sealed class TenantController : ApiController +{ + private readonly ITenantService _tenantService; + + public TenantController( + INotificationHandler notifications, + ITenantService tenantService) : base(notifications) + { + _tenantService = tenantService; + } + + [HttpGet] + [SwaggerOperation("Get a list of all tenants")] + [SwaggerResponse(200, "Request successful", typeof(ResponseMessage>))] + public async Task GetAllTenantsAsync() + { + var tenants = await _tenantService.GetAllTenantsAsync(); + return Response(tenants); + } + + [HttpGet("{id:guid}")] + [SwaggerOperation("Get a tenant by id")] + [SwaggerResponse(200, "Request successful", typeof(ResponseMessage))] + public async Task GetTenantByIdAsync( + [FromRoute] Guid id, + [FromQuery] bool isDeleted = false) + { + var tenant = await _tenantService.GetTenantByIdAsync(id, isDeleted); + return Response(tenant); + } + + [HttpPost] + [SwaggerOperation("Create a new tenant")] + [SwaggerResponse(200, "Request successful", typeof(ResponseMessage))] + public async Task CreateTenantAsync([FromBody] CreateTenantViewModel tenant) + { + var tenantId = await _tenantService.CreateTenantAsync(tenant); + return Response(tenantId); + } + + [HttpPut] + [SwaggerOperation("Update an existing tenant")] + [SwaggerResponse(200, "Request successful", typeof(ResponseMessage))] + public async Task UpdateTenantAsync([FromBody] UpdateTenantViewModel tenant) + { + await _tenantService.UpdateTenantAsync(tenant); + return Response(tenant); + } + + [HttpDelete("{id:guid}")] + [SwaggerOperation("Delete an existing tenant")] + [SwaggerResponse(200, "Request successful", typeof(ResponseMessage))] + public async Task DeleteTenantAsync([FromRoute] Guid id) + { + await _tenantService.DeleteTenantAsync(id); + return Response(id); + } +} \ No newline at end of file diff --git a/CleanArchitecture.Api/Controllers/UserController.cs b/CleanArchitecture.Api/Controllers/UserController.cs index b6f7904..aa66217 100644 --- a/CleanArchitecture.Api/Controllers/UserController.cs +++ b/CleanArchitecture.Api/Controllers/UserController.cs @@ -13,6 +13,7 @@ using Swashbuckle.AspNetCore.Annotations; namespace CleanArchitecture.Api.Controllers; [ApiController] +[Authorize] [Route("/api/v1/[controller]")] public sealed class UserController : ApiController { @@ -25,7 +26,6 @@ public sealed class UserController : ApiController _userService = userService; } - [Authorize] [HttpGet] [SwaggerOperation("Get a list of all users")] [SwaggerResponse(200, "Request successful", typeof(ResponseMessage>))] @@ -35,7 +35,6 @@ public sealed class UserController : ApiController return Response(users); } - [Authorize] [HttpGet("{id:guid}")] [SwaggerOperation("Get a user by id")] [SwaggerResponse(200, "Request successful", typeof(ResponseMessage))] @@ -47,7 +46,6 @@ public sealed class UserController : ApiController return Response(user); } - [Authorize] [HttpGet("me")] [SwaggerOperation("Get the current active user")] [SwaggerResponse(200, "Request successful", typeof(ResponseMessage))] @@ -58,6 +56,7 @@ public sealed class UserController : ApiController } [HttpPost] + [AllowAnonymous] [SwaggerOperation("Create a new user")] [SwaggerResponse(200, "Request successful", typeof(ResponseMessage))] public async Task CreateUserAsync([FromBody] CreateUserViewModel viewModel) @@ -66,7 +65,6 @@ public sealed class UserController : ApiController return Response(userId); } - [Authorize] [HttpDelete("{id:guid}")] [SwaggerOperation("Delete a user")] [SwaggerResponse(200, "Request successful", typeof(ResponseMessage))] @@ -76,7 +74,6 @@ public sealed class UserController : ApiController return Response(id); } - [Authorize] [HttpPut] [SwaggerOperation("Update a user")] [SwaggerResponse(200, "Request successful", typeof(ResponseMessage))] @@ -86,7 +83,6 @@ public sealed class UserController : ApiController return Response(viewModel); } - [Authorize] [HttpPost("changePassword")] [SwaggerOperation("Change a password for the current active user")] [SwaggerResponse(200, "Request successful", typeof(ResponseMessage))] @@ -97,6 +93,7 @@ public sealed class UserController : ApiController } [HttpPost("login")] + [AllowAnonymous] [SwaggerOperation("Get a signed token for a user")] [SwaggerResponse(200, "Request successful", typeof(ResponseMessage))] public async Task LoginUserAsync([FromBody] LoginUserViewModel viewModel) diff --git a/CleanArchitecture.Application/CleanArchitecture.Application.csproj b/CleanArchitecture.Application/CleanArchitecture.Application.csproj index fd17662..2c7102f 100644 --- a/CleanArchitecture.Application/CleanArchitecture.Application.csproj +++ b/CleanArchitecture.Application/CleanArchitecture.Application.csproj @@ -14,4 +14,6 @@ + + diff --git a/CleanArchitecture.Application/Extensions/ServiceCollectionExtension.cs b/CleanArchitecture.Application/Extensions/ServiceCollectionExtension.cs index 784380f..03d5044 100644 --- a/CleanArchitecture.Application/Extensions/ServiceCollectionExtension.cs +++ b/CleanArchitecture.Application/Extensions/ServiceCollectionExtension.cs @@ -1,8 +1,11 @@ using System.Collections.Generic; using CleanArchitecture.Application.Interfaces; +using CleanArchitecture.Application.Queries.Tenants.GetAll; +using CleanArchitecture.Application.Queries.Tenants.GetTenantById; using CleanArchitecture.Application.Queries.Users.GetAll; using CleanArchitecture.Application.Queries.Users.GetUserById; using CleanArchitecture.Application.Services; +using CleanArchitecture.Application.ViewModels.Tenants; using CleanArchitecture.Application.ViewModels.Users; using MediatR; using Microsoft.Extensions.DependencyInjection; @@ -14,15 +17,21 @@ public static class ServiceCollectionExtension public static IServiceCollection AddServices(this IServiceCollection services) { services.AddScoped(); + services.AddScoped(); return services; } public static IServiceCollection AddQueryHandlers(this IServiceCollection services) { + // User services.AddScoped, GetUserByIdQueryHandler>(); services.AddScoped>, GetAllUsersQueryHandler>(); + // Tenant + services.AddScoped, GetTenantByIdQueryHandler>(); + services.AddScoped>, GetAllTenantsQueryHandler>(); + return services; } } \ No newline at end of file diff --git a/CleanArchitecture.Application/Interfaces/ITenantService.cs b/CleanArchitecture.Application/Interfaces/ITenantService.cs new file mode 100644 index 0000000..fbf4332 --- /dev/null +++ b/CleanArchitecture.Application/Interfaces/ITenantService.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using CleanArchitecture.Application.ViewModels.Tenants; + +namespace CleanArchitecture.Application.Interfaces; + +public interface ITenantService +{ + public Task CreateTenantAsync(CreateTenantViewModel tenant); + public Task UpdateTenantAsync(UpdateTenantViewModel tenant); + public Task DeleteTenantAsync(Guid tenantId); + public Task GetTenantByIdAsync(Guid tenantId, bool deleted); + public Task> GetAllTenantsAsync(); +} \ No newline at end of file diff --git a/CleanArchitecture.Application/Queries/Tenants/GetAll/GetAllTenantsQuery.cs b/CleanArchitecture.Application/Queries/Tenants/GetAll/GetAllTenantsQuery.cs new file mode 100644 index 0000000..09e81a7 --- /dev/null +++ b/CleanArchitecture.Application/Queries/Tenants/GetAll/GetAllTenantsQuery.cs @@ -0,0 +1,7 @@ +using System.Collections.Generic; +using CleanArchitecture.Application.ViewModels.Tenants; +using MediatR; + +namespace CleanArchitecture.Application.Queries.Tenants.GetAll; + +public sealed record GetAllTenantsQuery() : IRequest>; \ No newline at end of file diff --git a/CleanArchitecture.Application/Queries/Tenants/GetAll/GetAllTenantsQueryHandler.cs b/CleanArchitecture.Application/Queries/Tenants/GetAll/GetAllTenantsQueryHandler.cs new file mode 100644 index 0000000..cd21b8a --- /dev/null +++ b/CleanArchitecture.Application/Queries/Tenants/GetAll/GetAllTenantsQueryHandler.cs @@ -0,0 +1,32 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using CleanArchitecture.Application.ViewModels.Tenants; +using CleanArchitecture.Domain.Interfaces.Repositories; +using MediatR; +using Microsoft.EntityFrameworkCore; + +namespace CleanArchitecture.Application.Queries.Tenants.GetAll; + +public sealed class GetAllTenantsQueryHandler : + IRequestHandler> +{ + private readonly ITenantRepository _tenantRepository; + + public GetAllTenantsQueryHandler(ITenantRepository tenantRepository) + { + _tenantRepository = tenantRepository; + } + + public async Task> Handle( + GetAllTenantsQuery request, + CancellationToken cancellationToken) + { + return await _tenantRepository + .GetAllNoTracking() + .Where(x => !x.Deleted) + .Select(x => TenantViewModel.FromTenant(x)) + .ToListAsync(cancellationToken); + } +} \ No newline at end of file diff --git a/CleanArchitecture.Application/Queries/Tenants/GetTenantById/GetTenantByIdQuery.cs b/CleanArchitecture.Application/Queries/Tenants/GetTenantById/GetTenantByIdQuery.cs new file mode 100644 index 0000000..8639dfe --- /dev/null +++ b/CleanArchitecture.Application/Queries/Tenants/GetTenantById/GetTenantByIdQuery.cs @@ -0,0 +1,7 @@ +using System; +using CleanArchitecture.Application.ViewModels.Tenants; +using MediatR; + +namespace CleanArchitecture.Application.Queries.Tenants.GetTenantById; + +public sealed record GetTenantByIdQuery(Guid TenantId, bool IsDeleted) : IRequest; \ No newline at end of file diff --git a/CleanArchitecture.Application/Queries/Tenants/GetTenantById/GetTenantByIdQueryHandler.cs b/CleanArchitecture.Application/Queries/Tenants/GetTenantById/GetTenantByIdQueryHandler.cs new file mode 100644 index 0000000..751ab59 --- /dev/null +++ b/CleanArchitecture.Application/Queries/Tenants/GetTenantById/GetTenantByIdQueryHandler.cs @@ -0,0 +1,45 @@ +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using CleanArchitecture.Application.ViewModels.Tenants; +using CleanArchitecture.Domain.Errors; +using CleanArchitecture.Domain.Interfaces; +using CleanArchitecture.Domain.Interfaces.Repositories; +using CleanArchitecture.Domain.Notifications; +using MediatR; + +namespace CleanArchitecture.Application.Queries.Tenants.GetTenantById; + +public sealed class GetTenantByIdQueryHandler : + IRequestHandler +{ + private readonly ITenantRepository _tenantRepository; + private readonly IMediatorHandler _bus; + + public GetTenantByIdQueryHandler(ITenantRepository tenantRepository, IMediatorHandler bus) + { + _tenantRepository = tenantRepository; + _bus = bus; + } + + public async Task Handle(GetTenantByIdQuery request, CancellationToken cancellationToken) + { + var tenant = _tenantRepository + .GetAllNoTracking() + .FirstOrDefault(x => + x.Id == request.TenantId && + x.Deleted == request.IsDeleted); + + if (tenant is null) + { + await _bus.RaiseEventAsync( + new DomainNotification( + nameof(GetTenantByIdQuery), + $"Tenant with id {request.TenantId} could not be found", + ErrorCodes.ObjectNotFound)); + return null; + } + + return TenantViewModel.FromTenant(tenant); + } +} \ 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 f057731..aa7974f 100644 --- a/CleanArchitecture.Application/Queries/Users/GetUserById/GetUserByIdQueryHandler.cs +++ b/CleanArchitecture.Application/Queries/Users/GetUserById/GetUserByIdQueryHandler.cs @@ -30,7 +30,7 @@ public sealed class GetUserByIdQueryHandler : x.Id == request.UserId && x.Deleted == request.IsDeleted); - if (user == null) + if (user is null) { await _bus.RaiseEventAsync( new DomainNotification( diff --git a/CleanArchitecture.Application/Services/TenantService.cs b/CleanArchitecture.Application/Services/TenantService.cs new file mode 100644 index 0000000..62279c0 --- /dev/null +++ b/CleanArchitecture.Application/Services/TenantService.cs @@ -0,0 +1,56 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using CleanArchitecture.Application.Interfaces; +using CleanArchitecture.Application.Queries.Tenants.GetAll; +using CleanArchitecture.Application.Queries.Tenants.GetTenantById; +using CleanArchitecture.Application.ViewModels.Tenants; +using CleanArchitecture.Domain.Commands.Tenants.CreateTenant; +using CleanArchitecture.Domain.Commands.Tenants.DeleteTenant; +using CleanArchitecture.Domain.Commands.Tenants.UpdateTenant; +using CleanArchitecture.Domain.Interfaces; + +namespace CleanArchitecture.Application.Services; + +public sealed class TenantService : ITenantService +{ + private readonly IMediatorHandler _bus; + + public TenantService(IMediatorHandler bus) + { + _bus = bus; + } + + public async Task CreateTenantAsync(CreateTenantViewModel tenant) + { + var tenantId = Guid.NewGuid(); + + await _bus.SendCommandAsync(new CreateTenantCommand( + tenantId, + tenant.Name)); + + return tenantId; + } + + public async Task UpdateTenantAsync(UpdateTenantViewModel tenant) + { + await _bus.SendCommandAsync(new UpdateTenantCommand( + tenant.Id, + tenant.Name)); + } + + public async Task DeleteTenantAsync(Guid tenantId) + { + await _bus.SendCommandAsync(new DeleteTenantCommand(tenantId)); + } + + public async Task GetTenantByIdAsync(Guid tenantId, bool deleted) + { + return await _bus.QueryAsync(new GetTenantByIdQuery(tenantId, deleted)); + } + + public async Task> GetAllTenantsAsync() + { + return await _bus.QueryAsync(new GetAllTenantsQuery()); + } +} \ No newline at end of file diff --git a/CleanArchitecture.Application/ViewModels/Tenants/CreateTenantViewModel.cs b/CleanArchitecture.Application/ViewModels/Tenants/CreateTenantViewModel.cs new file mode 100644 index 0000000..e536004 --- /dev/null +++ b/CleanArchitecture.Application/ViewModels/Tenants/CreateTenantViewModel.cs @@ -0,0 +1,3 @@ +namespace CleanArchitecture.Application.ViewModels.Tenants; + +public sealed record CreateTenantViewModel(string Name); \ No newline at end of file diff --git a/CleanArchitecture.Application/ViewModels/Tenants/TenantViewModel.cs b/CleanArchitecture.Application/ViewModels/Tenants/TenantViewModel.cs new file mode 100644 index 0000000..586768f --- /dev/null +++ b/CleanArchitecture.Application/ViewModels/Tenants/TenantViewModel.cs @@ -0,0 +1,24 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using CleanArchitecture.Application.ViewModels.Users; +using CleanArchitecture.Domain.Entities; + +namespace CleanArchitecture.Application.ViewModels.Tenants; + +public sealed class TenantViewModel +{ + public Guid Id { get; set; } + public string Name { get; set; } = string.Empty; + public IEnumerable Users { get; set; } = new List(); + + public static TenantViewModel FromTenant(Tenant tenant) + { + return new TenantViewModel + { + Id = tenant.Id, + Name = tenant.Name, + Users = tenant.Users.Select(UserViewModel.FromUser) + }; + } +} \ No newline at end of file diff --git a/CleanArchitecture.Application/ViewModels/Tenants/UpdateTenantViewModel.cs b/CleanArchitecture.Application/ViewModels/Tenants/UpdateTenantViewModel.cs new file mode 100644 index 0000000..72e595e --- /dev/null +++ b/CleanArchitecture.Application/ViewModels/Tenants/UpdateTenantViewModel.cs @@ -0,0 +1,7 @@ +using System; + +namespace CleanArchitecture.Application.ViewModels.Tenants; + +public sealed record UpdateTenantViewModel( + Guid Id, + string Name); \ No newline at end of file diff --git a/CleanArchitecture.Application/gRPC/UsersApiImplementation.cs b/CleanArchitecture.Application/gRPC/UsersApiImplementation.cs index d8f9599..ff872e1 100644 --- a/CleanArchitecture.Application/gRPC/UsersApiImplementation.cs +++ b/CleanArchitecture.Application/gRPC/UsersApiImplementation.cs @@ -18,8 +18,8 @@ public sealed class UsersApiImplementation : UsersApi.UsersApiBase _userRepository = userRepository; } - public override async Task GetByIds( - GetByIdsRequest request, + public override async Task GetByIds( + GetUsersByIdsRequest request, ServerCallContext context) { var idsAsGuids = new List(request.Ids.Count); @@ -45,7 +45,7 @@ public sealed class UsersApiImplementation : UsersApi.UsersApiBase }) .ToListAsync(); - var result = new GetByIdsResult(); + var result = new GetUsersByIdsResult(); result.Users.AddRange(users); diff --git a/CleanArchitecture.Domain.Tests/CommandHandler/User/ChangePassword/ChangePasswordCommandHandlerTests.cs b/CleanArchitecture.Domain.Tests/CommandHandler/User/ChangePassword/ChangePasswordCommandHandlerTests.cs index 08d16ca..293c603 100644 --- a/CleanArchitecture.Domain.Tests/CommandHandler/User/ChangePassword/ChangePasswordCommandHandlerTests.cs +++ b/CleanArchitecture.Domain.Tests/CommandHandler/User/ChangePassword/ChangePasswordCommandHandlerTests.cs @@ -22,7 +22,7 @@ public sealed class ChangePasswordCommandHandlerTests _fixture .VerifyNoDomainNotification() .VerifyCommit() - .VerifyRaisedEvent(x => x.UserId == user.Id); + .VerifyRaisedEvent(x => x.AggregateId == user.Id); } [Fact] @@ -40,7 +40,7 @@ public sealed class ChangePasswordCommandHandlerTests .VerifyAnyDomainNotification() .VerifyExistingNotification( ErrorCodes.ObjectNotFound, - $"There is no User with Id {userId}"); + $"There is no user with Id {userId}"); } [Fact] @@ -57,7 +57,7 @@ public sealed class ChangePasswordCommandHandlerTests .VerifyNoRaisedEvent() .VerifyAnyDomainNotification() .VerifyExistingNotification( - DomainErrorCodes.UserPasswordIncorrect, + DomainErrorCodes.User.UserPasswordIncorrect, "The password is incorrect"); } } \ 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 39c8186..4a1a41a 100644 --- a/CleanArchitecture.Domain.Tests/CommandHandler/User/ChangePassword/ChangePasswordCommandValidationTests.cs +++ b/CleanArchitecture.Domain.Tests/CommandHandler/User/ChangePassword/ChangePasswordCommandValidationTests.cs @@ -28,12 +28,12 @@ public sealed class ChangePasswordCommandValidationTests : var errors = new List { - DomainErrorCodes.UserEmptyPassword, - DomainErrorCodes.UserSpecialCharPassword, - DomainErrorCodes.UserNumberPassword, - DomainErrorCodes.UserLowercaseLetterPassword, - DomainErrorCodes.UserUppercaseLetterPassword, - DomainErrorCodes.UserShortPassword + DomainErrorCodes.User.UserEmptyPassword, + DomainErrorCodes.User.UserSpecialCharPassword, + DomainErrorCodes.User.UserNumberPassword, + DomainErrorCodes.User.UserLowercaseLetterPassword, + DomainErrorCodes.User.UserUppercaseLetterPassword, + DomainErrorCodes.User.UserShortPassword }; ShouldHaveExpectedErrors(command, errors.ToArray()); @@ -44,7 +44,7 @@ public sealed class ChangePasswordCommandValidationTests : { var command = CreateTestCommand("z8tnayvd5FNLU9AQm"); - ShouldHaveSingleError(command, DomainErrorCodes.UserSpecialCharPassword); + ShouldHaveSingleError(command, DomainErrorCodes.User.UserSpecialCharPassword); } [Fact] @@ -52,7 +52,7 @@ public sealed class ChangePasswordCommandValidationTests : { var command = CreateTestCommand("z]tnayvdFNLU:]AQm"); - ShouldHaveSingleError(command, DomainErrorCodes.UserNumberPassword); + ShouldHaveSingleError(command, DomainErrorCodes.User.UserNumberPassword); } [Fact] @@ -60,7 +60,7 @@ public sealed class ChangePasswordCommandValidationTests : { var command = CreateTestCommand("Z8]TNAYVDFNLU:]AQM"); - ShouldHaveSingleError(command, DomainErrorCodes.UserLowercaseLetterPassword); + ShouldHaveSingleError(command, DomainErrorCodes.User.UserLowercaseLetterPassword); } [Fact] @@ -68,7 +68,7 @@ public sealed class ChangePasswordCommandValidationTests : { var command = CreateTestCommand("z8]tnayvd5fnlu9:]aqm"); - ShouldHaveSingleError(command, DomainErrorCodes.UserUppercaseLetterPassword); + ShouldHaveSingleError(command, DomainErrorCodes.User.UserUppercaseLetterPassword); } [Fact] @@ -76,7 +76,7 @@ public sealed class ChangePasswordCommandValidationTests : { var command = CreateTestCommand("zA6{"); - ShouldHaveSingleError(command, DomainErrorCodes.UserShortPassword); + ShouldHaveSingleError(command, DomainErrorCodes.User.UserShortPassword); } [Fact] @@ -84,7 +84,7 @@ public sealed class ChangePasswordCommandValidationTests : { var command = CreateTestCommand(string.Concat(Enumerable.Repeat("zA6{", 12), 12)); - ShouldHaveSingleError(command, DomainErrorCodes.UserLongPassword); + ShouldHaveSingleError(command, DomainErrorCodes.User.UserLongPassword); } private static ChangePasswordCommand CreateTestCommand( diff --git a/CleanArchitecture.Domain.Tests/CommandHandler/User/CreateUser/CreateUserCommandHandlerTests.cs b/CleanArchitecture.Domain.Tests/CommandHandler/User/CreateUser/CreateUserCommandHandlerTests.cs index b902a98..14fcb04 100644 --- a/CleanArchitecture.Domain.Tests/CommandHandler/User/CreateUser/CreateUserCommandHandlerTests.cs +++ b/CleanArchitecture.Domain.Tests/CommandHandler/User/CreateUser/CreateUserCommandHandlerTests.cs @@ -28,7 +28,7 @@ public sealed class CreateUserCommandHandlerTests _fixture .VerifyNoDomainNotification() .VerifyCommit() - .VerifyRaisedEvent(x => x.UserId == command.UserId); + .VerifyRaisedEvent(x => x.AggregateId == command.UserId); } [Fact] @@ -51,7 +51,7 @@ public sealed class CreateUserCommandHandlerTests .VerifyNoRaisedEvent() .VerifyAnyDomainNotification() .VerifyExistingNotification( - DomainErrorCodes.UserAlreadyExists, - $"There is already a User with Id {command.UserId}"); + DomainErrorCodes.User.UserAlreadyExists, + $"There is already a user with Id {command.UserId}"); } } \ No newline at end of file diff --git a/CleanArchitecture.Domain.Tests/CommandHandler/User/CreateUser/CreateUserCommandValidationTests.cs b/CleanArchitecture.Domain.Tests/CommandHandler/User/CreateUser/CreateUserCommandValidationTests.cs index 7906a1f..3e303ec 100644 --- a/CleanArchitecture.Domain.Tests/CommandHandler/User/CreateUser/CreateUserCommandValidationTests.cs +++ b/CleanArchitecture.Domain.Tests/CommandHandler/User/CreateUser/CreateUserCommandValidationTests.cs @@ -29,7 +29,7 @@ public sealed class CreateUserCommandValidationTests : ShouldHaveSingleError( command, - DomainErrorCodes.UserEmptyId, + DomainErrorCodes.User.UserEmptyId, "User id may not be empty"); } @@ -40,7 +40,7 @@ public sealed class CreateUserCommandValidationTests : ShouldHaveSingleError( command, - DomainErrorCodes.UserInvalidEmail, + DomainErrorCodes.User.UserInvalidEmail, "Email is not a valid email address"); } @@ -51,7 +51,7 @@ public sealed class CreateUserCommandValidationTests : ShouldHaveSingleError( command, - DomainErrorCodes.UserInvalidEmail, + DomainErrorCodes.User.UserInvalidEmail, "Email is not a valid email address"); } @@ -62,7 +62,7 @@ public sealed class CreateUserCommandValidationTests : ShouldHaveSingleError( command, - DomainErrorCodes.UserEmailExceedsMaxLength, + DomainErrorCodes.User.UserEmailExceedsMaxLength, "Email may not be longer than 320 characters"); } @@ -73,7 +73,7 @@ public sealed class CreateUserCommandValidationTests : ShouldHaveSingleError( command, - DomainErrorCodes.UserEmptyFirstName, + DomainErrorCodes.User.UserEmptyFirstName, "FirstName may not be empty"); } @@ -84,7 +84,7 @@ public sealed class CreateUserCommandValidationTests : ShouldHaveSingleError( command, - DomainErrorCodes.UserFirstNameExceedsMaxLength, + DomainErrorCodes.User.UserFirstNameExceedsMaxLength, "FirstName may not be longer than 100 characters"); } @@ -95,7 +95,7 @@ public sealed class CreateUserCommandValidationTests : ShouldHaveSingleError( command, - DomainErrorCodes.UserEmptyLastName, + DomainErrorCodes.User.UserEmptyLastName, "LastName may not be empty"); } @@ -106,7 +106,7 @@ public sealed class CreateUserCommandValidationTests : ShouldHaveSingleError( command, - DomainErrorCodes.UserLastNameExceedsMaxLength, + DomainErrorCodes.User.UserLastNameExceedsMaxLength, "LastName may not be longer than 100 characters"); } @@ -117,12 +117,12 @@ public sealed class CreateUserCommandValidationTests : var errors = new List { - DomainErrorCodes.UserEmptyPassword, - DomainErrorCodes.UserSpecialCharPassword, - DomainErrorCodes.UserNumberPassword, - DomainErrorCodes.UserLowercaseLetterPassword, - DomainErrorCodes.UserUppercaseLetterPassword, - DomainErrorCodes.UserShortPassword + DomainErrorCodes.User.UserEmptyPassword, + DomainErrorCodes.User.UserSpecialCharPassword, + DomainErrorCodes.User.UserNumberPassword, + DomainErrorCodes.User.UserLowercaseLetterPassword, + DomainErrorCodes.User.UserUppercaseLetterPassword, + DomainErrorCodes.User.UserShortPassword }; ShouldHaveExpectedErrors(command, errors.ToArray()); @@ -133,7 +133,7 @@ public sealed class CreateUserCommandValidationTests : { var command = CreateTestCommand(password: "z8tnayvd5FNLU9AQm"); - ShouldHaveSingleError(command, DomainErrorCodes.UserSpecialCharPassword); + ShouldHaveSingleError(command, DomainErrorCodes.User.UserSpecialCharPassword); } [Fact] @@ -141,7 +141,7 @@ public sealed class CreateUserCommandValidationTests : { var command = CreateTestCommand(password: "z]tnayvdFNLU:]AQm"); - ShouldHaveSingleError(command, DomainErrorCodes.UserNumberPassword); + ShouldHaveSingleError(command, DomainErrorCodes.User.UserNumberPassword); } [Fact] @@ -149,7 +149,7 @@ public sealed class CreateUserCommandValidationTests : { var command = CreateTestCommand(password: "Z8]TNAYVDFNLU:]AQM"); - ShouldHaveSingleError(command, DomainErrorCodes.UserLowercaseLetterPassword); + ShouldHaveSingleError(command, DomainErrorCodes.User.UserLowercaseLetterPassword); } [Fact] @@ -157,7 +157,7 @@ public sealed class CreateUserCommandValidationTests : { var command = CreateTestCommand(password: "z8]tnayvd5fnlu9:]aqm"); - ShouldHaveSingleError(command, DomainErrorCodes.UserUppercaseLetterPassword); + ShouldHaveSingleError(command, DomainErrorCodes.User.UserUppercaseLetterPassword); } [Fact] @@ -165,7 +165,7 @@ public sealed class CreateUserCommandValidationTests : { var command = CreateTestCommand(password: "zA6{"); - ShouldHaveSingleError(command, DomainErrorCodes.UserShortPassword); + ShouldHaveSingleError(command, DomainErrorCodes.User.UserShortPassword); } [Fact] @@ -173,7 +173,7 @@ public sealed class CreateUserCommandValidationTests : { var command = CreateTestCommand(password: string.Concat(Enumerable.Repeat("zA6{", 12), 12)); - ShouldHaveSingleError(command, DomainErrorCodes.UserLongPassword); + ShouldHaveSingleError(command, DomainErrorCodes.User.UserLongPassword); } private static CreateUserCommand CreateTestCommand( diff --git a/CleanArchitecture.Domain.Tests/CommandHandler/User/DeleteUser/DeleteUserCommandHandlerTests.cs b/CleanArchitecture.Domain.Tests/CommandHandler/User/DeleteUser/DeleteUserCommandHandlerTests.cs index fabfd29..1e65782 100644 --- a/CleanArchitecture.Domain.Tests/CommandHandler/User/DeleteUser/DeleteUserCommandHandlerTests.cs +++ b/CleanArchitecture.Domain.Tests/CommandHandler/User/DeleteUser/DeleteUserCommandHandlerTests.cs @@ -22,7 +22,7 @@ public sealed class DeleteUserCommandHandlerTests _fixture .VerifyNoDomainNotification() .VerifyCommit() - .VerifyRaisedEvent(x => x.UserId == user.Id); + .VerifyRaisedEvent(x => x.AggregateId == user.Id); } [Fact] @@ -40,6 +40,6 @@ public sealed class DeleteUserCommandHandlerTests .VerifyAnyDomainNotification() .VerifyExistingNotification( ErrorCodes.ObjectNotFound, - $"There is no User with Id {command.UserId}"); + $"There is no user with Id {command.UserId}"); } } \ No newline at end of file diff --git a/CleanArchitecture.Domain.Tests/CommandHandler/User/DeleteUser/DeleteUserCommandValidationTests.cs b/CleanArchitecture.Domain.Tests/CommandHandler/User/DeleteUser/DeleteUserCommandValidationTests.cs index a2e6e45..9464154 100644 --- a/CleanArchitecture.Domain.Tests/CommandHandler/User/DeleteUser/DeleteUserCommandValidationTests.cs +++ b/CleanArchitecture.Domain.Tests/CommandHandler/User/DeleteUser/DeleteUserCommandValidationTests.cs @@ -27,7 +27,7 @@ public sealed class DeleteUserCommandValidationTests : ShouldHaveSingleError( command, - DomainErrorCodes.UserEmptyId, + DomainErrorCodes.User.UserEmptyId, "User id may not be empty"); } diff --git a/CleanArchitecture.Domain.Tests/CommandHandler/User/LoginUser/LoginUserCommandHandlerTests.cs b/CleanArchitecture.Domain.Tests/CommandHandler/User/LoginUser/LoginUserCommandHandlerTests.cs index ba4deaa..1c26051 100644 --- a/CleanArchitecture.Domain.Tests/CommandHandler/User/LoginUser/LoginUserCommandHandlerTests.cs +++ b/CleanArchitecture.Domain.Tests/CommandHandler/User/LoginUser/LoginUserCommandHandlerTests.cs @@ -57,7 +57,7 @@ public sealed class LoginUserCommandHandlerTests .VerifyAnyDomainNotification() .VerifyExistingNotification( ErrorCodes.ObjectNotFound, - $"There is no User with Email {command.Email}"); + $"There is no user with email {command.Email}"); token.Should().BeEmpty(); } @@ -74,7 +74,7 @@ public sealed class LoginUserCommandHandlerTests _fixture .VerifyAnyDomainNotification() .VerifyExistingNotification( - DomainErrorCodes.UserPasswordIncorrect, + DomainErrorCodes.User.UserPasswordIncorrect, "The password is incorrect"); token.Should().BeEmpty(); diff --git a/CleanArchitecture.Domain.Tests/CommandHandler/User/LoginUser/LoginUserCommandValidationTests.cs b/CleanArchitecture.Domain.Tests/CommandHandler/User/LoginUser/LoginUserCommandValidationTests.cs index fd85ce8..d875b73 100644 --- a/CleanArchitecture.Domain.Tests/CommandHandler/User/LoginUser/LoginUserCommandValidationTests.cs +++ b/CleanArchitecture.Domain.Tests/CommandHandler/User/LoginUser/LoginUserCommandValidationTests.cs @@ -28,7 +28,7 @@ public sealed class LoginUserCommandValidationTests : ShouldHaveSingleError( command, - DomainErrorCodes.UserInvalidEmail, + DomainErrorCodes.User.UserInvalidEmail, "Email is not a valid email address"); } @@ -39,7 +39,7 @@ public sealed class LoginUserCommandValidationTests : ShouldHaveSingleError( command, - DomainErrorCodes.UserInvalidEmail, + DomainErrorCodes.User.UserInvalidEmail, "Email is not a valid email address"); } @@ -50,7 +50,7 @@ public sealed class LoginUserCommandValidationTests : ShouldHaveSingleError( command, - DomainErrorCodes.UserEmailExceedsMaxLength, + DomainErrorCodes.User.UserEmailExceedsMaxLength, "Email may not be longer than 320 characters"); } @@ -61,12 +61,12 @@ public sealed class LoginUserCommandValidationTests : var errors = new List { - DomainErrorCodes.UserEmptyPassword, - DomainErrorCodes.UserSpecialCharPassword, - DomainErrorCodes.UserNumberPassword, - DomainErrorCodes.UserLowercaseLetterPassword, - DomainErrorCodes.UserUppercaseLetterPassword, - DomainErrorCodes.UserShortPassword + DomainErrorCodes.User.UserEmptyPassword, + DomainErrorCodes.User.UserSpecialCharPassword, + DomainErrorCodes.User.UserNumberPassword, + DomainErrorCodes.User.UserLowercaseLetterPassword, + DomainErrorCodes.User.UserUppercaseLetterPassword, + DomainErrorCodes.User.UserShortPassword }; ShouldHaveExpectedErrors(command, errors.ToArray()); @@ -77,7 +77,7 @@ public sealed class LoginUserCommandValidationTests : { var command = CreateTestCommand(password: "z8tnayvd5FNLU9AQm"); - ShouldHaveSingleError(command, DomainErrorCodes.UserSpecialCharPassword); + ShouldHaveSingleError(command, DomainErrorCodes.User.UserSpecialCharPassword); } [Fact] @@ -85,7 +85,7 @@ public sealed class LoginUserCommandValidationTests : { var command = CreateTestCommand(password: "z]tnayvdFNLU:]AQm"); - ShouldHaveSingleError(command, DomainErrorCodes.UserNumberPassword); + ShouldHaveSingleError(command, DomainErrorCodes.User.UserNumberPassword); } [Fact] @@ -93,7 +93,7 @@ public sealed class LoginUserCommandValidationTests : { var command = CreateTestCommand(password: "Z8]TNAYVDFNLU:]AQM"); - ShouldHaveSingleError(command, DomainErrorCodes.UserLowercaseLetterPassword); + ShouldHaveSingleError(command, DomainErrorCodes.User.UserLowercaseLetterPassword); } [Fact] @@ -101,7 +101,7 @@ public sealed class LoginUserCommandValidationTests : { var command = CreateTestCommand(password: "z8]tnayvd5fnlu9:]aqm"); - ShouldHaveSingleError(command, DomainErrorCodes.UserUppercaseLetterPassword); + ShouldHaveSingleError(command, DomainErrorCodes.User.UserUppercaseLetterPassword); } [Fact] @@ -109,7 +109,7 @@ public sealed class LoginUserCommandValidationTests : { var command = CreateTestCommand(password: "zA6{"); - ShouldHaveSingleError(command, DomainErrorCodes.UserShortPassword); + ShouldHaveSingleError(command, DomainErrorCodes.User.UserShortPassword); } [Fact] @@ -117,7 +117,7 @@ public sealed class LoginUserCommandValidationTests : { var command = CreateTestCommand(password: string.Concat(Enumerable.Repeat("zA6{", 12), 12)); - ShouldHaveSingleError(command, DomainErrorCodes.UserLongPassword); + ShouldHaveSingleError(command, DomainErrorCodes.User.UserLongPassword); } private static LoginUserCommand CreateTestCommand( diff --git a/CleanArchitecture.Domain.Tests/CommandHandler/User/UpdateUser/UpdateUserCommandHandlerTests.cs b/CleanArchitecture.Domain.Tests/CommandHandler/User/UpdateUser/UpdateUserCommandHandlerTests.cs index 66281d3..4bcb809 100644 --- a/CleanArchitecture.Domain.Tests/CommandHandler/User/UpdateUser/UpdateUserCommandHandlerTests.cs +++ b/CleanArchitecture.Domain.Tests/CommandHandler/User/UpdateUser/UpdateUserCommandHandlerTests.cs @@ -30,7 +30,7 @@ public sealed class UpdateUserCommandHandlerTests _fixture .VerifyNoDomainNotification() .VerifyCommit() - .VerifyRaisedEvent(x => x.UserId == command.UserId); + .VerifyRaisedEvent(x => x.AggregateId == command.UserId); } [Fact] @@ -53,7 +53,7 @@ public sealed class UpdateUserCommandHandlerTests .VerifyAnyDomainNotification() .VerifyExistingNotification( ErrorCodes.ObjectNotFound, - $"There is no User with Id {command.UserId}"); + $"There is no user with Id {command.UserId}"); } [Fact] @@ -86,7 +86,7 @@ public sealed class UpdateUserCommandHandlerTests .VerifyNoRaisedEvent() .VerifyAnyDomainNotification() .VerifyExistingNotification( - DomainErrorCodes.UserAlreadyExists, - $"There is already a User with Email {command.Email}"); + DomainErrorCodes.User.UserAlreadyExists, + $"There is already a user with email {command.Email}"); } } \ No newline at end of file diff --git a/CleanArchitecture.Domain.Tests/CommandHandler/User/UpdateUser/UpdateUserCommandValidationTests.cs b/CleanArchitecture.Domain.Tests/CommandHandler/User/UpdateUser/UpdateUserCommandValidationTests.cs index 7a919f4..762dcb1 100644 --- a/CleanArchitecture.Domain.Tests/CommandHandler/User/UpdateUser/UpdateUserCommandValidationTests.cs +++ b/CleanArchitecture.Domain.Tests/CommandHandler/User/UpdateUser/UpdateUserCommandValidationTests.cs @@ -28,7 +28,7 @@ public sealed class UpdateUserCommandValidationTests : ShouldHaveSingleError( command, - DomainErrorCodes.UserEmptyId, + DomainErrorCodes.User.UserEmptyId, "User id may not be empty"); } @@ -39,7 +39,7 @@ public sealed class UpdateUserCommandValidationTests : ShouldHaveSingleError( command, - DomainErrorCodes.UserInvalidEmail, + DomainErrorCodes.User.UserInvalidEmail, "Email is not a valid email address"); } @@ -50,7 +50,7 @@ public sealed class UpdateUserCommandValidationTests : ShouldHaveSingleError( command, - DomainErrorCodes.UserInvalidEmail, + DomainErrorCodes.User.UserInvalidEmail, "Email is not a valid email address"); } @@ -61,7 +61,7 @@ public sealed class UpdateUserCommandValidationTests : ShouldHaveSingleError( command, - DomainErrorCodes.UserEmailExceedsMaxLength, + DomainErrorCodes.User.UserEmailExceedsMaxLength, "Email may not be longer than 320 characters"); } @@ -72,7 +72,7 @@ public sealed class UpdateUserCommandValidationTests : ShouldHaveSingleError( command, - DomainErrorCodes.UserEmptyFirstName, + DomainErrorCodes.User.UserEmptyFirstName, "FirstName may not be empty"); } @@ -83,7 +83,7 @@ public sealed class UpdateUserCommandValidationTests : ShouldHaveSingleError( command, - DomainErrorCodes.UserFirstNameExceedsMaxLength, + DomainErrorCodes.User.UserFirstNameExceedsMaxLength, "FirstName may not be longer than 100 characters"); } @@ -94,7 +94,7 @@ public sealed class UpdateUserCommandValidationTests : ShouldHaveSingleError( command, - DomainErrorCodes.UserEmptyLastName, + DomainErrorCodes.User.UserEmptyLastName, "LastName may not be empty"); } @@ -105,7 +105,7 @@ public sealed class UpdateUserCommandValidationTests : ShouldHaveSingleError( command, - DomainErrorCodes.UserLastNameExceedsMaxLength, + DomainErrorCodes.User.UserLastNameExceedsMaxLength, "LastName may not be longer than 100 characters"); } diff --git a/CleanArchitecture.Domain/ApiUser.cs b/CleanArchitecture.Domain/ApiUser.cs index 708cec4..df1db8a 100644 --- a/CleanArchitecture.Domain/ApiUser.cs +++ b/CleanArchitecture.Domain/ApiUser.cs @@ -55,12 +55,12 @@ public sealed class ApiUser : IUser { get { - if (_name != null) + if (_name is not null) { return _name; } var identity = _httpContextAccessor.HttpContext?.User.Identity; - if (identity == null) + if (identity is null) { _name = string.Empty; return string.Empty; diff --git a/CleanArchitecture.Domain/Commands/CommandHandlerBase.cs b/CleanArchitecture.Domain/Commands/CommandHandlerBase.cs index 54a557b..6ab6b8f 100644 --- a/CleanArchitecture.Domain/Commands/CommandHandlerBase.cs +++ b/CleanArchitecture.Domain/Commands/CommandHandlerBase.cs @@ -62,7 +62,7 @@ public abstract class CommandHandlerBase return true; } - if (command.ValidationResult == null) + if (command.ValidationResult is null) { throw new InvalidOperationException("Command is invalid and should therefore have a validation result"); } diff --git a/CleanArchitecture.Domain/Commands/Tenants/CreateTenant/CreateTenantCommand.cs b/CleanArchitecture.Domain/Commands/Tenants/CreateTenant/CreateTenantCommand.cs new file mode 100644 index 0000000..3a97121 --- /dev/null +++ b/CleanArchitecture.Domain/Commands/Tenants/CreateTenant/CreateTenantCommand.cs @@ -0,0 +1,21 @@ +using System; + +namespace CleanArchitecture.Domain.Commands.Tenants.CreateTenant; + +public sealed class CreateTenantCommand : CommandBase +{ + private static readonly CreateTenantCommandValidation s_validation = new(); + + public string Name { get; } + + public CreateTenantCommand(Guid tenantId, string name) : base(tenantId) + { + Name = name; + } + + public override bool IsValid() + { + ValidationResult = s_validation.Validate(this); + return ValidationResult.IsValid; + } +} \ No newline at end of file diff --git a/CleanArchitecture.Domain/Commands/Tenants/CreateTenant/CreateTenantCommandHandler.cs b/CleanArchitecture.Domain/Commands/Tenants/CreateTenant/CreateTenantCommandHandler.cs new file mode 100644 index 0000000..78c4aa1 --- /dev/null +++ b/CleanArchitecture.Domain/Commands/Tenants/CreateTenant/CreateTenantCommandHandler.cs @@ -0,0 +1,58 @@ +using System.Threading; +using System.Threading.Tasks; +using CleanArchitecture.Domain.Entities; +using CleanArchitecture.Domain.Errors; +using CleanArchitecture.Domain.Events.Tenant; +using CleanArchitecture.Domain.Interfaces; +using CleanArchitecture.Domain.Interfaces.Repositories; +using CleanArchitecture.Domain.Notifications; +using MediatR; + +namespace CleanArchitecture.Domain.Commands.Tenants.CreateTenant; + +public sealed class CreateTenantCommandHandler : CommandHandlerBase, + IRequestHandler +{ + private readonly ITenantRepository _tenantRepository; + + public CreateTenantCommandHandler( + IMediatorHandler bus, + IUnitOfWork unitOfWork, + INotificationHandler notifications, + ITenantRepository tenantRepository) : base(bus, unitOfWork, notifications) + { + _tenantRepository = tenantRepository; + } + + public async Task Handle(CreateTenantCommand request, CancellationToken cancellationToken) + { + if (!await TestValidityAsync(request)) + { + return; + } + + if (await _tenantRepository.ExistsAsync(request.AggregateId)) + { + await NotifyAsync( + new DomainNotification( + request.MessageType, + $"There is already a tenant with Id {request.AggregateId}", + DomainErrorCodes.Tenant.TenantAlreadyExists)); + + return; + } + + var tenant = new Tenant( + request.AggregateId, + request.Name); + + _tenantRepository.Add(tenant); + + if (await CommitAsync()) + { + await Bus.RaiseEventAsync(new TenantCreatedEvent( + tenant.Id, + tenant.Name)); + } + } +} \ No newline at end of file diff --git a/CleanArchitecture.Domain/Commands/Tenants/CreateTenant/CreateTenantCommandValidation.cs b/CleanArchitecture.Domain/Commands/Tenants/CreateTenant/CreateTenantCommandValidation.cs new file mode 100644 index 0000000..4838663 --- /dev/null +++ b/CleanArchitecture.Domain/Commands/Tenants/CreateTenant/CreateTenantCommandValidation.cs @@ -0,0 +1,33 @@ +using CleanArchitecture.Domain.Constants; +using CleanArchitecture.Domain.Errors; +using FluentValidation; + +namespace CleanArchitecture.Domain.Commands.Tenants.CreateTenant; + +public sealed class CreateTenantCommandValidation : AbstractValidator +{ + public CreateTenantCommandValidation() + { + AddRuleForId(); + AddRuleForName(); + } + + private void AddRuleForId() + { + RuleFor(cmd => cmd.AggregateId) + .NotEmpty() + .WithErrorCode(DomainErrorCodes.Tenant.TenantEmptyId) + .WithMessage("Tenant id may not be empty"); + } + + private void AddRuleForName() + { + RuleFor(cmd => cmd.Name) + .NotEmpty() + .WithErrorCode(DomainErrorCodes.Tenant.TenantEmptyName) + .WithMessage("Name may not be empty") + .MaximumLength(MaxLengths.Tenant.Name) + .WithErrorCode(DomainErrorCodes.Tenant.TenantNameExceedsMaxLength) + .WithMessage($"Name may not be longer than {MaxLengths.Tenant.Name} characters"); + } +} \ No newline at end of file diff --git a/CleanArchitecture.Domain/Commands/Tenants/DeleteTenant/DeleteTenantCommand.cs b/CleanArchitecture.Domain/Commands/Tenants/DeleteTenant/DeleteTenantCommand.cs new file mode 100644 index 0000000..9e112ca --- /dev/null +++ b/CleanArchitecture.Domain/Commands/Tenants/DeleteTenant/DeleteTenantCommand.cs @@ -0,0 +1,18 @@ +using System; + +namespace CleanArchitecture.Domain.Commands.Tenants.DeleteTenant; + +public sealed class DeleteTenantCommand : CommandBase +{ + private static readonly DeleteTenantCommandValidation s_validation = new(); + + public DeleteTenantCommand(Guid tenantId) : base(tenantId) + { + } + + public override bool IsValid() + { + ValidationResult = s_validation.Validate(this); + return ValidationResult.IsValid; + } +} \ No newline at end of file diff --git a/CleanArchitecture.Domain/Commands/Tenants/DeleteTenant/DeleteTenantCommandHandler.cs b/CleanArchitecture.Domain/Commands/Tenants/DeleteTenant/DeleteTenantCommandHandler.cs new file mode 100644 index 0000000..1a1ab43 --- /dev/null +++ b/CleanArchitecture.Domain/Commands/Tenants/DeleteTenant/DeleteTenantCommandHandler.cs @@ -0,0 +1,63 @@ +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using CleanArchitecture.Domain.Errors; +using CleanArchitecture.Domain.Events.Tenant; +using CleanArchitecture.Domain.Interfaces; +using CleanArchitecture.Domain.Interfaces.Repositories; +using CleanArchitecture.Domain.Notifications; +using MediatR; + +namespace CleanArchitecture.Domain.Commands.Tenants.DeleteTenant; + +public sealed class DeleteTenantCommandHandler : CommandHandlerBase, + IRequestHandler +{ + private readonly ITenantRepository _tenantRepository; + private readonly IUserRepository _userRepository; + + public DeleteTenantCommandHandler( + IMediatorHandler bus, + IUnitOfWork unitOfWork, + INotificationHandler notifications, + ITenantRepository tenantRepository, + IUserRepository userRepository) : base(bus, unitOfWork, notifications) + { + _tenantRepository = tenantRepository; + _userRepository = userRepository; + } + + public async Task Handle(DeleteTenantCommand request, CancellationToken cancellationToken) + { + if (!await TestValidityAsync(request)) + { + return; + } + + var tenant = await _tenantRepository.GetByIdAsync(request.AggregateId); + + if (tenant is null) + { + await NotifyAsync( + new DomainNotification( + request.MessageType, + $"There is no tenant with Id {request.AggregateId}", + ErrorCodes.ObjectNotFound)); + + return; + } + + var tenantUsers = _userRepository + .GetAll() + .Where(x => x.TenantId == request.AggregateId); + + _userRepository.RemoveRange(tenantUsers); + + _tenantRepository.Remove(tenant); + + if (await CommitAsync()) + { + await Bus.RaiseEventAsync(new TenantDeletedEvent(tenant.Id)); + } + } +} \ No newline at end of file diff --git a/CleanArchitecture.Domain/Commands/Tenants/DeleteTenant/DeleteTenantCommandValidation.cs b/CleanArchitecture.Domain/Commands/Tenants/DeleteTenant/DeleteTenantCommandValidation.cs new file mode 100644 index 0000000..dadddd8 --- /dev/null +++ b/CleanArchitecture.Domain/Commands/Tenants/DeleteTenant/DeleteTenantCommandValidation.cs @@ -0,0 +1,20 @@ +using CleanArchitecture.Domain.Errors; +using FluentValidation; + +namespace CleanArchitecture.Domain.Commands.Tenants.DeleteTenant; + +public sealed class DeleteTenantCommandValidation : AbstractValidator +{ + public DeleteTenantCommandValidation() + { + AddRuleForId(); + } + + private void AddRuleForId() + { + RuleFor(cmd => cmd.AggregateId) + .NotEmpty() + .WithErrorCode(DomainErrorCodes.Tenant.TenantEmptyId) + .WithMessage("Tenant id may not be empty"); + } +} \ No newline at end of file diff --git a/CleanArchitecture.Domain/Commands/Tenants/UpdateTenant/UpdateTenantCommand.cs b/CleanArchitecture.Domain/Commands/Tenants/UpdateTenant/UpdateTenantCommand.cs new file mode 100644 index 0000000..3962e9b --- /dev/null +++ b/CleanArchitecture.Domain/Commands/Tenants/UpdateTenant/UpdateTenantCommand.cs @@ -0,0 +1,21 @@ +using System; + +namespace CleanArchitecture.Domain.Commands.Tenants.UpdateTenant; + +public sealed class UpdateTenantCommand : CommandBase +{ + private static readonly UpdateTenantCommandValidation s_validation = new(); + + public string Name { get; } + + public UpdateTenantCommand(Guid tenantId, string name) : base(tenantId) + { + Name = name; + } + + public override bool IsValid() + { + ValidationResult = s_validation.Validate(this); + return ValidationResult.IsValid; + } +} \ No newline at end of file diff --git a/CleanArchitecture.Domain/Commands/Tenants/UpdateTenant/UpdateTenantCommandHandler.cs b/CleanArchitecture.Domain/Commands/Tenants/UpdateTenant/UpdateTenantCommandHandler.cs new file mode 100644 index 0000000..f1ba406 --- /dev/null +++ b/CleanArchitecture.Domain/Commands/Tenants/UpdateTenant/UpdateTenantCommandHandler.cs @@ -0,0 +1,55 @@ +using System.Threading; +using System.Threading.Tasks; +using CleanArchitecture.Domain.Errors; +using CleanArchitecture.Domain.Events.Tenant; +using CleanArchitecture.Domain.Interfaces; +using CleanArchitecture.Domain.Interfaces.Repositories; +using CleanArchitecture.Domain.Notifications; +using MediatR; + +namespace CleanArchitecture.Domain.Commands.Tenants.UpdateTenant; + +public sealed class UpdateTenantCommandHandler : CommandHandlerBase, + IRequestHandler +{ + private readonly ITenantRepository _tenantRepository; + + public UpdateTenantCommandHandler( + IMediatorHandler bus, + IUnitOfWork unitOfWork, + INotificationHandler notifications, + ITenantRepository tenantRepository) : base(bus, unitOfWork, notifications) + { + _tenantRepository = tenantRepository; + } + + public async Task Handle(UpdateTenantCommand request, CancellationToken cancellationToken) + { + if (!await TestValidityAsync(request)) + { + return; + } + + var tenant = await _tenantRepository.GetByIdAsync(request.AggregateId); + + if (tenant is null) + { + await NotifyAsync( + new DomainNotification( + request.MessageType, + $"There is no tenant with Id {request.AggregateId}", + ErrorCodes.ObjectNotFound)); + + return; + } + + tenant.SetName(request.Name); + + if (await CommitAsync()) + { + await Bus.RaiseEventAsync(new TenantUpdatedEvent( + tenant.Id, + tenant.Name)); + } + } +} \ No newline at end of file diff --git a/CleanArchitecture.Domain/Commands/Tenants/UpdateTenant/UpdateTenantCommandValidation.cs b/CleanArchitecture.Domain/Commands/Tenants/UpdateTenant/UpdateTenantCommandValidation.cs new file mode 100644 index 0000000..905e638 --- /dev/null +++ b/CleanArchitecture.Domain/Commands/Tenants/UpdateTenant/UpdateTenantCommandValidation.cs @@ -0,0 +1,33 @@ +using CleanArchitecture.Domain.Constants; +using CleanArchitecture.Domain.Errors; +using FluentValidation; + +namespace CleanArchitecture.Domain.Commands.Tenants.UpdateTenant; + +public sealed class UpdateTenantCommandValidation : AbstractValidator +{ + public UpdateTenantCommandValidation() + { + AddRuleForId(); + AddRuleForName(); + } + + private void AddRuleForId() + { + RuleFor(cmd => cmd.AggregateId) + .NotEmpty() + .WithErrorCode(DomainErrorCodes.Tenant.TenantEmptyId) + .WithMessage("Tenant id may not be empty"); + } + + private void AddRuleForName() + { + RuleFor(cmd => cmd.Name) + .NotEmpty() + .WithErrorCode(DomainErrorCodes.Tenant.TenantEmptyName) + .WithMessage("Name may not be empty") + .MaximumLength(MaxLengths.Tenant.Name) + .WithErrorCode(DomainErrorCodes.Tenant.TenantNameExceedsMaxLength) + .WithMessage($"Name may not be longer than {MaxLengths.Tenant.Name} characters"); + } +} \ No newline at end of file diff --git a/CleanArchitecture.Domain/Commands/Users/ChangePassword/ChangePasswordCommand.cs b/CleanArchitecture.Domain/Commands/Users/ChangePassword/ChangePasswordCommand.cs index 4a27534..8d60484 100644 --- a/CleanArchitecture.Domain/Commands/Users/ChangePassword/ChangePasswordCommand.cs +++ b/CleanArchitecture.Domain/Commands/Users/ChangePassword/ChangePasswordCommand.cs @@ -4,7 +4,7 @@ namespace CleanArchitecture.Domain.Commands.Users.ChangePassword; public sealed class ChangePasswordCommand : CommandBase { - private readonly ChangePasswordCommandValidation _validation = new(); + private static readonly ChangePasswordCommandValidation s_validation = new(); public ChangePasswordCommand(string password, string newPassword) : base(Guid.NewGuid()) { @@ -17,7 +17,7 @@ public sealed class ChangePasswordCommand : CommandBase public override bool IsValid() { - ValidationResult = _validation.Validate(this); + ValidationResult = s_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 index 38a85dd..713d43e 100644 --- a/CleanArchitecture.Domain/Commands/Users/ChangePassword/ChangePasswordCommandHandler.cs +++ b/CleanArchitecture.Domain/Commands/Users/ChangePassword/ChangePasswordCommandHandler.cs @@ -36,12 +36,12 @@ public sealed class ChangePasswordCommandHandler : CommandHandlerBase, var user = await _userRepository.GetByIdAsync(_user.GetUserId()); - if (user == null) + if (user is null) { await NotifyAsync( new DomainNotification( request.MessageType, - $"There is no User with Id {_user.GetUserId()}", + $"There is no user with Id {_user.GetUserId()}", ErrorCodes.ObjectNotFound)); return; @@ -53,7 +53,7 @@ public sealed class ChangePasswordCommandHandler : CommandHandlerBase, new DomainNotification( request.MessageType, "The password is incorrect", - DomainErrorCodes.UserPasswordIncorrect)); + DomainErrorCodes.User.UserPasswordIncorrect)); return; } diff --git a/CleanArchitecture.Domain/Commands/Users/CreateUser/CreateUserCommand.cs b/CleanArchitecture.Domain/Commands/Users/CreateUser/CreateUserCommand.cs index 62406ad..fb17c86 100644 --- a/CleanArchitecture.Domain/Commands/Users/CreateUser/CreateUserCommand.cs +++ b/CleanArchitecture.Domain/Commands/Users/CreateUser/CreateUserCommand.cs @@ -4,7 +4,7 @@ namespace CleanArchitecture.Domain.Commands.Users.CreateUser; public sealed class CreateUserCommand : CommandBase { - private readonly CreateUserCommandValidation _validation = new(); + private static readonly CreateUserCommandValidation s_validation = new(); public CreateUserCommand( Guid userId, @@ -31,7 +31,7 @@ public sealed class CreateUserCommand : CommandBase public override bool IsValid() { - ValidationResult = _validation.Validate(this); + ValidationResult = s_validation.Validate(this); return ValidationResult.IsValid; } } \ 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 464aad4..7e40c4d 100644 --- a/CleanArchitecture.Domain/Commands/Users/CreateUser/CreateUserCommandHandler.cs +++ b/CleanArchitecture.Domain/Commands/Users/CreateUser/CreateUserCommandHandler.cs @@ -35,25 +35,25 @@ public sealed class CreateUserCommandHandler : CommandHandlerBase, var existingUser = await _userRepository.GetByIdAsync(request.UserId); - if (existingUser != null) + if (existingUser is not null) { await Bus.RaiseEventAsync( new DomainNotification( request.MessageType, - $"There is already a User with Id {request.UserId}", - DomainErrorCodes.UserAlreadyExists)); + $"There is already a user with Id {request.UserId}", + DomainErrorCodes.User.UserAlreadyExists)); return; } existingUser = await _userRepository.GetByEmailAsync(request.Email); - if (existingUser != null) + if (existingUser is not null) { await Bus.RaiseEventAsync( new DomainNotification( request.MessageType, - $"There is already a User with Email {request.Email}", - DomainErrorCodes.UserAlreadyExists)); + $"There is already a user with email {request.Email}", + DomainErrorCodes.User.UserAlreadyExists)); return; } diff --git a/CleanArchitecture.Domain/Commands/Users/CreateUser/CreateUserCommandValidation.cs b/CleanArchitecture.Domain/Commands/Users/CreateUser/CreateUserCommandValidation.cs index 5a51947..c2d9564 100644 --- a/CleanArchitecture.Domain/Commands/Users/CreateUser/CreateUserCommandValidation.cs +++ b/CleanArchitecture.Domain/Commands/Users/CreateUser/CreateUserCommandValidation.cs @@ -20,7 +20,7 @@ public sealed class CreateUserCommandValidation : AbstractValidator cmd.UserId) .NotEmpty() - .WithErrorCode(DomainErrorCodes.UserEmptyId) + .WithErrorCode(DomainErrorCodes.User.UserEmptyId) .WithMessage("User id may not be empty"); } @@ -28,10 +28,10 @@ public sealed class CreateUserCommandValidation : AbstractValidator cmd.Email) .EmailAddress() - .WithErrorCode(DomainErrorCodes.UserInvalidEmail) + .WithErrorCode(DomainErrorCodes.User.UserInvalidEmail) .WithMessage("Email is not a valid email address") .MaximumLength(MaxLengths.User.Email) - .WithErrorCode(DomainErrorCodes.UserEmailExceedsMaxLength) + .WithErrorCode(DomainErrorCodes.User.UserEmailExceedsMaxLength) .WithMessage("Email may not be longer than 320 characters"); } @@ -39,10 +39,10 @@ public sealed class CreateUserCommandValidation : AbstractValidator cmd.FirstName) .NotEmpty() - .WithErrorCode(DomainErrorCodes.UserEmptyFirstName) + .WithErrorCode(DomainErrorCodes.User.UserEmptyFirstName) .WithMessage("FirstName may not be empty") .MaximumLength(MaxLengths.User.FirstName) - .WithErrorCode(DomainErrorCodes.UserFirstNameExceedsMaxLength) + .WithErrorCode(DomainErrorCodes.User.UserFirstNameExceedsMaxLength) .WithMessage("FirstName may not be longer than 100 characters"); } @@ -50,10 +50,10 @@ public sealed class CreateUserCommandValidation : AbstractValidator cmd.LastName) .NotEmpty() - .WithErrorCode(DomainErrorCodes.UserEmptyLastName) + .WithErrorCode(DomainErrorCodes.User.UserEmptyLastName) .WithMessage("LastName may not be empty") .MaximumLength(MaxLengths.User.LastName) - .WithErrorCode(DomainErrorCodes.UserLastNameExceedsMaxLength) + .WithErrorCode(DomainErrorCodes.User.UserLastNameExceedsMaxLength) .WithMessage("LastName may not be longer than 100 characters"); } diff --git a/CleanArchitecture.Domain/Commands/Users/DeleteUser/DeleteUserCommand.cs b/CleanArchitecture.Domain/Commands/Users/DeleteUser/DeleteUserCommand.cs index 48c42f5..2aa4910 100644 --- a/CleanArchitecture.Domain/Commands/Users/DeleteUser/DeleteUserCommand.cs +++ b/CleanArchitecture.Domain/Commands/Users/DeleteUser/DeleteUserCommand.cs @@ -4,7 +4,7 @@ namespace CleanArchitecture.Domain.Commands.Users.DeleteUser; public sealed class DeleteUserCommand : CommandBase { - private readonly DeleteUserCommandValidation _validation = new(); + private static readonly DeleteUserCommandValidation s_validation = new(); public DeleteUserCommand(Guid userId) : base(userId) { @@ -15,7 +15,7 @@ public sealed class DeleteUserCommand : CommandBase public override bool IsValid() { - ValidationResult = _validation.Validate(this); + ValidationResult = s_validation.Validate(this); return ValidationResult.IsValid; } } \ 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 1fc4970..e7d69b5 100644 --- a/CleanArchitecture.Domain/Commands/Users/DeleteUser/DeleteUserCommandHandler.cs +++ b/CleanArchitecture.Domain/Commands/Users/DeleteUser/DeleteUserCommandHandler.cs @@ -36,12 +36,12 @@ public sealed class DeleteUserCommandHandler : CommandHandlerBase, var user = await _userRepository.GetByIdAsync(request.UserId); - if (user == null) + if (user is null) { await NotifyAsync( new DomainNotification( request.MessageType, - $"There is no User with Id {request.UserId}", + $"There is no user with Id {request.UserId}", ErrorCodes.ObjectNotFound)); return; diff --git a/CleanArchitecture.Domain/Commands/Users/DeleteUser/DeleteUserCommandValidation.cs b/CleanArchitecture.Domain/Commands/Users/DeleteUser/DeleteUserCommandValidation.cs index 99dc44a..7ca78ad 100644 --- a/CleanArchitecture.Domain/Commands/Users/DeleteUser/DeleteUserCommandValidation.cs +++ b/CleanArchitecture.Domain/Commands/Users/DeleteUser/DeleteUserCommandValidation.cs @@ -14,7 +14,7 @@ public sealed class DeleteUserCommandValidation : AbstractValidator cmd.UserId) .NotEmpty() - .WithErrorCode(DomainErrorCodes.UserEmptyId) + .WithErrorCode(DomainErrorCodes.User.UserEmptyId) .WithMessage("User id may not be empty"); } } \ No newline at end of file diff --git a/CleanArchitecture.Domain/Commands/Users/LoginUser/LoginUserCommand.cs b/CleanArchitecture.Domain/Commands/Users/LoginUser/LoginUserCommand.cs index 2fcdc00..d5bc6e6 100644 --- a/CleanArchitecture.Domain/Commands/Users/LoginUser/LoginUserCommand.cs +++ b/CleanArchitecture.Domain/Commands/Users/LoginUser/LoginUserCommand.cs @@ -6,7 +6,7 @@ namespace CleanArchitecture.Domain.Commands.Users.LoginUser; public sealed class LoginUserCommand : CommandBase, IRequest { - private readonly LoginUserCommandValidation _validation = new(); + private static readonly LoginUserCommandValidation s_validation = new(); public LoginUserCommand( @@ -22,7 +22,7 @@ public sealed class LoginUserCommand : CommandBase, public override bool IsValid() { - ValidationResult = _validation.Validate(this); + ValidationResult = s_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 index dee2b53..aee5fe6 100644 --- a/CleanArchitecture.Domain/Commands/Users/LoginUser/LoginUserCommandHandler.cs +++ b/CleanArchitecture.Domain/Commands/Users/LoginUser/LoginUserCommandHandler.cs @@ -45,12 +45,12 @@ public sealed class LoginUserCommandHandler : CommandHandlerBase, var user = await _userRepository.GetByEmailAsync(request.Email); - if (user == null) + if (user is null) { await NotifyAsync( new DomainNotification( request.MessageType, - $"There is no User with Email {request.Email}", + $"There is no user with email {request.Email}", ErrorCodes.ObjectNotFound)); return ""; @@ -64,7 +64,7 @@ public sealed class LoginUserCommandHandler : CommandHandlerBase, new DomainNotification( request.MessageType, "The password is incorrect", - DomainErrorCodes.UserPasswordIncorrect)); + DomainErrorCodes.User.UserPasswordIncorrect)); return ""; } diff --git a/CleanArchitecture.Domain/Commands/Users/LoginUser/LoginUserCommandValidation.cs b/CleanArchitecture.Domain/Commands/Users/LoginUser/LoginUserCommandValidation.cs index 3be735b..8148946 100644 --- a/CleanArchitecture.Domain/Commands/Users/LoginUser/LoginUserCommandValidation.cs +++ b/CleanArchitecture.Domain/Commands/Users/LoginUser/LoginUserCommandValidation.cs @@ -17,10 +17,10 @@ public sealed class LoginUserCommandValidation : AbstractValidator cmd.Email) .EmailAddress() - .WithErrorCode(DomainErrorCodes.UserInvalidEmail) + .WithErrorCode(DomainErrorCodes.User.UserInvalidEmail) .WithMessage("Email is not a valid email address") .MaximumLength(MaxLengths.User.Email) - .WithErrorCode(DomainErrorCodes.UserEmailExceedsMaxLength) + .WithErrorCode(DomainErrorCodes.User.UserEmailExceedsMaxLength) .WithMessage("Email may not be longer than 320 characters"); } diff --git a/CleanArchitecture.Domain/Commands/Users/UpdateUser/UpdateUserCommand.cs b/CleanArchitecture.Domain/Commands/Users/UpdateUser/UpdateUserCommand.cs index 13bc3d9..d91434d 100644 --- a/CleanArchitecture.Domain/Commands/Users/UpdateUser/UpdateUserCommand.cs +++ b/CleanArchitecture.Domain/Commands/Users/UpdateUser/UpdateUserCommand.cs @@ -5,7 +5,7 @@ namespace CleanArchitecture.Domain.Commands.Users.UpdateUser; public sealed class UpdateUserCommand : CommandBase { - private readonly UpdateUserCommandValidation _validation = new(); + private static readonly UpdateUserCommandValidation s_validation = new(); public UpdateUserCommand( Guid userId, @@ -29,7 +29,7 @@ public sealed class UpdateUserCommand : CommandBase public override bool IsValid() { - ValidationResult = _validation.Validate(this); + ValidationResult = s_validation.Validate(this); return ValidationResult.IsValid; } } \ 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 cf0227e..93c7e1a 100644 --- a/CleanArchitecture.Domain/Commands/Users/UpdateUser/UpdateUserCommandHandler.cs +++ b/CleanArchitecture.Domain/Commands/Users/UpdateUser/UpdateUserCommandHandler.cs @@ -36,12 +36,12 @@ public sealed class UpdateUserCommandHandler : CommandHandlerBase, var user = await _userRepository.GetByIdAsync(request.UserId); - if (user == null) + if (user is null) { await Bus.RaiseEventAsync( new DomainNotification( request.MessageType, - $"There is no User with Id {request.UserId}", + $"There is no user with Id {request.UserId}", ErrorCodes.ObjectNotFound)); return; } @@ -61,13 +61,13 @@ public sealed class UpdateUserCommandHandler : CommandHandlerBase, { var existingUser = await _userRepository.GetByEmailAsync(request.Email); - if (existingUser != null) + if (existingUser is not null) { await Bus.RaiseEventAsync( new DomainNotification( request.MessageType, - $"There is already a User with Email {request.Email}", - DomainErrorCodes.UserAlreadyExists)); + $"There is already a user with email {request.Email}", + DomainErrorCodes.User.UserAlreadyExists)); return; } } diff --git a/CleanArchitecture.Domain/Commands/Users/UpdateUser/UpdateUserCommandValidation.cs b/CleanArchitecture.Domain/Commands/Users/UpdateUser/UpdateUserCommandValidation.cs index 17abee3..dbbb4f4 100644 --- a/CleanArchitecture.Domain/Commands/Users/UpdateUser/UpdateUserCommandValidation.cs +++ b/CleanArchitecture.Domain/Commands/Users/UpdateUser/UpdateUserCommandValidation.cs @@ -19,7 +19,7 @@ public sealed class UpdateUserCommandValidation : AbstractValidator cmd.UserId) .NotEmpty() - .WithErrorCode(DomainErrorCodes.UserEmptyId) + .WithErrorCode(DomainErrorCodes.User.UserEmptyId) .WithMessage("User id may not be empty"); } @@ -27,10 +27,10 @@ public sealed class UpdateUserCommandValidation : AbstractValidator cmd.Email) .EmailAddress() - .WithErrorCode(DomainErrorCodes.UserInvalidEmail) + .WithErrorCode(DomainErrorCodes.User.UserInvalidEmail) .WithMessage("Email is not a valid email address") .MaximumLength(MaxLengths.User.Email) - .WithErrorCode(DomainErrorCodes.UserEmailExceedsMaxLength) + .WithErrorCode(DomainErrorCodes.User.UserEmailExceedsMaxLength) .WithMessage("Email may not be longer than 320 characters"); } @@ -38,10 +38,10 @@ public sealed class UpdateUserCommandValidation : AbstractValidator cmd.FirstName) .NotEmpty() - .WithErrorCode(DomainErrorCodes.UserEmptyFirstName) + .WithErrorCode(DomainErrorCodes.User.UserEmptyFirstName) .WithMessage("FirstName may not be empty") .MaximumLength(MaxLengths.User.FirstName) - .WithErrorCode(DomainErrorCodes.UserFirstNameExceedsMaxLength) + .WithErrorCode(DomainErrorCodes.User.UserFirstNameExceedsMaxLength) .WithMessage("FirstName may not be longer than 100 characters"); } @@ -49,10 +49,10 @@ public sealed class UpdateUserCommandValidation : AbstractValidator cmd.LastName) .NotEmpty() - .WithErrorCode(DomainErrorCodes.UserEmptyLastName) + .WithErrorCode(DomainErrorCodes.User.UserEmptyLastName) .WithMessage("LastName may not be empty") .MaximumLength(MaxLengths.User.LastName) - .WithErrorCode(DomainErrorCodes.UserLastNameExceedsMaxLength) + .WithErrorCode(DomainErrorCodes.User.UserLastNameExceedsMaxLength) .WithMessage("LastName may not be longer than 100 characters"); } @@ -60,7 +60,7 @@ public sealed class UpdateUserCommandValidation : AbstractValidator cmd.Role) .IsInEnum() - .WithErrorCode(DomainErrorCodes.UserInvalidRole) + .WithErrorCode(DomainErrorCodes.User.UserInvalidRole) .WithMessage("Role is not a valid role"); } } \ No newline at end of file diff --git a/CleanArchitecture.Domain/Entities/Tenant.cs b/CleanArchitecture.Domain/Entities/Tenant.cs index 555b095..39cefb6 100644 --- a/CleanArchitecture.Domain/Entities/Tenant.cs +++ b/CleanArchitecture.Domain/Entities/Tenant.cs @@ -15,4 +15,9 @@ public class Tenant : Entity { Name = name; } + + public void SetName(string name) + { + Name = name; + } } \ No newline at end of file diff --git a/CleanArchitecture.Domain/Errors/DomainErrorCodes.cs b/CleanArchitecture.Domain/Errors/DomainErrorCodes.cs index a6091de..d4131c8 100644 --- a/CleanArchitecture.Domain/Errors/DomainErrorCodes.cs +++ b/CleanArchitecture.Domain/Errors/DomainErrorCodes.cs @@ -2,26 +2,40 @@ namespace CleanArchitecture.Domain.Errors; public static class DomainErrorCodes { - // User Validation - public const string UserEmptyId = "USER_EMPTY_ID"; - public const string UserEmptyFirstName = "USER_EMPTY_FIRST_NAME"; - public const string UserEmptyLastName = "USER_EMPTY_LAST_NAME"; - public const string UserEmailExceedsMaxLength = "USER_EMAIL_EXCEEDS_MAX_LENGTH"; - public const string UserFirstNameExceedsMaxLength = "USER_FIRST_NAME_EXCEEDS_MAX_LENGTH"; - public const string UserLastNameExceedsMaxLength = "USER_LAST_NAME_EXCEEDS_MAX_LENGTH"; - public const string UserInvalidEmail = "USER_INVALID_EMAIL"; - public const string UserInvalidRole = "USER_INVALID_ROLE"; + public static class User + { + // User Validation + public const string UserEmptyId = "USER_EMPTY_ID"; + public const string UserEmptyFirstName = "USER_EMPTY_FIRST_NAME"; + public const string UserEmptyLastName = "USER_EMPTY_LAST_NAME"; + public const string UserEmailExceedsMaxLength = "USER_EMAIL_EXCEEDS_MAX_LENGTH"; + public const string UserFirstNameExceedsMaxLength = "USER_FIRST_NAME_EXCEEDS_MAX_LENGTH"; + public const string UserLastNameExceedsMaxLength = "USER_LAST_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"; - 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 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"; - public const string UserPasswordIncorrect = "USER_PASSWORD_INCORRECT"; + // General + public const string UserAlreadyExists = "USER_ALREADY_EXISTS"; + public const string UserPasswordIncorrect = "USER_PASSWORD_INCORRECT"; + } + + public static class Tenant + { + // Tenant Validation + public const string TenantEmptyId = "TENANT_EMPTY_ID"; + public const string TenantEmptyName = "TENANT_EMPTY_NAME"; + public const string TenantNameExceedsMaxLength = "TENANT_NAME_EXCEEDS_MAX_LENGTH"; + + // General + public const string TenantAlreadyExists = "TENANT_ALREADY_EXISTS"; + } } \ No newline at end of file diff --git a/CleanArchitecture.Domain/EventHandler/TenantEventHandler.cs b/CleanArchitecture.Domain/EventHandler/TenantEventHandler.cs new file mode 100644 index 0000000..ed46c1a --- /dev/null +++ b/CleanArchitecture.Domain/EventHandler/TenantEventHandler.cs @@ -0,0 +1,27 @@ +using System.Threading; +using System.Threading.Tasks; +using CleanArchitecture.Domain.Events.Tenant; +using MediatR; + +namespace CleanArchitecture.Domain.EventHandler; + +public sealed class TenantEventHandler : + INotificationHandler, + INotificationHandler, + INotificationHandler +{ + public Task Handle(TenantCreatedEvent notification, CancellationToken cancellationToken) + { + return Task.CompletedTask; + } + + public Task Handle(TenantDeletedEvent notification, CancellationToken cancellationToken) + { + return Task.CompletedTask; + } + + public Task Handle(TenantUpdatedEvent notification, CancellationToken cancellationToken) + { + return Task.CompletedTask; + } +} \ No newline at end of file diff --git a/CleanArchitecture.Domain/Events/Tenant/TenantCreatedEvent.cs b/CleanArchitecture.Domain/Events/Tenant/TenantCreatedEvent.cs new file mode 100644 index 0000000..b79f34e --- /dev/null +++ b/CleanArchitecture.Domain/Events/Tenant/TenantCreatedEvent.cs @@ -0,0 +1,14 @@ +using System; +using CleanArchitecture.Domain.DomainEvents; + +namespace CleanArchitecture.Domain.Events.Tenant; + +public sealed class TenantCreatedEvent : DomainEvent +{ + public string Name { get; set; } + + public TenantCreatedEvent(Guid tenantId, string name) : base(tenantId) + { + Name = name; + } +} \ No newline at end of file diff --git a/CleanArchitecture.Domain/Events/Tenant/TenantDeletedEvent.cs b/CleanArchitecture.Domain/Events/Tenant/TenantDeletedEvent.cs new file mode 100644 index 0000000..e96bdfd --- /dev/null +++ b/CleanArchitecture.Domain/Events/Tenant/TenantDeletedEvent.cs @@ -0,0 +1,11 @@ +using System; +using CleanArchitecture.Domain.DomainEvents; + +namespace CleanArchitecture.Domain.Events.Tenant; + +public sealed class TenantDeletedEvent : DomainEvent +{ + public TenantDeletedEvent(Guid tenantId) : base(tenantId) + { + } +} \ No newline at end of file diff --git a/CleanArchitecture.Domain/Events/Tenant/TenantUpdatedEvent.cs b/CleanArchitecture.Domain/Events/Tenant/TenantUpdatedEvent.cs new file mode 100644 index 0000000..00b66b3 --- /dev/null +++ b/CleanArchitecture.Domain/Events/Tenant/TenantUpdatedEvent.cs @@ -0,0 +1,14 @@ +using System; +using CleanArchitecture.Domain.DomainEvents; + +namespace CleanArchitecture.Domain.Events.Tenant; + +public sealed class TenantUpdatedEvent : DomainEvent +{ + public string Name { get; set; } + + public TenantUpdatedEvent(Guid tenantId, string name) : base(tenantId) + { + Name = name; + } +} \ No newline at end of file diff --git a/CleanArchitecture.Domain/Events/User/PasswordChangedEvent.cs b/CleanArchitecture.Domain/Events/User/PasswordChangedEvent.cs index e3bf3f1..c5b1b25 100644 --- a/CleanArchitecture.Domain/Events/User/PasswordChangedEvent.cs +++ b/CleanArchitecture.Domain/Events/User/PasswordChangedEvent.cs @@ -7,8 +7,5 @@ public sealed class PasswordChangedEvent : DomainEvent { 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 f21e681..95d11bf 100644 --- a/CleanArchitecture.Domain/Events/User/UserCreatedEvent.cs +++ b/CleanArchitecture.Domain/Events/User/UserCreatedEvent.cs @@ -7,8 +7,5 @@ public sealed class UserCreatedEvent : DomainEvent { 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 5245879..8b485f5 100644 --- a/CleanArchitecture.Domain/Events/User/UserDeletedEvent.cs +++ b/CleanArchitecture.Domain/Events/User/UserDeletedEvent.cs @@ -7,8 +7,5 @@ public sealed class UserDeletedEvent : DomainEvent { 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 d78cd72..7056b95 100644 --- a/CleanArchitecture.Domain/Events/User/UserUpdatedEvent.cs +++ b/CleanArchitecture.Domain/Events/User/UserUpdatedEvent.cs @@ -7,8 +7,5 @@ public sealed class UserUpdatedEvent : DomainEvent { 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 4c3b2b9..a82ac0a 100644 --- a/CleanArchitecture.Domain/Extensions/ServiceCollectionExtension.cs +++ b/CleanArchitecture.Domain/Extensions/ServiceCollectionExtension.cs @@ -1,9 +1,13 @@ +using CleanArchitecture.Domain.Commands.Tenants.CreateTenant; +using CleanArchitecture.Domain.Commands.Tenants.DeleteTenant; +using CleanArchitecture.Domain.Commands.Tenants.UpdateTenant; 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.Tenant; using CleanArchitecture.Domain.Events.User; using CleanArchitecture.Domain.Interfaces; using MediatR; @@ -22,6 +26,10 @@ public static class ServiceCollectionExtension services.AddScoped, ChangePasswordCommandHandler>(); services.AddScoped, LoginUserCommandHandler>(); + // Tenant + services.AddScoped, CreateTenantCommandHandler>(); + services.AddScoped, UpdateTenantCommandHandler>(); + services.AddScoped, DeleteTenantCommandHandler>(); return services; } @@ -33,6 +41,11 @@ public static class ServiceCollectionExtension services.AddScoped, UserEventHandler>(); services.AddScoped, UserEventHandler>(); services.AddScoped, UserEventHandler>(); + + // Tenant + services.AddScoped, TenantEventHandler>(); + services.AddScoped, TenantEventHandler>(); + services.AddScoped, TenantEventHandler>(); return services; } diff --git a/CleanArchitecture.Domain/Extensions/Validation/CustomValidator.cs b/CleanArchitecture.Domain/Extensions/Validation/CustomValidator.cs index 5157f16..dea14c7 100644 --- a/CleanArchitecture.Domain/Extensions/Validation/CustomValidator.cs +++ b/CleanArchitecture.Domain/Extensions/Validation/CustomValidator.cs @@ -23,13 +23,13 @@ public static partial class CustomValidator 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); + .NotEmpty().WithErrorCode(DomainErrorCodes.User.UserEmptyPassword) + .MinimumLength(minLength).WithErrorCode(DomainErrorCodes.User.UserShortPassword) + .MaximumLength(maxLength).WithErrorCode(DomainErrorCodes.User.UserLongPassword) + .Matches("[A-Z]").WithErrorCode(DomainErrorCodes.User.UserUppercaseLetterPassword) + .Matches("[a-z]").WithErrorCode(DomainErrorCodes.User.UserLowercaseLetterPassword) + .Matches("[0-9]").WithErrorCode(DomainErrorCodes.User.UserNumberPassword) + .Matches("[^a-zA-Z0-9]").WithErrorCode(DomainErrorCodes.User.UserSpecialCharPassword); return options; } diff --git a/CleanArchitecture.Domain/Interfaces/Repositories/IRepository.cs b/CleanArchitecture.Domain/Interfaces/Repositories/IRepository.cs index 7c162bd..5d90b49 100644 --- a/CleanArchitecture.Domain/Interfaces/Repositories/IRepository.cs +++ b/CleanArchitecture.Domain/Interfaces/Repositories/IRepository.cs @@ -22,4 +22,5 @@ public interface IRepository : IDisposable where TEntity : Entity Task ExistsAsync(Guid id); public void Remove(TEntity entity, bool hardDelete = false); + void RemoveRange(IEnumerable entities, bool hardDelete = false); } \ No newline at end of file diff --git a/CleanArchitecture.Domain/Interfaces/Repositories/ITenantRepository.cs b/CleanArchitecture.Domain/Interfaces/Repositories/ITenantRepository.cs new file mode 100644 index 0000000..d2e0464 --- /dev/null +++ b/CleanArchitecture.Domain/Interfaces/Repositories/ITenantRepository.cs @@ -0,0 +1,7 @@ +using CleanArchitecture.Domain.Entities; + +namespace CleanArchitecture.Domain.Interfaces.Repositories; + +public interface ITenantRepository : IRepository +{ +} \ No newline at end of file diff --git a/CleanArchitecture.Infrastructure/EventSourcing/EventStoreContext.cs b/CleanArchitecture.Infrastructure/EventSourcing/EventStoreContext.cs index 0edaffa..b28104b 100644 --- a/CleanArchitecture.Infrastructure/EventSourcing/EventStoreContext.cs +++ b/CleanArchitecture.Infrastructure/EventSourcing/EventStoreContext.cs @@ -13,7 +13,7 @@ public sealed class EventStoreContext : IEventStoreContext { _user = user; - if (httpContextAccessor?.HttpContext == null || + if (httpContextAccessor?.HttpContext is null || !httpContextAccessor.HttpContext.Request.Headers.TryGetValue("X-CLEAN-ARCHITECTURE-CORRELATION-ID", out var id)) { _correlationId = $"internal - {Guid.NewGuid()}"; diff --git a/CleanArchitecture.Infrastructure/Extensions/ServiceCollectionExtensions.cs b/CleanArchitecture.Infrastructure/Extensions/ServiceCollectionExtensions.cs index 02d063f..aec00dd 100644 --- a/CleanArchitecture.Infrastructure/Extensions/ServiceCollectionExtensions.cs +++ b/CleanArchitecture.Infrastructure/Extensions/ServiceCollectionExtensions.cs @@ -46,6 +46,7 @@ public static class ServiceCollectionExtensions // Repositories services.AddScoped(); + services.AddScoped(); return services; } diff --git a/CleanArchitecture.Infrastructure/Repositories/BaseRepository.cs b/CleanArchitecture.Infrastructure/Repositories/BaseRepository.cs index db13d69..9137422 100644 --- a/CleanArchitecture.Infrastructure/Repositories/BaseRepository.cs +++ b/CleanArchitecture.Infrastructure/Repositories/BaseRepository.cs @@ -55,9 +55,9 @@ public class BaseRepository : IRepository where TEntity : Enti DbSet.Update(entity); } - public Task ExistsAsync(Guid id) + public async Task ExistsAsync(Guid id) { - return DbSet.AnyAsync(entity => entity.Id == id); + return await DbSet.AnyAsync(entity => entity.Id == id); } public void Remove(TEntity entity, bool hardDelete = false) @@ -72,6 +72,20 @@ public class BaseRepository : IRepository where TEntity : Enti DbSet.Update(entity); } } + + public void RemoveRange(IEnumerable entities, bool hardDelete = false) + { + if (hardDelete) + { + DbSet.RemoveRange(entities); + return; + } + + foreach (var entity in entities) + { + entity.Delete(); + } + } public int SaveChanges() { diff --git a/CleanArchitecture.Infrastructure/Repositories/TenantRepository.cs b/CleanArchitecture.Infrastructure/Repositories/TenantRepository.cs new file mode 100644 index 0000000..f86518b --- /dev/null +++ b/CleanArchitecture.Infrastructure/Repositories/TenantRepository.cs @@ -0,0 +1,12 @@ +using CleanArchitecture.Domain.Entities; +using CleanArchitecture.Domain.Interfaces.Repositories; +using CleanArchitecture.Infrastructure.Database; + +namespace CleanArchitecture.Infrastructure.Repositories; + +public sealed class TenantRepository : BaseRepository, ITenantRepository +{ + public TenantRepository(ApplicationDbContext context) : base(context) + { + } +} \ No newline at end of file diff --git a/CleanArchitecture.IntegrationTests/Extensions/FunctionalTestsServiceCollectionExtensions.cs b/CleanArchitecture.IntegrationTests/Extensions/FunctionalTestsServiceCollectionExtensions.cs index d81e2c0..f3c9b08 100644 --- a/CleanArchitecture.IntegrationTests/Extensions/FunctionalTestsServiceCollectionExtensions.cs +++ b/CleanArchitecture.IntegrationTests/Extensions/FunctionalTestsServiceCollectionExtensions.cs @@ -15,7 +15,7 @@ public static class FunctionalTestsServiceCollectionExtensions DbConnection connection) where TContext : DbContext { var descriptor = services.SingleOrDefault(d => d.ServiceType == typeof(DbContextOptions)); - if (descriptor != null) + if (descriptor is not null) services.Remove(descriptor); services.AddScoped(p => diff --git a/CleanArchitecture.IntegrationTests/gRPC/GetUsersByIdsTests.cs b/CleanArchitecture.IntegrationTests/gRPC/GetUsersByIdsTests.cs index 0ad9f75..8464d7d 100644 --- a/CleanArchitecture.IntegrationTests/gRPC/GetUsersByIdsTests.cs +++ b/CleanArchitecture.IntegrationTests/gRPC/GetUsersByIdsTests.cs @@ -21,7 +21,7 @@ public sealed class GetUsersByIdsTests : IClassFixture { var client = new UsersApi.UsersApiClient(_fixture.GrpcChannel); - var request = new GetByIdsRequest(); + var request = new GetUsersByIdsRequest(); request.Ids.Add(_fixture.CreatedUserId.ToString()); var response = await client.GetByIdsAsync(request); diff --git a/CleanArchitecture.Proto/CleanArchitecture.Proto.csproj b/CleanArchitecture.Proto/CleanArchitecture.Proto.csproj index df3c40c..1cef0c5 100644 --- a/CleanArchitecture.Proto/CleanArchitecture.Proto.csproj +++ b/CleanArchitecture.Proto/CleanArchitecture.Proto.csproj @@ -5,14 +5,11 @@ enable - - - - - + + diff --git a/CleanArchitecture.Proto/Tenants/Models.proto b/CleanArchitecture.Proto/Tenants/Models.proto new file mode 100644 index 0000000..c98c121 --- /dev/null +++ b/CleanArchitecture.Proto/Tenants/Models.proto @@ -0,0 +1,17 @@ +syntax = "proto3"; + +option csharp_namespace = "CleanArchitecture.Proto.Tenants"; + +message Tenant { + string id = 1; + string name = 2; + bool isDeleted = 3; +} + +message GetTenantsByIdsResult { + repeated Tenant tenants = 1; +} + +message GetTenantsByIdsRequest { + repeated string ids = 1; +} \ No newline at end of file diff --git a/CleanArchitecture.Proto/Tenants/TenantsApi.proto b/CleanArchitecture.Proto/Tenants/TenantsApi.proto new file mode 100644 index 0000000..776d2d7 --- /dev/null +++ b/CleanArchitecture.Proto/Tenants/TenantsApi.proto @@ -0,0 +1,9 @@ +syntax = "proto3"; + +option csharp_namespace = "CleanArchitecture.Proto.Tenants"; + +import "Tenants/Models.proto"; + +service TenantsApi { + rpc GetByIds(GetTenantsByIdsRequest) returns (GetTenantsByIdsResult); +} diff --git a/CleanArchitecture.Proto/Users/Models.proto b/CleanArchitecture.Proto/Users/Models.proto index 03e59a7..c372420 100644 --- a/CleanArchitecture.Proto/Users/Models.proto +++ b/CleanArchitecture.Proto/Users/Models.proto @@ -10,10 +10,10 @@ message GrpcUser { bool isDeleted = 6; } -message GetByIdsResult { +message GetUsersByIdsResult { repeated GrpcUser users = 1; } -message GetByIdsRequest { +message GetUsersByIdsRequest { repeated string ids = 1; } \ No newline at end of file diff --git a/CleanArchitecture.Proto/Users/UsersApi.proto b/CleanArchitecture.Proto/Users/UsersApi.proto index e718218..069393e 100644 --- a/CleanArchitecture.Proto/Users/UsersApi.proto +++ b/CleanArchitecture.Proto/Users/UsersApi.proto @@ -5,5 +5,5 @@ option csharp_namespace = "CleanArchitecture.Proto.Users"; import "Users/Models.proto"; service UsersApi { - rpc GetByIds(GetByIdsRequest) returns (GetByIdsResult); + rpc GetByIds(GetUsersByIdsRequest) returns (GetUsersByIdsResult); } diff --git a/CleanArchitecture.Shared/CleanArchitecture.Shared.csproj b/CleanArchitecture.Shared/CleanArchitecture.Shared.csproj index de3c61c..882f5b9 100644 --- a/CleanArchitecture.Shared/CleanArchitecture.Shared.csproj +++ b/CleanArchitecture.Shared/CleanArchitecture.Shared.csproj @@ -5,4 +5,5 @@ enable + diff --git a/CleanArchitecture.Shared/Tenants/TenantViewModel.cs b/CleanArchitecture.Shared/Tenants/TenantViewModel.cs new file mode 100644 index 0000000..97cd79f --- /dev/null +++ b/CleanArchitecture.Shared/Tenants/TenantViewModel.cs @@ -0,0 +1,7 @@ +using System; + +namespace CleanArchitecture.Shared.Tenants; + +public sealed record TenantViewModel( + Guid Id, + string Name); \ No newline at end of file diff --git a/CleanArchitecture.gRPC.Tests/Users/GetUsersByIdsTests.cs b/CleanArchitecture.gRPC.Tests/Users/GetUsersByIdsTests.cs index 7022833..1c0e333 100644 --- a/CleanArchitecture.gRPC.Tests/Users/GetUsersByIdsTests.cs +++ b/CleanArchitecture.gRPC.Tests/Users/GetUsersByIdsTests.cs @@ -62,9 +62,9 @@ public sealed class GetUsersByIdsTests : IClassFixture } } - private static GetByIdsRequest SetupRequest(IEnumerable ids) + private static GetUsersByIdsRequest SetupRequest(IEnumerable ids) { - var request = new GetByIdsRequest(); + var request = new GetUsersByIdsRequest(); request.Ids.AddRange(ids.Select(id => id.ToString())); request.Ids.Add("Not a guid"); diff --git a/CleanArchitecture.gRPC/CleanArchitecture.cs b/CleanArchitecture.gRPC/CleanArchitecture.cs index 649b2fc..8aa7853 100644 --- a/CleanArchitecture.gRPC/CleanArchitecture.cs +++ b/CleanArchitecture.gRPC/CleanArchitecture.cs @@ -5,11 +5,17 @@ namespace CleanArchitecture.gRPC; public sealed class CleanArchitecture : ICleanArchitecture { private readonly IUsersContext _users; + private readonly ITenantsContext _tenants; public IUsersContext Users => _users; + public ITenantsContext Tenants => _tenants; - public CleanArchitecture(IUsersContext users) + public CleanArchitecture( + IUsersContext users, + ITenantsContext tenants) { _users = users; + _tenants = tenants; + } } diff --git a/CleanArchitecture.gRPC/Contexts/TenantsContext.cs b/CleanArchitecture.gRPC/Contexts/TenantsContext.cs new file mode 100644 index 0000000..a8235ad --- /dev/null +++ b/CleanArchitecture.gRPC/Contexts/TenantsContext.cs @@ -0,0 +1,32 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using CleanArchitecture.gRPC.Interfaces; +using CleanArchitecture.Proto.Tenants; +using CleanArchitecture.Shared.Tenants; + +namespace CleanArchitecture.gRPC.Contexts; + +public sealed class TenantsContext : ITenantsContext +{ + private readonly TenantsApi.TenantsApiClient _client; + + public TenantsContext(TenantsApi.TenantsApiClient client) + { + _client = client; + } + + public async Task> GetTenantsByIds(IEnumerable ids) + { + var request = new GetTenantsByIdsRequest(); + + request.Ids.AddRange(ids.Select(id => id.ToString())); + + var result = await _client.GetByIdsAsync(request); + + return result.Tenants.Select(tenant => new TenantViewModel( + Guid.Parse(tenant.Id), + tenant.Name)); + } +} \ No newline at end of file diff --git a/CleanArchitecture.gRPC/Contexts/UsersContext.cs b/CleanArchitecture.gRPC/Contexts/UsersContext.cs index 4822231..a00cbb7 100644 --- a/CleanArchitecture.gRPC/Contexts/UsersContext.cs +++ b/CleanArchitecture.gRPC/Contexts/UsersContext.cs @@ -19,7 +19,7 @@ public sealed class UsersContext : IUsersContext public async Task> GetUsersByIds(IEnumerable ids) { - var request = new GetByIdsRequest(); + var request = new GetUsersByIdsRequest(); request.Ids.AddRange(ids.Select(id => id.ToString())); diff --git a/CleanArchitecture.gRPC/Extensions/ServiceCollectionExtensions.cs b/CleanArchitecture.gRPC/Extensions/ServiceCollectionExtensions.cs index 72d5c3c..89a9084 100644 --- a/CleanArchitecture.gRPC/Extensions/ServiceCollectionExtensions.cs +++ b/CleanArchitecture.gRPC/Extensions/ServiceCollectionExtensions.cs @@ -1,6 +1,7 @@ using CleanArchitecture.gRPC.Contexts; using CleanArchitecture.gRPC.Interfaces; using CleanArchitecture.gRPC.Models; +using CleanArchitecture.Proto.Tenants; using CleanArchitecture.Proto.Users; using Grpc.Net.Client; using Microsoft.Extensions.Configuration; @@ -35,20 +36,24 @@ public static class ServiceCollectionExtensions public static IServiceCollection AddCleanArchitectureGrpcClient( this IServiceCollection services, - string tetraQueryApiUrl) + string gRPCUrl) { - if (string.IsNullOrWhiteSpace(tetraQueryApiUrl)) + if (string.IsNullOrWhiteSpace(gRPCUrl)) { return services; } - var channel = GrpcChannel.ForAddress(tetraQueryApiUrl); + var channel = GrpcChannel.ForAddress(gRPCUrl); var usersClient = new UsersApi.UsersApiClient(channel); services.AddSingleton(usersClient); - services.AddSingleton(); + var tenantsClient = new TenantsApi.TenantsApiClient(channel); + services.AddSingleton(tenantsClient); + services.AddSingleton(); + services.AddSingleton(); + return services; } } diff --git a/CleanArchitecture.gRPC/ICleanArchitecture.cs b/CleanArchitecture.gRPC/ICleanArchitecture.cs index 168df0d..9e04af9 100644 --- a/CleanArchitecture.gRPC/ICleanArchitecture.cs +++ b/CleanArchitecture.gRPC/ICleanArchitecture.cs @@ -5,4 +5,5 @@ namespace CleanArchitecture.gRPC; public interface ICleanArchitecture { IUsersContext Users { get; } + ITenantsContext Tenants { get; } } diff --git a/CleanArchitecture.gRPC/Interfaces/ITenantsContext.cs b/CleanArchitecture.gRPC/Interfaces/ITenantsContext.cs new file mode 100644 index 0000000..db07813 --- /dev/null +++ b/CleanArchitecture.gRPC/Interfaces/ITenantsContext.cs @@ -0,0 +1,11 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using CleanArchitecture.Shared.Tenants; + +namespace CleanArchitecture.gRPC.Interfaces; + +public interface ITenantsContext +{ + Task> GetTenantsByIds(IEnumerable ids); +} \ No newline at end of file From a3152580a26df4e2fcd3c9917ffba242ffdf5df7 Mon Sep 17 00:00:00 2001 From: alex289 Date: Wed, 30 Aug 2023 23:31:47 +0200 Subject: [PATCH 05/10] test: Add tenant tests --- CleanArchitecture.Api/Program.cs | 1 + .../Tenants/GetAllTenantsTestFixture.cs | 37 +++++ .../Tenants/GetTenantByIdTestFixture.cs | 39 +++++ .../Tenants/GetAllTenantsQueryHandlerTests.cs | 39 +++++ .../Tenants/GetTenantByIdQueryHandlerTests.cs | 57 ++++++++ .../GetAll/GetAllTenantsQueryHandler.cs | 1 + .../GetTenantByIdQueryHandler.cs | 2 + .../Services/UserService.cs | 3 +- .../gRPC/TenantsApiImplementation.cs | 52 +++++++ .../viewmodels/Users/UpdateUserViewModel.cs | 3 +- .../CreateTenantCommandHandlerTests.cs | 69 +++++++++ .../CreateTenantCommandTestFixture.cs | 38 +++++ .../CreateTenantCommandValidationTests.cs | 53 +++++++ .../DeleteTenantCommandHandlerTests.cs | 45 ++++++ .../DeleteTenantCommandTestFixture.cs | 39 +++++ .../DeleteTenantCommandValidationTests.cs | 38 +++++ .../UpdateTenantCommandHandlerTests.cs | 69 +++++++++ .../UpdateTenantCommandTestFixture.cs | 39 +++++ .../UpdateTenantCommandValidationTests.cs | 53 +++++++ .../CreateUserCommandHandlerTests.cs | 60 +++++++- .../CreateUserCommandTestFixture.cs | 35 ++++- .../CreateUserCommandValidationTests.cs | 21 ++- .../LoginUserCommandValidationTests.cs | 5 +- .../UpdateUserCommandHandlerTests.cs | 9 +- .../UpdateUserCommandValidationTests.cs | 28 +++- .../CreateTenantCommandHandler.cs | 19 ++- .../DeleteTenantCommandHandler.cs | 21 ++- .../UpdateTenantCommandHandler.cs | 19 ++- .../CreateUser/CreateUserCommandHandler.cs | 34 ++++- .../CreateUser/CreateUserCommandValidation.cs | 15 +- .../LoginUser/LoginUserCommandValidation.cs | 2 +- .../Users/UpdateUser/UpdateUserCommand.cs | 4 +- .../UpdateUser/UpdateUserCommandHandler.cs | 8 +- .../UpdateUser/UpdateUserCommandValidation.cs | 15 +- .../Constants/MaxLengths.cs | 2 +- CleanArchitecture.Domain/Entities/User.cs | 7 +- .../Errors/DomainErrorCodes.cs | 2 +- .../Repositories/BaseRepository.cs | 2 +- .../Controller/TenantControllerTests.cs | 136 ++++++++++++++++++ .../Controller/UserControllerTests.cs | 9 +- .../Fixtures/TenantTestFixture.cs | 19 +++ .../Fixtures/TestFixtureBase.cs | 18 +++ .../Fixtures/UserTestFixture.cs | 1 + .../gRPC/GetTenantsByIdsTestFixture.cs | 37 +++++ .../gRPC/GetTenantsByIdsTests.cs | 38 +++++ .../gRPC/GetUsersByIdsTests.cs | 2 +- .../Fixtures/TenantTestFixture.cs | 33 +++++ ...UserTestsFixture.cs => UserTestFixture.cs} | 4 +- .../Tenants/GetTenantsByIdsTests.cs | 72 ++++++++++ .../Users/GetUsersByIdsTests.cs | 13 +- Readme.md | 5 +- 51 files changed, 1315 insertions(+), 57 deletions(-) create mode 100644 CleanArchitecture.Application.Tests/Fixtures/Queries/Tenants/GetAllTenantsTestFixture.cs create mode 100644 CleanArchitecture.Application.Tests/Fixtures/Queries/Tenants/GetTenantByIdTestFixture.cs create mode 100644 CleanArchitecture.Application.Tests/Queries/Tenants/GetAllTenantsQueryHandlerTests.cs create mode 100644 CleanArchitecture.Application.Tests/Queries/Tenants/GetTenantByIdQueryHandlerTests.cs create mode 100644 CleanArchitecture.Application/gRPC/TenantsApiImplementation.cs create mode 100644 CleanArchitecture.Domain.Tests/CommandHandler/Tenant/CreateTenant/CreateTenantCommandHandlerTests.cs create mode 100644 CleanArchitecture.Domain.Tests/CommandHandler/Tenant/CreateTenant/CreateTenantCommandTestFixture.cs create mode 100644 CleanArchitecture.Domain.Tests/CommandHandler/Tenant/CreateTenant/CreateTenantCommandValidationTests.cs create mode 100644 CleanArchitecture.Domain.Tests/CommandHandler/Tenant/DeleteTenant/DeleteTenantCommandHandlerTests.cs create mode 100644 CleanArchitecture.Domain.Tests/CommandHandler/Tenant/DeleteTenant/DeleteTenantCommandTestFixture.cs create mode 100644 CleanArchitecture.Domain.Tests/CommandHandler/Tenant/DeleteTenant/DeleteTenantCommandValidationTests.cs create mode 100644 CleanArchitecture.Domain.Tests/CommandHandler/Tenant/UpdateTenant/UpdateTenantCommandHandlerTests.cs create mode 100644 CleanArchitecture.Domain.Tests/CommandHandler/Tenant/UpdateTenant/UpdateTenantCommandTestFixture.cs create mode 100644 CleanArchitecture.Domain.Tests/CommandHandler/Tenant/UpdateTenant/UpdateTenantCommandValidationTests.cs create mode 100644 CleanArchitecture.IntegrationTests/Controller/TenantControllerTests.cs create mode 100644 CleanArchitecture.IntegrationTests/Fixtures/TenantTestFixture.cs create mode 100644 CleanArchitecture.IntegrationTests/Fixtures/gRPC/GetTenantsByIdsTestFixture.cs create mode 100644 CleanArchitecture.IntegrationTests/gRPC/GetTenantsByIdsTests.cs create mode 100644 CleanArchitecture.gRPC.Tests/Fixtures/TenantTestFixture.cs rename CleanArchitecture.gRPC.Tests/Fixtures/{UserTestsFixture.cs => UserTestFixture.cs} (95%) create mode 100644 CleanArchitecture.gRPC.Tests/Tenants/GetTenantsByIdsTests.cs diff --git a/CleanArchitecture.Api/Program.cs b/CleanArchitecture.Api/Program.cs index da17e91..f940491 100644 --- a/CleanArchitecture.Api/Program.cs +++ b/CleanArchitecture.Api/Program.cs @@ -91,6 +91,7 @@ app.MapHealthChecks("/healthz", new HealthCheckOptions }); app.MapControllers(); app.MapGrpcService(); +app.MapGrpcService(); app.Run(); diff --git a/CleanArchitecture.Application.Tests/Fixtures/Queries/Tenants/GetAllTenantsTestFixture.cs b/CleanArchitecture.Application.Tests/Fixtures/Queries/Tenants/GetAllTenantsTestFixture.cs new file mode 100644 index 0000000..90763e6 --- /dev/null +++ b/CleanArchitecture.Application.Tests/Fixtures/Queries/Tenants/GetAllTenantsTestFixture.cs @@ -0,0 +1,37 @@ +using System; +using System.Collections.Generic; +using CleanArchitecture.Application.Queries.Tenants.GetAll; +using CleanArchitecture.Domain.Entities; +using CleanArchitecture.Domain.Interfaces.Repositories; +using MockQueryable.NSubstitute; +using NSubstitute; + +namespace CleanArchitecture.Application.Tests.Fixtures.Queries.Tenants; + +public sealed class GetAllTenantsTestFixture : QueryHandlerBaseFixture +{ + public GetAllTenantsQueryHandler QueryHandler { get; } + private ITenantRepository TenantRepository { get; } + + public GetAllTenantsTestFixture() + { + TenantRepository = Substitute.For(); + + QueryHandler = new(TenantRepository); + } + + public Tenant SetupTenant(bool deleted = false) + { + var tenant = new Tenant(Guid.NewGuid(), "Tenant 1"); + + if (deleted) + { + tenant.Delete(); + } + + var tenantList = new List { tenant }.BuildMock(); + TenantRepository.GetAllNoTracking().Returns(tenantList); + + return tenant; + } +} \ No newline at end of file diff --git a/CleanArchitecture.Application.Tests/Fixtures/Queries/Tenants/GetTenantByIdTestFixture.cs b/CleanArchitecture.Application.Tests/Fixtures/Queries/Tenants/GetTenantByIdTestFixture.cs new file mode 100644 index 0000000..4ba31ef --- /dev/null +++ b/CleanArchitecture.Application.Tests/Fixtures/Queries/Tenants/GetTenantByIdTestFixture.cs @@ -0,0 +1,39 @@ +using System; +using System.Collections.Generic; +using CleanArchitecture.Application.Queries.Tenants.GetTenantById; +using CleanArchitecture.Domain.Entities; +using CleanArchitecture.Domain.Interfaces.Repositories; +using MockQueryable.NSubstitute; +using NSubstitute; + +namespace CleanArchitecture.Application.Tests.Fixtures.Queries.Tenants; + +public sealed class GetTenantByIdTestFixture : QueryHandlerBaseFixture +{ + public GetTenantByIdQueryHandler QueryHandler { get; } + private ITenantRepository TenantRepository { get; } + + public GetTenantByIdTestFixture() + { + TenantRepository = Substitute.For(); + + QueryHandler = new( + TenantRepository, + Bus); + } + + public Tenant SetupTenant(bool deleted = false) + { + var tenant = new Tenant(Guid.NewGuid(), "Tenant 1"); + + if (deleted) + { + tenant.Delete(); + } + + var tenantList = new List { tenant }.BuildMock(); + TenantRepository.GetAllNoTracking().Returns(tenantList); + + return tenant; + } +} \ No newline at end of file diff --git a/CleanArchitecture.Application.Tests/Queries/Tenants/GetAllTenantsQueryHandlerTests.cs b/CleanArchitecture.Application.Tests/Queries/Tenants/GetAllTenantsQueryHandlerTests.cs new file mode 100644 index 0000000..882eedf --- /dev/null +++ b/CleanArchitecture.Application.Tests/Queries/Tenants/GetAllTenantsQueryHandlerTests.cs @@ -0,0 +1,39 @@ +using System.Linq; +using System.Threading.Tasks; +using CleanArchitecture.Application.Queries.Tenants.GetAll; +using CleanArchitecture.Application.Tests.Fixtures.Queries.Tenants; +using FluentAssertions; +using Xunit; + +namespace CleanArchitecture.Application.Tests.Queries.Tenants; + +public sealed class GetAllTenantsQueryHandlerTests +{ + private readonly GetAllTenantsTestFixture _fixture = new(); + + [Fact] + public async Task Should_Get_Existing_Tenant() + { + var tenant = _fixture.SetupTenant(); + + var result = await _fixture.QueryHandler.Handle( + new GetAllTenantsQuery(), + default); + + _fixture.VerifyNoDomainNotification(); + + tenant.Should().BeEquivalentTo(result.First()); + } + + [Fact] + public async Task Should_Not_Get_Deleted_Tenant() + { + _fixture.SetupTenant(true); + + var result = await _fixture.QueryHandler.Handle( + new GetAllTenantsQuery(), + default); + + result.Should().HaveCount(0); + } +} \ No newline at end of file diff --git a/CleanArchitecture.Application.Tests/Queries/Tenants/GetTenantByIdQueryHandlerTests.cs b/CleanArchitecture.Application.Tests/Queries/Tenants/GetTenantByIdQueryHandlerTests.cs new file mode 100644 index 0000000..222d456 --- /dev/null +++ b/CleanArchitecture.Application.Tests/Queries/Tenants/GetTenantByIdQueryHandlerTests.cs @@ -0,0 +1,57 @@ +using System.Threading.Tasks; +using CleanArchitecture.Application.Queries.Tenants.GetTenantById; +using CleanArchitecture.Application.Tests.Fixtures.Queries.Tenants; +using CleanArchitecture.Domain.Errors; +using FluentAssertions; +using Xunit; + +namespace CleanArchitecture.Application.Tests.Queries.Tenants; + +public sealed class GetTenantByIdQueryHandlerTests +{ + private readonly GetTenantByIdTestFixture _fixture = new(); + + [Fact] + public async Task Should_Get_Existing_Tenant() + { + var tenant = _fixture.SetupTenant(); + + var result = await _fixture.QueryHandler.Handle( + new GetTenantByIdQuery(tenant.Id, false), + default); + + _fixture.VerifyNoDomainNotification(); + + tenant.Should().BeEquivalentTo(result); + } + + [Fact] + public async Task Should_Get_Deleted_Tenant() + { + var tenant = _fixture.SetupTenant(true); + + var result = await _fixture.QueryHandler.Handle( + new GetTenantByIdQuery(tenant.Id, true), + default); + + _fixture.VerifyNoDomainNotification(); + + tenant.Should().BeEquivalentTo(result); + } + + [Fact] + public async Task Should_Not_Get_Deleted_Tenant() + { + var tenant = _fixture.SetupTenant(true); + + var result = await _fixture.QueryHandler.Handle( + new GetTenantByIdQuery(tenant.Id, false), + default); + + _fixture.VerifyExistingNotification( + nameof(GetTenantByIdQuery), + ErrorCodes.ObjectNotFound, + $"Tenant with id {tenant.Id} could not be found"); + result.Should().BeNull(); + } +} \ No newline at end of file diff --git a/CleanArchitecture.Application/Queries/Tenants/GetAll/GetAllTenantsQueryHandler.cs b/CleanArchitecture.Application/Queries/Tenants/GetAll/GetAllTenantsQueryHandler.cs index cd21b8a..844a5f4 100644 --- a/CleanArchitecture.Application/Queries/Tenants/GetAll/GetAllTenantsQueryHandler.cs +++ b/CleanArchitecture.Application/Queries/Tenants/GetAll/GetAllTenantsQueryHandler.cs @@ -25,6 +25,7 @@ public sealed class GetAllTenantsQueryHandler : { return await _tenantRepository .GetAllNoTracking() + .Include(x => x.Users) .Where(x => !x.Deleted) .Select(x => TenantViewModel.FromTenant(x)) .ToListAsync(cancellationToken); diff --git a/CleanArchitecture.Application/Queries/Tenants/GetTenantById/GetTenantByIdQueryHandler.cs b/CleanArchitecture.Application/Queries/Tenants/GetTenantById/GetTenantByIdQueryHandler.cs index 751ab59..9157fe4 100644 --- a/CleanArchitecture.Application/Queries/Tenants/GetTenantById/GetTenantByIdQueryHandler.cs +++ b/CleanArchitecture.Application/Queries/Tenants/GetTenantById/GetTenantByIdQueryHandler.cs @@ -7,6 +7,7 @@ using CleanArchitecture.Domain.Interfaces; using CleanArchitecture.Domain.Interfaces.Repositories; using CleanArchitecture.Domain.Notifications; using MediatR; +using Microsoft.EntityFrameworkCore; namespace CleanArchitecture.Application.Queries.Tenants.GetTenantById; @@ -26,6 +27,7 @@ public sealed class GetTenantByIdQueryHandler : { var tenant = _tenantRepository .GetAllNoTracking() + .Include(x => x.Users) .FirstOrDefault(x => x.Id == request.TenantId && x.Deleted == request.IsDeleted); diff --git a/CleanArchitecture.Application/Services/UserService.cs b/CleanArchitecture.Application/Services/UserService.cs index 13920c7..f6181f7 100644 --- a/CleanArchitecture.Application/Services/UserService.cs +++ b/CleanArchitecture.Application/Services/UserService.cs @@ -62,7 +62,8 @@ public sealed class UserService : IUserService user.Email, user.FirstName, user.LastName, - user.Role)); + user.Role, + user.TenantId)); } public async Task DeleteUserAsync(Guid userId) diff --git a/CleanArchitecture.Application/gRPC/TenantsApiImplementation.cs b/CleanArchitecture.Application/gRPC/TenantsApiImplementation.cs new file mode 100644 index 0000000..d356c9e --- /dev/null +++ b/CleanArchitecture.Application/gRPC/TenantsApiImplementation.cs @@ -0,0 +1,52 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using CleanArchitecture.Domain.Interfaces.Repositories; +using CleanArchitecture.Proto.Tenants; +using Grpc.Core; +using Microsoft.EntityFrameworkCore; + +namespace CleanArchitecture.Application.gRPC; + +public sealed class TenantsApiImplementation : TenantsApi.TenantsApiBase +{ + private readonly ITenantRepository _tenantRepository; + + public TenantsApiImplementation(ITenantRepository tenantRepository) + { + _tenantRepository = tenantRepository; + } + + public override async Task GetByIds( + GetTenantsByIdsRequest request, + ServerCallContext context) + { + var idsAsGuids = new List(request.Ids.Count); + + foreach (var id in request.Ids) + { + if (Guid.TryParse(id, out var parsed)) + { + idsAsGuids.Add(parsed); + } + } + + var tenants = await _tenantRepository + .GetAllNoTracking() + .Where(tenant => idsAsGuids.Contains(tenant.Id)) + .Select(tenant => new Tenant + { + Id = tenant.Id.ToString(), + Name = tenant.Name, + IsDeleted = tenant.Deleted + }) + .ToListAsync(); + + var result = new GetTenantsByIdsResult(); + + result.Tenants.AddRange(tenants); + + return result; + } +} \ No newline at end of file diff --git a/CleanArchitecture.Application/viewmodels/Users/UpdateUserViewModel.cs b/CleanArchitecture.Application/viewmodels/Users/UpdateUserViewModel.cs index 1826704..85f1846 100644 --- a/CleanArchitecture.Application/viewmodels/Users/UpdateUserViewModel.cs +++ b/CleanArchitecture.Application/viewmodels/Users/UpdateUserViewModel.cs @@ -8,4 +8,5 @@ public sealed record UpdateUserViewModel( string Email, string FirstName, string LastName, - UserRole Role); \ No newline at end of file + UserRole Role, + Guid TenantId); \ No newline at end of file diff --git a/CleanArchitecture.Domain.Tests/CommandHandler/Tenant/CreateTenant/CreateTenantCommandHandlerTests.cs b/CleanArchitecture.Domain.Tests/CommandHandler/Tenant/CreateTenant/CreateTenantCommandHandlerTests.cs new file mode 100644 index 0000000..77e2038 --- /dev/null +++ b/CleanArchitecture.Domain.Tests/CommandHandler/Tenant/CreateTenant/CreateTenantCommandHandlerTests.cs @@ -0,0 +1,69 @@ +using System; +using CleanArchitecture.Domain.Commands.Tenants.CreateTenant; +using CleanArchitecture.Domain.Errors; +using CleanArchitecture.Domain.Events.Tenant; +using Xunit; + +namespace CleanArchitecture.Domain.Tests.CommandHandler.Tenant.CreateTenant; + +public sealed class CreateTenantCommandHandlerTests +{ + private readonly CreateTenantCommandTestFixture _fixture = new(); + + [Fact] + public void Should_Create_Tenant() + { + var command = new CreateTenantCommand( + Guid.NewGuid(), + "Test Tenant"); + + _fixture.CommandHandler.Handle(command, default!).Wait(); + + _fixture + .VerifyNoDomainNotification() + .VerifyCommit() + .VerifyRaisedEvent(x => + x.AggregateId == command.AggregateId && + x.Name == command.Name); + } + + [Fact] + public void Should_Not_Create_Tenant_Insufficient_Permissions() + { + _fixture.SetupUser(); + + var command = new CreateTenantCommand( + Guid.NewGuid(), + "Test Tenant"); + + _fixture.CommandHandler.Handle(command, default!).Wait(); + + _fixture + .VerifyNoCommit() + .VerifyNoRaisedEvent() + .VerifyAnyDomainNotification() + .VerifyExistingNotification( + ErrorCodes.InsufficientPermissions, + $"No permission to create tenant {command.AggregateId}"); + } + + [Fact] + public void Should_Not_Create_Tenant_Already_Exists() + { + var command = new CreateTenantCommand( + Guid.NewGuid(), + "Test Tenant"); + + _fixture.SetupExistingTenant(command.AggregateId); + + _fixture.CommandHandler.Handle(command, default!).Wait(); + + _fixture + .VerifyNoCommit() + .VerifyNoRaisedEvent() + .VerifyAnyDomainNotification() + .VerifyExistingNotification( + DomainErrorCodes.Tenant.TenantAlreadyExists, + $"There is already a tenant with Id {command.AggregateId}"); + } +} \ No newline at end of file diff --git a/CleanArchitecture.Domain.Tests/CommandHandler/Tenant/CreateTenant/CreateTenantCommandTestFixture.cs b/CleanArchitecture.Domain.Tests/CommandHandler/Tenant/CreateTenant/CreateTenantCommandTestFixture.cs new file mode 100644 index 0000000..9c9a48c --- /dev/null +++ b/CleanArchitecture.Domain.Tests/CommandHandler/Tenant/CreateTenant/CreateTenantCommandTestFixture.cs @@ -0,0 +1,38 @@ +using System; +using CleanArchitecture.Domain.Commands.Tenants.CreateTenant; +using CleanArchitecture.Domain.Enums; +using CleanArchitecture.Domain.Interfaces.Repositories; +using NSubstitute; + +namespace CleanArchitecture.Domain.Tests.CommandHandler.Tenant.CreateTenant; + +public sealed class CreateTenantCommandTestFixture : CommandHandlerFixtureBase +{ + public CreateTenantCommandHandler CommandHandler { get;} + + private ITenantRepository TenantRepository { get; } + + public CreateTenantCommandTestFixture() + { + TenantRepository = Substitute.For(); + + CommandHandler = new( + Bus, + UnitOfWork, + NotificationHandler, + TenantRepository, + User); + } + + public void SetupUser() + { + User.GetUserRole().Returns(UserRole.User); + } + + public void SetupExistingTenant(Guid id) + { + TenantRepository + .ExistsAsync(Arg.Is(x => x == id)) + .Returns(true); + } +} \ No newline at end of file diff --git a/CleanArchitecture.Domain.Tests/CommandHandler/Tenant/CreateTenant/CreateTenantCommandValidationTests.cs b/CleanArchitecture.Domain.Tests/CommandHandler/Tenant/CreateTenant/CreateTenantCommandValidationTests.cs new file mode 100644 index 0000000..25aec37 --- /dev/null +++ b/CleanArchitecture.Domain.Tests/CommandHandler/Tenant/CreateTenant/CreateTenantCommandValidationTests.cs @@ -0,0 +1,53 @@ +using System; +using CleanArchitecture.Domain.Commands.Tenants.CreateTenant; +using CleanArchitecture.Domain.Errors; +using Xunit; + +namespace CleanArchitecture.Domain.Tests.CommandHandler.Tenant.CreateTenant; + +public sealed class CreateTenantCommandValidationTests : + ValidationTestBase +{ + public CreateTenantCommandValidationTests() : base(new CreateTenantCommandValidation()) + { + } + + [Fact] + public void Should_Be_Valid() + { + var command = CreateTestCommand(); + + ShouldBeValid(command); + } + + [Fact] + public void Should_Be_Invalid_For_Empty_Tenant_Id() + { + var command = CreateTestCommand(Guid.Empty); + + ShouldHaveSingleError( + command, + DomainErrorCodes.Tenant.TenantEmptyId, + "Tenant id may not be empty"); + } + + [Fact] + public void Should_Be_Invalid_For_Empty_Tenant_Name() + { + var command = CreateTestCommand(name: ""); + + ShouldHaveSingleError( + command, + DomainErrorCodes.Tenant.TenantEmptyName, + "Name may not be empty"); + } + + private static CreateTenantCommand CreateTestCommand( + Guid? id = null, + string? name = null) + { + return new( + id ?? Guid.NewGuid(), + name ?? "Test Tenant"); + } +} \ No newline at end of file diff --git a/CleanArchitecture.Domain.Tests/CommandHandler/Tenant/DeleteTenant/DeleteTenantCommandHandlerTests.cs b/CleanArchitecture.Domain.Tests/CommandHandler/Tenant/DeleteTenant/DeleteTenantCommandHandlerTests.cs new file mode 100644 index 0000000..27b5a2d --- /dev/null +++ b/CleanArchitecture.Domain.Tests/CommandHandler/Tenant/DeleteTenant/DeleteTenantCommandHandlerTests.cs @@ -0,0 +1,45 @@ +using System; +using CleanArchitecture.Domain.Commands.Tenants.DeleteTenant; +using CleanArchitecture.Domain.Errors; +using CleanArchitecture.Domain.Events.Tenant; +using Xunit; + +namespace CleanArchitecture.Domain.Tests.CommandHandler.Tenant.DeleteTenant; + +public sealed class DeleteTenantCommandHandlerTests +{ + private readonly DeleteTenantCommandTestFixture _fixture = new(); + + [Fact] + public void Should_Delete_Tenant() + { + var tenant = _fixture.SetupTenant(); + + var command = new DeleteTenantCommand(tenant.Id); + + _fixture.CommandHandler.Handle(command, default).Wait(); + + _fixture + .VerifyNoDomainNotification() + .VerifyCommit() + .VerifyRaisedEvent(x => x.AggregateId == tenant.Id); + } + + [Fact] + public void Should_Not_Delete_Non_Existing_Tenant() + { + _fixture.SetupTenant(); + + var command = new DeleteTenantCommand(Guid.NewGuid()); + + _fixture.CommandHandler.Handle(command, default).Wait(); + + _fixture + .VerifyNoCommit() + .VerifyNoRaisedEvent() + .VerifyAnyDomainNotification() + .VerifyExistingNotification( + ErrorCodes.ObjectNotFound, + $"There is no tenant with Id {command.AggregateId}"); + } +} \ No newline at end of file diff --git a/CleanArchitecture.Domain.Tests/CommandHandler/Tenant/DeleteTenant/DeleteTenantCommandTestFixture.cs b/CleanArchitecture.Domain.Tests/CommandHandler/Tenant/DeleteTenant/DeleteTenantCommandTestFixture.cs new file mode 100644 index 0000000..9ed5c2c --- /dev/null +++ b/CleanArchitecture.Domain.Tests/CommandHandler/Tenant/DeleteTenant/DeleteTenantCommandTestFixture.cs @@ -0,0 +1,39 @@ +using System; +using CleanArchitecture.Domain.Commands.Tenants.DeleteTenant; +using CleanArchitecture.Domain.Interfaces.Repositories; +using NSubstitute; + +namespace CleanArchitecture.Domain.Tests.CommandHandler.Tenant.DeleteTenant; + +public sealed class DeleteTenantCommandTestFixture : CommandHandlerFixtureBase +{ + public DeleteTenantCommandHandler CommandHandler { get;} + + private ITenantRepository TenantRepository { get; } + private IUserRepository UserRepository { get; } + + public DeleteTenantCommandTestFixture() + { + TenantRepository = Substitute.For(); + UserRepository = Substitute.For(); + + CommandHandler = new( + Bus, + UnitOfWork, + NotificationHandler, + TenantRepository, + UserRepository, + User); + } + + public Entities.Tenant SetupTenant() + { + var tenant = new Entities.Tenant(Guid.NewGuid(), "TestTenant"); + + TenantRepository + .GetByIdAsync(Arg.Is(y => y == tenant.Id)) + .Returns(tenant); + + return tenant; + } +} \ No newline at end of file diff --git a/CleanArchitecture.Domain.Tests/CommandHandler/Tenant/DeleteTenant/DeleteTenantCommandValidationTests.cs b/CleanArchitecture.Domain.Tests/CommandHandler/Tenant/DeleteTenant/DeleteTenantCommandValidationTests.cs new file mode 100644 index 0000000..48d3877 --- /dev/null +++ b/CleanArchitecture.Domain.Tests/CommandHandler/Tenant/DeleteTenant/DeleteTenantCommandValidationTests.cs @@ -0,0 +1,38 @@ +using System; +using CleanArchitecture.Domain.Commands.Tenants.DeleteTenant; +using CleanArchitecture.Domain.Errors; +using Xunit; + +namespace CleanArchitecture.Domain.Tests.CommandHandler.Tenant.DeleteTenant; + +public sealed class DeleteTenantCommandValidationTests : + ValidationTestBase +{ + public DeleteTenantCommandValidationTests() : base(new DeleteTenantCommandValidation()) + { + } + + [Fact] + public void Should_Be_Valid() + { + var command = CreateTestCommand(); + + ShouldBeValid(command); + } + + [Fact] + public void Should_Be_Invalid_For_Empty_Tenant_Id() + { + var command = CreateTestCommand(Guid.Empty); + + ShouldHaveSingleError( + command, + DomainErrorCodes.Tenant.TenantEmptyId, + "Tenant id may not be empty"); + } + + private static DeleteTenantCommand CreateTestCommand(Guid? tenantId = null) + { + return new(tenantId ?? Guid.NewGuid()); + } +} \ No newline at end of file diff --git a/CleanArchitecture.Domain.Tests/CommandHandler/Tenant/UpdateTenant/UpdateTenantCommandHandlerTests.cs b/CleanArchitecture.Domain.Tests/CommandHandler/Tenant/UpdateTenant/UpdateTenantCommandHandlerTests.cs new file mode 100644 index 0000000..8fa4424 --- /dev/null +++ b/CleanArchitecture.Domain.Tests/CommandHandler/Tenant/UpdateTenant/UpdateTenantCommandHandlerTests.cs @@ -0,0 +1,69 @@ +using System; +using CleanArchitecture.Domain.Commands.Tenants.UpdateTenant; +using CleanArchitecture.Domain.Errors; +using CleanArchitecture.Domain.Events.Tenant; +using Xunit; + +namespace CleanArchitecture.Domain.Tests.CommandHandler.Tenant.UpdateTenant; + +public sealed class UpdateTenantCommandHandlerTests +{ + private readonly UpdateTenantCommandTestFixture _fixture = new(); + + [Fact] + public void Should_Update_Tenant() + { + var command = new UpdateTenantCommand( + Guid.NewGuid(), + "Tenant Name"); + + _fixture.SetupExistingTenant(command.AggregateId); + + _fixture.CommandHandler.Handle(command, default!).Wait(); + + _fixture + .VerifyCommit() + .VerifyNoDomainNotification() + .VerifyRaisedEvent(x => + x.AggregateId == command.AggregateId && + x.Name == command.Name); + } + + [Fact] + public void Should_Not_Update_Tenant_Insufficient_Permissions() + { + var command = new UpdateTenantCommand( + Guid.NewGuid(), + "Tenant Name"); + + _fixture.SetupUser(); + + _fixture.CommandHandler.Handle(command, default!).Wait(); + + _fixture + .VerifyNoCommit() + .VerifyNoRaisedEvent() + .VerifyAnyDomainNotification() + .VerifyExistingNotification( + ErrorCodes.InsufficientPermissions, + $"No permission to update tenant {command.AggregateId}"); + } + + [Fact] + public void Should_Not_Update_Tenant_Not_Existing() + { + var command = new UpdateTenantCommand( + Guid.NewGuid(), + "Tenant Name"); + + _fixture.CommandHandler.Handle(command, default!).Wait(); + + _fixture + .VerifyNoCommit() + .VerifyNoRaisedEvent() + .VerifyAnyDomainNotification() + .VerifyExistingNotification( + ErrorCodes.ObjectNotFound, + $"There is no tenant with Id {command.AggregateId}"); + } +} \ No newline at end of file diff --git a/CleanArchitecture.Domain.Tests/CommandHandler/Tenant/UpdateTenant/UpdateTenantCommandTestFixture.cs b/CleanArchitecture.Domain.Tests/CommandHandler/Tenant/UpdateTenant/UpdateTenantCommandTestFixture.cs new file mode 100644 index 0000000..82879d6 --- /dev/null +++ b/CleanArchitecture.Domain.Tests/CommandHandler/Tenant/UpdateTenant/UpdateTenantCommandTestFixture.cs @@ -0,0 +1,39 @@ +using System; +using CleanArchitecture.Domain.Commands.Tenants.UpdateTenant; +using CleanArchitecture.Domain.Enums; +using CleanArchitecture.Domain.Interfaces; +using CleanArchitecture.Domain.Interfaces.Repositories; +using NSubstitute; + +namespace CleanArchitecture.Domain.Tests.CommandHandler.Tenant.UpdateTenant; + +public sealed class UpdateTenantCommandTestFixture : CommandHandlerFixtureBase +{ + public UpdateTenantCommandHandler CommandHandler { get;} + + private ITenantRepository TenantRepository { get; } + + public UpdateTenantCommandTestFixture() + { + TenantRepository = Substitute.For(); + + CommandHandler = new( + Bus, + UnitOfWork, + NotificationHandler, + TenantRepository, + User); + } + + public void SetupUser() + { + User.GetUserRole().Returns(UserRole.User); + } + + public void SetupExistingTenant(Guid id) + { + TenantRepository + .GetByIdAsync(Arg.Is(x => x == id)) + .Returns(new Entities.Tenant(id, "Test Tenant")); + } +} \ No newline at end of file diff --git a/CleanArchitecture.Domain.Tests/CommandHandler/Tenant/UpdateTenant/UpdateTenantCommandValidationTests.cs b/CleanArchitecture.Domain.Tests/CommandHandler/Tenant/UpdateTenant/UpdateTenantCommandValidationTests.cs new file mode 100644 index 0000000..4aac523 --- /dev/null +++ b/CleanArchitecture.Domain.Tests/CommandHandler/Tenant/UpdateTenant/UpdateTenantCommandValidationTests.cs @@ -0,0 +1,53 @@ +using System; +using CleanArchitecture.Domain.Commands.Tenants.UpdateTenant; +using CleanArchitecture.Domain.Errors; +using Xunit; + +namespace CleanArchitecture.Domain.Tests.CommandHandler.Tenant.UpdateTenant; + +public sealed class UpdateTenantCommandValidationTests : + ValidationTestBase +{ + public UpdateTenantCommandValidationTests() : base(new UpdateTenantCommandValidation()) + { + } + + [Fact] + public void Should_Be_Valid() + { + var command = CreateTestCommand(); + + ShouldBeValid(command); + } + + [Fact] + public void Should_Be_Invalid_For_Empty_Tenant_Id() + { + var command = CreateTestCommand(Guid.Empty); + + ShouldHaveSingleError( + command, + DomainErrorCodes.Tenant.TenantEmptyId, + "Tenant id may not be empty"); + } + + [Fact] + public void Should_Be_Invalid_For_Empty_Tenant_Name() + { + var command = CreateTestCommand(name: ""); + + ShouldHaveSingleError( + command, + DomainErrorCodes.Tenant.TenantEmptyName, + "Name may not be empty"); + } + + private static UpdateTenantCommand CreateTestCommand( + Guid? id = null, + string? name = null) + { + return new( + id ?? Guid.NewGuid(), + name ?? "Test Tenant"); + } +} \ 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 14fcb04..5382f23 100644 --- a/CleanArchitecture.Domain.Tests/CommandHandler/User/CreateUser/CreateUserCommandHandlerTests.cs +++ b/CleanArchitecture.Domain.Tests/CommandHandler/User/CreateUser/CreateUserCommandHandlerTests.cs @@ -13,11 +13,15 @@ public sealed class CreateUserCommandHandlerTests [Fact] public void Should_Create_User() { - _fixture.SetupUser(); + // Todo: Fix tests + _fixture.SetupCurrentUser(); + + var user = _fixture.SetupUser(); + _fixture.SetupTenant(user.TenantId); var command = new CreateUserCommand( Guid.NewGuid(), - Guid.NewGuid(), + user.TenantId, "test@email.com", "Test", "Email", @@ -34,6 +38,8 @@ public sealed class CreateUserCommandHandlerTests [Fact] public void Should_Not_Create_Already_Existing_User() { + _fixture.SetupCurrentUser(); + var user = _fixture.SetupUser(); var command = new CreateUserCommand( @@ -54,4 +60,54 @@ public sealed class CreateUserCommandHandlerTests DomainErrorCodes.User.UserAlreadyExists, $"There is already a user with Id {command.UserId}"); } + + [Fact] + public void Should_Not_Create_User_Tenant_Does_Not_Exist() + { + _fixture.SetupCurrentUser(); + + _fixture.SetupUser(); + + var command = new CreateUserCommand( + Guid.NewGuid(), + Guid.NewGuid(), + "test@email.com", + "Test", + "Email", + "Po=PF]PC6t.?8?ks)A6W"); + + _fixture.CommandHandler.Handle(command, default).Wait(); + + _fixture + .VerifyNoCommit() + .VerifyNoRaisedEvent() + .VerifyAnyDomainNotification() + .VerifyExistingNotification( + ErrorCodes.ObjectNotFound, + $"There is no tenant with Id {command.TenantId}"); + } + + [Fact] + public void Should_Not_Create_User_Insufficient_Permissions() + { + _fixture.SetupUser(); + + var command = new CreateUserCommand( + Guid.NewGuid(), + Guid.NewGuid(), + "test@email.com", + "Test", + "Email", + "Po=PF]PC6t.?8?ks)A6W"); + + _fixture.CommandHandler.Handle(command, default).Wait(); + + _fixture + .VerifyNoCommit() + .VerifyNoRaisedEvent() + .VerifyAnyDomainNotification() + .VerifyExistingNotification( + ErrorCodes.InsufficientPermissions, + "You are not allowed to create users"); + } } \ No newline at end of file diff --git a/CleanArchitecture.Domain.Tests/CommandHandler/User/CreateUser/CreateUserCommandTestFixture.cs b/CleanArchitecture.Domain.Tests/CommandHandler/User/CreateUser/CreateUserCommandTestFixture.cs index 31ea40e..6d6e5dc 100644 --- a/CleanArchitecture.Domain.Tests/CommandHandler/User/CreateUser/CreateUserCommandTestFixture.cs +++ b/CleanArchitecture.Domain.Tests/CommandHandler/User/CreateUser/CreateUserCommandTestFixture.cs @@ -1,6 +1,7 @@ using System; using CleanArchitecture.Domain.Commands.Users.CreateUser; using CleanArchitecture.Domain.Enums; +using CleanArchitecture.Domain.Interfaces; using CleanArchitecture.Domain.Interfaces.Repositories; using NSubstitute; @@ -11,16 +12,23 @@ public sealed class CreateUserCommandTestFixture : CommandHandlerFixtureBase public CreateUserCommandTestFixture() { UserRepository = Substitute.For(); + TenantRepository = Substitute.For(); + User = Substitute.For(); CommandHandler = new CreateUserCommandHandler( Bus, UnitOfWork, NotificationHandler, - UserRepository); + UserRepository, + TenantRepository, + User); } + // Todo: Properties over ctor public CreateUserCommandHandler CommandHandler { get; } private IUserRepository UserRepository { get; } + private ITenantRepository TenantRepository { get; } + private IUser User { get; } public Entities.User SetupUser() { @@ -39,4 +47,29 @@ public sealed class CreateUserCommandTestFixture : CommandHandlerFixtureBase return user; } + + public void SetupCurrentUser() + { + var userId = Guid.NewGuid(); + + User.GetUserId().Returns(userId); + + UserRepository + .GetByIdAsync(Arg.Is(y => y == userId)) + .Returns(new Entities.User( + userId, + Guid.NewGuid(), + "some email", + "some first name", + "some last name", + "some password", + UserRole.Admin)); + } + + public void SetupTenant(Guid tenantId) + { + TenantRepository + .ExistsAsync(Arg.Is(y => y == tenantId)) + .Returns(true); + } } \ No newline at end of file diff --git a/CleanArchitecture.Domain.Tests/CommandHandler/User/CreateUser/CreateUserCommandValidationTests.cs b/CleanArchitecture.Domain.Tests/CommandHandler/User/CreateUser/CreateUserCommandValidationTests.cs index 3e303ec..015b40d 100644 --- a/CleanArchitecture.Domain.Tests/CommandHandler/User/CreateUser/CreateUserCommandValidationTests.cs +++ b/CleanArchitecture.Domain.Tests/CommandHandler/User/CreateUser/CreateUserCommandValidationTests.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.Linq; using CleanArchitecture.Domain.Commands.Users.CreateUser; +using CleanArchitecture.Domain.Constants; using CleanArchitecture.Domain.Errors; using Xunit; @@ -58,12 +59,12 @@ public sealed class CreateUserCommandValidationTests : [Fact] public void Should_Be_Invalid_For_Email_Exceeds_Max_Length() { - var command = CreateTestCommand(email: new string('a', 320) + "@test.com"); + var command = CreateTestCommand(email: new string('a', MaxLengths.User.Email) + "@test.com"); ShouldHaveSingleError( command, DomainErrorCodes.User.UserEmailExceedsMaxLength, - "Email may not be longer than 320 characters"); + $"Email may not be longer than {MaxLengths.User.Email} characters"); } [Fact] @@ -80,12 +81,12 @@ public sealed class CreateUserCommandValidationTests : [Fact] public void Should_Be_Invalid_For_First_Name_Exceeds_Max_Length() { - var command = CreateTestCommand(firstName: new string('a', 101)); + var command = CreateTestCommand(firstName: new string('a', MaxLengths.User.FirstName + 1)); ShouldHaveSingleError( command, DomainErrorCodes.User.UserFirstNameExceedsMaxLength, - "FirstName may not be longer than 100 characters"); + $"FirstName may not be longer than {MaxLengths.User.FirstName} characters"); } [Fact] @@ -102,12 +103,12 @@ public sealed class CreateUserCommandValidationTests : [Fact] public void Should_Be_Invalid_For_Last_Name_Exceeds_Max_Length() { - var command = CreateTestCommand(lastName: new string('a', 101)); + var command = CreateTestCommand(lastName: new string('a', MaxLengths.User.LastName + 1)); ShouldHaveSingleError( command, DomainErrorCodes.User.UserLastNameExceedsMaxLength, - "LastName may not be longer than 100 characters"); + $"LastName may not be longer than {MaxLengths.User.LastName} characters"); } [Fact] @@ -175,6 +176,14 @@ public sealed class CreateUserCommandValidationTests : ShouldHaveSingleError(command, DomainErrorCodes.User.UserLongPassword); } + + [Fact] + public void Should_Be_Invalid_For_Empty_Tenant_Id() + { + var command = CreateTestCommand(tenantId: Guid.Empty); + + ShouldHaveSingleError(command, DomainErrorCodes.Tenant.TenantEmptyId); + } private static CreateUserCommand CreateTestCommand( Guid? userId = null, diff --git a/CleanArchitecture.Domain.Tests/CommandHandler/User/LoginUser/LoginUserCommandValidationTests.cs b/CleanArchitecture.Domain.Tests/CommandHandler/User/LoginUser/LoginUserCommandValidationTests.cs index d875b73..e6b4675 100644 --- a/CleanArchitecture.Domain.Tests/CommandHandler/User/LoginUser/LoginUserCommandValidationTests.cs +++ b/CleanArchitecture.Domain.Tests/CommandHandler/User/LoginUser/LoginUserCommandValidationTests.cs @@ -1,6 +1,7 @@ using System.Collections.Generic; using System.Linq; using CleanArchitecture.Domain.Commands.Users.LoginUser; +using CleanArchitecture.Domain.Constants; using CleanArchitecture.Domain.Errors; using Xunit; @@ -46,12 +47,12 @@ public sealed class LoginUserCommandValidationTests : [Fact] public void Should_Be_Invalid_For_Email_Exceeds_Max_Length() { - var command = CreateTestCommand(new string('a', 320) + "@test.com"); + var command = CreateTestCommand(new string('a', MaxLengths.User.Email) + "@test.com"); ShouldHaveSingleError( command, DomainErrorCodes.User.UserEmailExceedsMaxLength, - "Email may not be longer than 320 characters"); + $"Email may not be longer than {MaxLengths.User.Email} characters"); } [Fact] diff --git a/CleanArchitecture.Domain.Tests/CommandHandler/User/UpdateUser/UpdateUserCommandHandlerTests.cs b/CleanArchitecture.Domain.Tests/CommandHandler/User/UpdateUser/UpdateUserCommandHandlerTests.cs index 4bcb809..683d677 100644 --- a/CleanArchitecture.Domain.Tests/CommandHandler/User/UpdateUser/UpdateUserCommandHandlerTests.cs +++ b/CleanArchitecture.Domain.Tests/CommandHandler/User/UpdateUser/UpdateUserCommandHandlerTests.cs @@ -23,7 +23,8 @@ public sealed class UpdateUserCommandHandlerTests "test@email.com", "Test", "Email", - UserRole.User); + UserRole.User, + Guid.NewGuid()); await _fixture.CommandHandler.Handle(command, default); @@ -43,7 +44,8 @@ public sealed class UpdateUserCommandHandlerTests "test@email.com", "Test", "Email", - UserRole.User); + UserRole.User, + Guid.NewGuid()); await _fixture.CommandHandler.Handle(command, default); @@ -66,7 +68,8 @@ public sealed class UpdateUserCommandHandlerTests "test@email.com", "Test", "Email", - UserRole.User); + UserRole.User, + Guid.NewGuid()); _fixture.UserRepository .GetByEmailAsync(command.Email) diff --git a/CleanArchitecture.Domain.Tests/CommandHandler/User/UpdateUser/UpdateUserCommandValidationTests.cs b/CleanArchitecture.Domain.Tests/CommandHandler/User/UpdateUser/UpdateUserCommandValidationTests.cs index 762dcb1..161aabc 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.Constants; using CleanArchitecture.Domain.Enums; using CleanArchitecture.Domain.Errors; using Xunit; @@ -57,12 +58,12 @@ public sealed class UpdateUserCommandValidationTests : [Fact] public void Should_Be_Invalid_For_Email_Exceeds_Max_Length() { - var command = CreateTestCommand(email: new string('a', 320) + "@test.com"); + var command = CreateTestCommand(email: new string('a', MaxLengths.User.Email) + "@test.com"); ShouldHaveSingleError( command, DomainErrorCodes.User.UserEmailExceedsMaxLength, - "Email may not be longer than 320 characters"); + $"Email may not be longer than {MaxLengths.User.Email} characters"); } [Fact] @@ -79,12 +80,12 @@ public sealed class UpdateUserCommandValidationTests : [Fact] public void Should_Be_Invalid_For_First_Name_Exceeds_Max_Length() { - var command = CreateTestCommand(firstName: new string('a', 101)); + var command = CreateTestCommand(firstName: new string('a', MaxLengths.User.FirstName + 1)); ShouldHaveSingleError( command, DomainErrorCodes.User.UserFirstNameExceedsMaxLength, - "FirstName may not be longer than 100 characters"); + $"FirstName may not be longer than {MaxLengths.User.FirstName} characters"); } [Fact] @@ -101,16 +102,28 @@ public sealed class UpdateUserCommandValidationTests : [Fact] public void Should_Be_Invalid_For_Last_Name_Exceeds_Max_Length() { - var command = CreateTestCommand(lastName: new string('a', 101)); + var command = CreateTestCommand(lastName: new string('a', MaxLengths.User.LastName + 1)); ShouldHaveSingleError( command, DomainErrorCodes.User.UserLastNameExceedsMaxLength, - "LastName may not be longer than 100 characters"); + $"LastName may not be longer than {MaxLengths.User.LastName} characters"); + } + + [Fact] + public void Should_Be_Invalid_For_Empty_Tenant_Id() + { + var command = CreateTestCommand(tenantId: Guid.Empty); + + ShouldHaveSingleError( + command, + DomainErrorCodes.Tenant.TenantEmptyId, + "Tenant id may not be empty"); } private static UpdateUserCommand CreateTestCommand( Guid? userId = null, + Guid? tenantId = null, string? email = null, string? firstName = null, string? lastName = null, @@ -121,6 +134,7 @@ public sealed class UpdateUserCommandValidationTests : email ?? "test@email.com", firstName ?? "test", lastName ?? "email", - role ?? UserRole.User); + role ?? UserRole.User, + tenantId ?? Guid.NewGuid()); } } \ No newline at end of file diff --git a/CleanArchitecture.Domain/Commands/Tenants/CreateTenant/CreateTenantCommandHandler.cs b/CleanArchitecture.Domain/Commands/Tenants/CreateTenant/CreateTenantCommandHandler.cs index 78c4aa1..c96c8f0 100644 --- a/CleanArchitecture.Domain/Commands/Tenants/CreateTenant/CreateTenantCommandHandler.cs +++ b/CleanArchitecture.Domain/Commands/Tenants/CreateTenant/CreateTenantCommandHandler.cs @@ -1,6 +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.Tenant; using CleanArchitecture.Domain.Interfaces; @@ -14,14 +15,17 @@ public sealed class CreateTenantCommandHandler : CommandHandlerBase, IRequestHandler { private readonly ITenantRepository _tenantRepository; - + private readonly IUser _user; + public CreateTenantCommandHandler( IMediatorHandler bus, IUnitOfWork unitOfWork, INotificationHandler notifications, - ITenantRepository tenantRepository) : base(bus, unitOfWork, notifications) + ITenantRepository tenantRepository, + IUser user) : base(bus, unitOfWork, notifications) { _tenantRepository = tenantRepository; + _user = user; } public async Task Handle(CreateTenantCommand request, CancellationToken cancellationToken) @@ -30,6 +34,17 @@ public sealed class CreateTenantCommandHandler : CommandHandlerBase, { return; } + + if (_user.GetUserRole() != UserRole.Admin) + { + await NotifyAsync( + new DomainNotification( + request.MessageType, + $"No permission to create tenant {request.AggregateId}", + ErrorCodes.InsufficientPermissions)); + + return; + } if (await _tenantRepository.ExistsAsync(request.AggregateId)) { diff --git a/CleanArchitecture.Domain/Commands/Tenants/DeleteTenant/DeleteTenantCommandHandler.cs b/CleanArchitecture.Domain/Commands/Tenants/DeleteTenant/DeleteTenantCommandHandler.cs index 1a1ab43..6c6b91a 100644 --- a/CleanArchitecture.Domain/Commands/Tenants/DeleteTenant/DeleteTenantCommandHandler.cs +++ b/CleanArchitecture.Domain/Commands/Tenants/DeleteTenant/DeleteTenantCommandHandler.cs @@ -1,6 +1,7 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; +using CleanArchitecture.Domain.Enums; using CleanArchitecture.Domain.Errors; using CleanArchitecture.Domain.Events.Tenant; using CleanArchitecture.Domain.Interfaces; @@ -15,16 +16,19 @@ public sealed class DeleteTenantCommandHandler : CommandHandlerBase, { private readonly ITenantRepository _tenantRepository; private readonly IUserRepository _userRepository; - + private readonly IUser _user; + public DeleteTenantCommandHandler( IMediatorHandler bus, IUnitOfWork unitOfWork, INotificationHandler notifications, ITenantRepository tenantRepository, - IUserRepository userRepository) : base(bus, unitOfWork, notifications) + IUserRepository userRepository, + IUser user) : base(bus, unitOfWork, notifications) { _tenantRepository = tenantRepository; _userRepository = userRepository; + _user = user; } public async Task Handle(DeleteTenantCommand request, CancellationToken cancellationToken) @@ -33,6 +37,19 @@ public sealed class DeleteTenantCommandHandler : CommandHandlerBase, { return; } + + // Todo: Test following + + if (_user.GetUserRole() != UserRole.Admin) + { + await NotifyAsync( + new DomainNotification( + request.MessageType, + $"No permission to delete tenant {request.AggregateId}", + ErrorCodes.InsufficientPermissions)); + + return; + } var tenant = await _tenantRepository.GetByIdAsync(request.AggregateId); diff --git a/CleanArchitecture.Domain/Commands/Tenants/UpdateTenant/UpdateTenantCommandHandler.cs b/CleanArchitecture.Domain/Commands/Tenants/UpdateTenant/UpdateTenantCommandHandler.cs index f1ba406..4ef9929 100644 --- a/CleanArchitecture.Domain/Commands/Tenants/UpdateTenant/UpdateTenantCommandHandler.cs +++ b/CleanArchitecture.Domain/Commands/Tenants/UpdateTenant/UpdateTenantCommandHandler.cs @@ -1,5 +1,6 @@ using System.Threading; using System.Threading.Tasks; +using CleanArchitecture.Domain.Enums; using CleanArchitecture.Domain.Errors; using CleanArchitecture.Domain.Events.Tenant; using CleanArchitecture.Domain.Interfaces; @@ -13,14 +14,17 @@ public sealed class UpdateTenantCommandHandler : CommandHandlerBase, IRequestHandler { private readonly ITenantRepository _tenantRepository; - + private readonly IUser _user; + public UpdateTenantCommandHandler( IMediatorHandler bus, IUnitOfWork unitOfWork, INotificationHandler notifications, - ITenantRepository tenantRepository) : base(bus, unitOfWork, notifications) + ITenantRepository tenantRepository, + IUser user) : base(bus, unitOfWork, notifications) { _tenantRepository = tenantRepository; + _user = user; } public async Task Handle(UpdateTenantCommand request, CancellationToken cancellationToken) @@ -29,6 +33,17 @@ public sealed class UpdateTenantCommandHandler : CommandHandlerBase, { return; } + + if (_user.GetUserRole() != UserRole.Admin) + { + await NotifyAsync( + new DomainNotification( + request.MessageType, + $"No permission to update tenant {request.AggregateId}", + ErrorCodes.InsufficientPermissions)); + + return; + } var tenant = await _tenantRepository.GetByIdAsync(request.AggregateId); diff --git a/CleanArchitecture.Domain/Commands/Users/CreateUser/CreateUserCommandHandler.cs b/CleanArchitecture.Domain/Commands/Users/CreateUser/CreateUserCommandHandler.cs index 7e40c4d..4407656 100644 --- a/CleanArchitecture.Domain/Commands/Users/CreateUser/CreateUserCommandHandler.cs +++ b/CleanArchitecture.Domain/Commands/Users/CreateUser/CreateUserCommandHandler.cs @@ -16,14 +16,20 @@ public sealed class CreateUserCommandHandler : CommandHandlerBase, IRequestHandler { private readonly IUserRepository _userRepository; + private readonly ITenantRepository _tenantRepository; + private readonly IUser _user; public CreateUserCommandHandler( IMediatorHandler bus, IUnitOfWork unitOfWork, INotificationHandler notifications, - IUserRepository userRepository) : base(bus, unitOfWork, notifications) + IUserRepository userRepository, + ITenantRepository tenantRepository, + IUser user) : base(bus, unitOfWork, notifications) { _userRepository = userRepository; + _tenantRepository = tenantRepository; + _user = user; } public async Task Handle(CreateUserCommand request, CancellationToken cancellationToken) @@ -32,12 +38,24 @@ public sealed class CreateUserCommandHandler : CommandHandlerBase, { return; } + + var currentUser = await _userRepository.GetByIdAsync(_user.GetUserId()); + + if (currentUser is null || currentUser.Role != UserRole.Admin) + { + await NotifyAsync( + new DomainNotification( + request.MessageType, + "You are not allowed to create users", + ErrorCodes.InsufficientPermissions)); + return; + } var existingUser = await _userRepository.GetByIdAsync(request.UserId); if (existingUser is not null) { - await Bus.RaiseEventAsync( + await NotifyAsync( new DomainNotification( request.MessageType, $"There is already a user with Id {request.UserId}", @@ -49,7 +67,7 @@ public sealed class CreateUserCommandHandler : CommandHandlerBase, if (existingUser is not null) { - await Bus.RaiseEventAsync( + await NotifyAsync( new DomainNotification( request.MessageType, $"There is already a user with email {request.Email}", @@ -57,6 +75,16 @@ public sealed class CreateUserCommandHandler : CommandHandlerBase, return; } + if (!await _tenantRepository.ExistsAsync(request.TenantId)) + { + await NotifyAsync( + new DomainNotification( + request.MessageType, + $"There is no tenant with Id {request.TenantId}", + ErrorCodes.ObjectNotFound)); + return; + } + var passwordHash = BC.HashPassword(request.Password); var user = new User( diff --git a/CleanArchitecture.Domain/Commands/Users/CreateUser/CreateUserCommandValidation.cs b/CleanArchitecture.Domain/Commands/Users/CreateUser/CreateUserCommandValidation.cs index c2d9564..371bf60 100644 --- a/CleanArchitecture.Domain/Commands/Users/CreateUser/CreateUserCommandValidation.cs +++ b/CleanArchitecture.Domain/Commands/Users/CreateUser/CreateUserCommandValidation.cs @@ -10,6 +10,7 @@ public sealed class CreateUserCommandValidation : AbstractValidator cmd.TenantId) + .NotEmpty() + .WithErrorCode(DomainErrorCodes.Tenant.TenantEmptyId) + .WithMessage("Tenant id may not be empty"); + } + private void AddRuleForEmail() { RuleFor(cmd => cmd.Email) @@ -32,7 +41,7 @@ public sealed class CreateUserCommandValidation : AbstractValidator cmd.TenantId) + .NotEmpty() + .WithErrorCode(DomainErrorCodes.Tenant.TenantEmptyId) + .WithMessage("Tenant id may not be empty"); + } private void AddRuleForEmail() { @@ -31,7 +40,7 @@ public sealed class UpdateUserCommandValidation : AbstractValidator $"{FirstName}, {LastName}"; public Guid TenantId { get; private set; } @@ -58,4 +58,9 @@ public class User : Entity { Role = role; } + + public void SetTenant(Guid tenantId) + { + TenantId = tenantId; + } } \ No newline at end of file diff --git a/CleanArchitecture.Domain/Errors/DomainErrorCodes.cs b/CleanArchitecture.Domain/Errors/DomainErrorCodes.cs index d4131c8..76977e4 100644 --- a/CleanArchitecture.Domain/Errors/DomainErrorCodes.cs +++ b/CleanArchitecture.Domain/Errors/DomainErrorCodes.cs @@ -34,7 +34,7 @@ public static class DomainErrorCodes public const string TenantEmptyId = "TENANT_EMPTY_ID"; public const string TenantEmptyName = "TENANT_EMPTY_NAME"; public const string TenantNameExceedsMaxLength = "TENANT_NAME_EXCEEDS_MAX_LENGTH"; - + // General public const string TenantAlreadyExists = "TENANT_ALREADY_EXISTS"; } diff --git a/CleanArchitecture.Infrastructure/Repositories/BaseRepository.cs b/CleanArchitecture.Infrastructure/Repositories/BaseRepository.cs index 9137422..4be4db9 100644 --- a/CleanArchitecture.Infrastructure/Repositories/BaseRepository.cs +++ b/CleanArchitecture.Infrastructure/Repositories/BaseRepository.cs @@ -55,7 +55,7 @@ public class BaseRepository : IRepository where TEntity : Enti DbSet.Update(entity); } - public async Task ExistsAsync(Guid id) + public virtual async Task ExistsAsync(Guid id) { return await DbSet.AnyAsync(entity => entity.Id == id); } diff --git a/CleanArchitecture.IntegrationTests/Controller/TenantControllerTests.cs b/CleanArchitecture.IntegrationTests/Controller/TenantControllerTests.cs new file mode 100644 index 0000000..a3d60e5 --- /dev/null +++ b/CleanArchitecture.IntegrationTests/Controller/TenantControllerTests.cs @@ -0,0 +1,136 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Threading.Tasks; +using CleanArchitecture.Application.ViewModels.Tenants; +using CleanArchitecture.IntegrationTests.Extensions; +using CleanArchitecture.IntegrationTests.Fixtures; +using FluentAssertions; +using Xunit; +using Xunit.Priority; + +namespace CleanArchitecture.IntegrationTests.Controller; + +[Collection("IntegrationTests")] +[TestCaseOrderer(PriorityOrderer.Name, PriorityOrderer.Assembly)] +public sealed class TenantControllerTests : IClassFixture +{ + private readonly TenantTestFixture _fixture; + + public TenantControllerTests(TenantTestFixture fixture) + { + _fixture = fixture; + } + + [Fact] + [Priority(0)] + public async Task Should_Get_Tenant_By_Id() + { + await _fixture.AuthenticateUserAsync(); + + var response = await _fixture.ServerClient.GetAsync($"/api/v1/Tenant/{_fixture.CreatedTenantId}"); + + response.StatusCode.Should().Be(HttpStatusCode.OK); + + var message = await response.Content.ReadAsJsonAsync(); + + message?.Data.Should().NotBeNull(); + + message!.Data!.Id.Should().Be(_fixture.CreatedTenantId); + message.Data.Name.Should().Be("Test Tenant"); + } + + [Fact] + [Priority(5)] + public async Task Should_Get_All_Tenants() + { + await _fixture.AuthenticateUserAsync(); + + var response = await _fixture.ServerClient.GetAsync("api/v1/Tenant"); + + response.StatusCode.Should().Be(HttpStatusCode.OK); + + var message = await response.Content.ReadAsJsonAsync>(); + + message?.Data.Should().NotBeEmpty(); + message!.Data.Should().HaveCountGreaterOrEqualTo(2); + message.Data! + .FirstOrDefault(x => x.Id == _fixture.CreatedTenantId) + .Should().NotBeNull(); + } + + [Fact] + [Priority(10)] + public async Task Should_Create_Tenant() + { + await _fixture.AuthenticateUserAsync(); + + var request = new CreateTenantViewModel("Test Tenant 2"); + + var response = await _fixture.ServerClient.PostAsJsonAsync("/api/v1/Tenant", request); + + response.StatusCode.Should().Be(HttpStatusCode.OK); + + var message = await response.Content.ReadAsJsonAsync(); + var tenantId = message?.Data; + + // Check if tenant exists + var tenantResponse = await _fixture.ServerClient.GetAsync($"/api/v1/Tenant/{tenantId}"); + + tenantResponse.StatusCode.Should().Be(HttpStatusCode.OK); + + var tenantMessage = await tenantResponse.Content.ReadAsJsonAsync(); + + tenantMessage?.Data.Should().NotBeNull(); + + tenantMessage!.Data!.Id.Should().Be(tenantId!.Value); + tenantMessage.Data.Name.Should().Be(request.Name); + } + + [Fact] + [Priority(15)] + public async Task Should_Update_Tenant() + { + await _fixture.AuthenticateUserAsync(); + + var request = new UpdateTenantViewModel(_fixture.CreatedTenantId, "Test Tenant 3"); + + var response = await _fixture.ServerClient.PutAsJsonAsync("/api/v1/Tenant", request); + + response.StatusCode.Should().Be(HttpStatusCode.OK); + + var message = await response.Content.ReadAsJsonAsync(); + + message?.Data.Should().NotBeNull(); + message!.Data.Should().BeEquivalentTo(request); + + // Check if tenant is updated + var tenantResponse = await _fixture.ServerClient.GetAsync($"/api/v1/Tenant/{_fixture.CreatedTenantId}"); + + tenantResponse.StatusCode.Should().Be(HttpStatusCode.OK); + + var tenantMessage = await response.Content.ReadAsJsonAsync(); + + tenantMessage?.Data.Should().NotBeNull(); + + tenantMessage!.Data!.Id.Should().Be(_fixture.CreatedTenantId); + tenantMessage.Data.Name.Should().Be(request.Name); + } + + [Fact] + [Priority(20)] + public async Task Should_Delete_Tenant() + { + await _fixture.AuthenticateUserAsync(); + + var response = await _fixture.ServerClient.DeleteAsync($"/api/v1/Tenant/{_fixture.CreatedTenantId}"); + + response.StatusCode.Should().Be(HttpStatusCode.OK); + + // Check if tenant is deleted + var tenantResponse = await _fixture.ServerClient.GetAsync($"/api/v1/Tenant/{_fixture.CreatedTenantId}"); + + tenantResponse.StatusCode.Should().Be(HttpStatusCode.NotFound); + } +} \ No newline at end of file diff --git a/CleanArchitecture.IntegrationTests/Controller/UserControllerTests.cs b/CleanArchitecture.IntegrationTests/Controller/UserControllerTests.cs index ee176ec..4fd17ef 100644 --- a/CleanArchitecture.IntegrationTests/Controller/UserControllerTests.cs +++ b/CleanArchitecture.IntegrationTests/Controller/UserControllerTests.cs @@ -25,10 +25,14 @@ public sealed class UserControllerTests : IClassFixture _fixture = fixture; } + // Todo: Refactor tests to work alone + [Fact] [Priority(0)] public async Task Should_Create_User() { + await _fixture.AuthenticateUserAsync(); + var user = new CreateUserViewModel( _fixture.CreatedUserEmail, "Test", @@ -116,7 +120,8 @@ public sealed class UserControllerTests : IClassFixture "newtest@email.com", "NewTest", "NewEmail", - UserRole.User); + UserRole.User, + Ids.Seed.TenantId); var response = await _fixture.ServerClient.PutAsJsonAsync("/api/v1/user", user); @@ -232,5 +237,7 @@ public sealed class UserControllerTests : IClassFixture var content = message!.Data; content.Should().Be(_fixture.CreatedUserId); + + // Todo: Check if stuff is done } } \ No newline at end of file diff --git a/CleanArchitecture.IntegrationTests/Fixtures/TenantTestFixture.cs b/CleanArchitecture.IntegrationTests/Fixtures/TenantTestFixture.cs new file mode 100644 index 0000000..0ab2498 --- /dev/null +++ b/CleanArchitecture.IntegrationTests/Fixtures/TenantTestFixture.cs @@ -0,0 +1,19 @@ +using System; +using CleanArchitecture.Domain.Entities; +using CleanArchitecture.Infrastructure.Database; + +namespace CleanArchitecture.IntegrationTests.Fixtures; + +public sealed class TenantTestFixture : TestFixtureBase +{ + public Guid CreatedTenantId { get; } = Guid.NewGuid(); + + protected override void SeedTestData(ApplicationDbContext context) + { + context.Tenants.Add(new Tenant( + CreatedTenantId, + "Test Tenant")); + + context.SaveChanges(); + } +} \ No newline at end of file diff --git a/CleanArchitecture.IntegrationTests/Fixtures/TestFixtureBase.cs b/CleanArchitecture.IntegrationTests/Fixtures/TestFixtureBase.cs index 99a38ed..80c2273 100644 --- a/CleanArchitecture.IntegrationTests/Fixtures/TestFixtureBase.cs +++ b/CleanArchitecture.IntegrationTests/Fixtures/TestFixtureBase.cs @@ -1,6 +1,10 @@ using System; +using System.Net; using System.Net.Http; +using System.Threading.Tasks; +using CleanArchitecture.Application.ViewModels.Users; using CleanArchitecture.Infrastructure.Database; +using CleanArchitecture.IntegrationTests.Extensions; using CleanArchitecture.IntegrationTests.Infrastructure; using Microsoft.AspNetCore.Mvc.Testing; using Microsoft.Extensions.DependencyInjection; @@ -32,4 +36,18 @@ public class TestFixtureBase IServiceProvider scopedServices) { } + + // Todo: Fix auth + public virtual async Task AuthenticateUserAsync() + { + ServerClient.DefaultRequestHeaders.Clear(); + var user = new LoginUserViewModel( + "admin@email.com", + "!Password123#"); + + var response = await ServerClient.PostAsJsonAsync("/api/v1/user/login", user); + + var message = await response.Content.ReadAsJsonAsync(); + ServerClient.DefaultRequestHeaders.Add("Authorization", $"Bearer {message!.Data}"); + } } \ No newline at end of file diff --git a/CleanArchitecture.IntegrationTests/Fixtures/UserTestFixture.cs b/CleanArchitecture.IntegrationTests/Fixtures/UserTestFixture.cs index 5769927..e2421d2 100644 --- a/CleanArchitecture.IntegrationTests/Fixtures/UserTestFixture.cs +++ b/CleanArchitecture.IntegrationTests/Fixtures/UserTestFixture.cs @@ -11,6 +11,7 @@ public sealed class UserTestFixture : TestFixtureBase public void EnableAuthentication() { + ServerClient.DefaultRequestHeaders.Clear(); ServerClient.DefaultRequestHeaders.Add("Authorization", $"Bearer {CreatedUserToken}"); } } \ No newline at end of file diff --git a/CleanArchitecture.IntegrationTests/Fixtures/gRPC/GetTenantsByIdsTestFixture.cs b/CleanArchitecture.IntegrationTests/Fixtures/gRPC/GetTenantsByIdsTestFixture.cs new file mode 100644 index 0000000..5c9c3bd --- /dev/null +++ b/CleanArchitecture.IntegrationTests/Fixtures/gRPC/GetTenantsByIdsTestFixture.cs @@ -0,0 +1,37 @@ +using System; +using CleanArchitecture.Domain.Entities; +using CleanArchitecture.Infrastructure.Database; +using Grpc.Net.Client; + +namespace CleanArchitecture.IntegrationTests.Fixtures.gRPC; + +public sealed class GetTenantsByIdsTestFixture : TestFixtureBase +{ + public GrpcChannel GrpcChannel { get; } + public Guid CreatedTenantId { get; } = Guid.NewGuid(); + + public GetTenantsByIdsTestFixture() + { + GrpcChannel = GrpcChannel.ForAddress("http://localhost", new GrpcChannelOptions + { + HttpHandler = Factory.Server.CreateHandler() + }); + } + + protected override void SeedTestData(ApplicationDbContext context) + { + base.SeedTestData(context); + + var tenant = CreateTenant(); + + context.Tenants.Add(tenant); + context.SaveChanges(); + } + + public Tenant CreateTenant() + { + return new( + CreatedTenantId, + "Test Tenant"); + } +} \ No newline at end of file diff --git a/CleanArchitecture.IntegrationTests/gRPC/GetTenantsByIdsTests.cs b/CleanArchitecture.IntegrationTests/gRPC/GetTenantsByIdsTests.cs new file mode 100644 index 0000000..5c939a3 --- /dev/null +++ b/CleanArchitecture.IntegrationTests/gRPC/GetTenantsByIdsTests.cs @@ -0,0 +1,38 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using CleanArchitecture.IntegrationTests.Fixtures.gRPC; +using CleanArchitecture.Proto.Tenants; +using FluentAssertions; +using Xunit; + +namespace CleanArchitecture.IntegrationTests.gRPC; + +public sealed class GetTenantsByIdsTests : IClassFixture +{ + private readonly GetTenantsByIdsTestFixture _fixture; + + public GetTenantsByIdsTests(GetTenantsByIdsTestFixture fixture) + { + _fixture = fixture; + } + + [Fact] + public async Task Should_Get_Tenants_By_Ids() + { + var client = new TenantsApi.TenantsApiClient(_fixture.GrpcChannel); + + var request = new GetTenantsByIdsRequest(); + request.Ids.Add(_fixture.CreatedTenantId.ToString()); + + var response = await client.GetByIdsAsync(request); + + response.Tenants.Should().HaveCount(1); + + var tenant = response.Tenants.First(); + var createdTenant = _fixture.CreateTenant(); + + new Guid(tenant.Id).Should().Be(createdTenant.Id); + tenant.Name.Should().Be(createdTenant.Name); + } +} \ No newline at end of file diff --git a/CleanArchitecture.IntegrationTests/gRPC/GetUsersByIdsTests.cs b/CleanArchitecture.IntegrationTests/gRPC/GetUsersByIdsTests.cs index 8464d7d..6576493 100644 --- a/CleanArchitecture.IntegrationTests/gRPC/GetUsersByIdsTests.cs +++ b/CleanArchitecture.IntegrationTests/gRPC/GetUsersByIdsTests.cs @@ -26,7 +26,7 @@ public sealed class GetUsersByIdsTests : IClassFixture var response = await client.GetByIdsAsync(request); - response.Users.Count.Should().Be(1); + response.Users.Should().HaveCount(1); var user = response.Users.First(); var createdUser = _fixture.CreateUser(); diff --git a/CleanArchitecture.gRPC.Tests/Fixtures/TenantTestFixture.cs b/CleanArchitecture.gRPC.Tests/Fixtures/TenantTestFixture.cs new file mode 100644 index 0000000..4fb0177 --- /dev/null +++ b/CleanArchitecture.gRPC.Tests/Fixtures/TenantTestFixture.cs @@ -0,0 +1,33 @@ +using System; +using System.Collections.Generic; +using CleanArchitecture.Application.gRPC; +using CleanArchitecture.Domain.Entities; +using CleanArchitecture.Domain.Interfaces.Repositories; +using MockQueryable.NSubstitute; +using NSubstitute; + +namespace CleanArchitecture.gRPC.Tests.Fixtures; + +public sealed class TenantTestFixture +{ + public TenantsApiImplementation TenantsApiImplementation { get; } + private ITenantRepository TenantRepository { get; } + + public IEnumerable ExistingTenants { get; } + + public TenantTestFixture() + { + TenantRepository = Substitute.For(); + + ExistingTenants = new List + { + new Tenant(Guid.NewGuid(), "Tenant 1"), + new Tenant(Guid.NewGuid(), "Tenant 2"), + new Tenant(Guid.NewGuid(), "Tenant 3"), + }; + + TenantRepository.GetAllNoTracking().Returns(ExistingTenants.BuildMock()); + + TenantsApiImplementation = new(TenantRepository); + } +} \ No newline at end of file diff --git a/CleanArchitecture.gRPC.Tests/Fixtures/UserTestsFixture.cs b/CleanArchitecture.gRPC.Tests/Fixtures/UserTestFixture.cs similarity index 95% rename from CleanArchitecture.gRPC.Tests/Fixtures/UserTestsFixture.cs rename to CleanArchitecture.gRPC.Tests/Fixtures/UserTestFixture.cs index d0616f2..3954783 100644 --- a/CleanArchitecture.gRPC.Tests/Fixtures/UserTestsFixture.cs +++ b/CleanArchitecture.gRPC.Tests/Fixtures/UserTestFixture.cs @@ -9,9 +9,9 @@ using NSubstitute; namespace CleanArchitecture.gRPC.Tests.Fixtures; -public sealed class UserTestsFixture +public sealed class UserTestFixture { - public UserTestsFixture() + public UserTestFixture() { ExistingUsers = new List { diff --git a/CleanArchitecture.gRPC.Tests/Tenants/GetTenantsByIdsTests.cs b/CleanArchitecture.gRPC.Tests/Tenants/GetTenantsByIdsTests.cs new file mode 100644 index 0000000..5a60273 --- /dev/null +++ b/CleanArchitecture.gRPC.Tests/Tenants/GetTenantsByIdsTests.cs @@ -0,0 +1,72 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using CleanArchitecture.gRPC.Tests.Fixtures; +using CleanArchitecture.Proto.Tenants; +using FluentAssertions; +using Xunit; + +namespace CleanArchitecture.gRPC.Tests.Tenants; + +public sealed class GetTenantsByIdsTests : IClassFixture +{ + private readonly TenantTestFixture _fixture; + + public GetTenantsByIdsTests(TenantTestFixture fixture) + { + _fixture = fixture; + } + + [Fact] + public async Task Should_Get_Empty_List_If_No_Ids_Are_Given() + { + var result = await _fixture.TenantsApiImplementation.GetByIds( + SetupRequest(Enumerable.Empty()), + default!); + + result.Tenants.Should().HaveCount(0); + } + + [Fact] + public async Task? Should_Get_Requested_Tenants() + { + var nonExistingId = Guid.NewGuid(); + + var ids = _fixture.ExistingTenants + .Take(2) + .Select(tenant => tenant.Id) + .ToList(); + + ids.Add(nonExistingId); + + var result = await _fixture.TenantsApiImplementation.GetByIds( + SetupRequest(ids), + default!); + + result.Tenants.Should().HaveCount(2); + + foreach (var tenant in result.Tenants) + { + var tenantId = Guid.Parse(tenant.Id); + + tenantId.Should().NotBe(nonExistingId); + + var mockTenant = _fixture.ExistingTenants.First(t => t.Id == tenantId); + + mockTenant.Should().NotBeNull(); + + tenant.Name.Should().Be(mockTenant.Name); + } + } + + private static GetTenantsByIdsRequest SetupRequest(IEnumerable ids) + { + var request = new GetTenantsByIdsRequest(); + + request.Ids.AddRange(ids.Select(id => id.ToString())); + request.Ids.Add("Not a guid"); + + return request; + } +} \ No newline at end of file diff --git a/CleanArchitecture.gRPC.Tests/Users/GetUsersByIdsTests.cs b/CleanArchitecture.gRPC.Tests/Users/GetUsersByIdsTests.cs index 1c0e333..4744376 100644 --- a/CleanArchitecture.gRPC.Tests/Users/GetUsersByIdsTests.cs +++ b/CleanArchitecture.gRPC.Tests/Users/GetUsersByIdsTests.cs @@ -9,11 +9,11 @@ using Xunit; namespace CleanArchitecture.gRPC.Tests.Users; -public sealed class GetUsersByIdsTests : IClassFixture +public sealed class GetUsersByIdsTests : IClassFixture { - private readonly UserTestsFixture _fixture; + private readonly UserTestFixture _fixture; - public GetUsersByIdsTests(UserTestsFixture fixture) + public GetUsersByIdsTests(UserTestFixture fixture) { _fixture = fixture; } @@ -23,13 +23,13 @@ public sealed class GetUsersByIdsTests : IClassFixture { var result = await _fixture.UsersApiImplementation.GetByIds( SetupRequest(Enumerable.Empty()), - null!); + default!); result.Users.Should().HaveCount(0); } [Fact] - public async Task Should_Get_Requested_Asked_Ids() + public async Task Should_Get_Requested_Users() { var nonExistingId = Guid.NewGuid(); @@ -40,9 +40,10 @@ public sealed class GetUsersByIdsTests : IClassFixture ids.Add(nonExistingId); +// Todo: Use default instead of null everywhere var result = await _fixture.UsersApiImplementation.GetByIds( SetupRequest(ids), - null!); + default!); result.Users.Should().HaveCount(2); diff --git a/Readme.md b/Readme.md index e4f2ffa..f73b60d 100644 --- a/Readme.md +++ b/Readme.md @@ -2,7 +2,9 @@ ![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. +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 and Nsubstitute. + +The purpose of this project is to create a clean boilerplate for an API and to show how to implement specific features. ## Project Structure The project follows the Onion Architecture, which means that the codebase is organized into layers, with the domain model at the center and the outer layers dependent on the inner layers. @@ -20,6 +22,7 @@ The project uses the following dependencies: - **MediatR**: A lightweight library that provides a mediator pattern implementation for .NET. - **Entity Framework Core**: A modern object-relational mapper for .NET that provides data access to the application. - **FluentValidation**: A validation library that provides a fluent API for validating objects. +- **gRPC**: gRPC is an open-source remote procedure call framework that enables efficient communication between distributed systems using a variety of programming languages and protocols. ## Running the Project To run the project, follow these steps: From 61bcab6d7799b413629e3931877807843a1e4895 Mon Sep 17 00:00:00 2001 From: alex289 Date: Wed, 30 Aug 2023 23:36:48 +0200 Subject: [PATCH 06/10] chore: Code Cleanup --- .../CleanArchitecture.Api.csproj | 26 +++---- .../Controllers/TenantController.cs | 12 ++-- .../Extensions/ServiceCollectionExtension.cs | 9 ++- CleanArchitecture.Api/Models/DetailedError.cs | 6 +- .../Models/ResponseMessage.cs | 9 +-- CleanArchitecture.Api/Program.cs | 6 +- ...CleanArchitecture.Application.Tests.csproj | 14 ++-- .../Queries/QueryHandlerBaseFixture.cs | 6 +- .../Tenants/GetAllTenantsTestFixture.cs | 14 ++-- .../Tenants/GetTenantByIdTestFixture.cs | 12 ++-- .../Queries/Users/GetAllUsersTestFixture.cs | 28 ++++---- .../Queries/Users/GetUserByIdTestFixture.cs | 28 ++++---- .../Tenants/GetAllTenantsQueryHandlerTests.cs | 8 +-- .../Tenants/GetTenantByIdQueryHandlerTests.cs | 10 +-- .../CleanArchitecture.Application.csproj | 7 +- .../Extensions/ServiceCollectionExtension.cs | 5 +- .../Tenants/GetAll/GetAllTenantsQuery.cs | 2 +- .../GetTenantByIdQueryHandler.cs | 2 +- .../Services/TenantService.cs | 4 +- .../ViewModels/Tenants/TenantViewModel.cs | 2 +- .../gRPC/TenantsApiImplementation.cs | 4 +- .../CleanArchitecture.Domain.Tests.csproj | 12 ++-- .../CreateTenantCommandHandlerTests.cs | 8 +-- .../CreateTenantCommandTestFixture.cs | 12 ++-- .../CreateTenantCommandValidationTests.cs | 8 +-- .../DeleteTenantCommandHandlerTests.cs | 6 +- .../DeleteTenantCommandTestFixture.cs | 20 +++--- .../DeleteTenantCommandValidationTests.cs | 8 +-- .../UpdateTenantCommandHandlerTests.cs | 10 +-- .../UpdateTenantCommandTestFixture.cs | 15 ++-- .../UpdateTenantCommandValidationTests.cs | 8 +-- .../ChangePasswordCommandValidationTests.cs | 2 +- .../CreateUserCommandHandlerTests.cs | 10 +-- .../CreateUserCommandValidationTests.cs | 4 +- .../DeleteUserCommandValidationTests.cs | 2 +- .../LoginUserCommandValidationTests.cs | 2 +- .../UpdateUserCommandValidationTests.cs | 4 +- CleanArchitecture.Domain/ApiUser.cs | 3 + .../CleanArchitecture.Domain.csproj | 16 ++--- .../Commands/CommandHandlerBase.cs | 2 +- .../CreateTenant/CreateTenantCommand.cs | 6 +- .../CreateTenantCommandHandler.cs | 4 +- .../CreateTenantCommandValidation.cs | 4 +- .../DeleteTenant/DeleteTenantCommand.cs | 2 +- .../DeleteTenantCommandHandler.cs | 8 +-- .../DeleteTenantCommandValidation.cs | 2 +- .../UpdateTenant/UpdateTenantCommand.cs | 6 +- .../UpdateTenantCommandHandler.cs | 6 +- .../UpdateTenantCommandValidation.cs | 4 +- .../CreateUser/CreateUserCommandHandler.cs | 6 +- .../UpdateUser/UpdateUserCommandHandler.cs | 2 +- .../UpdateUser/UpdateUserCommandValidation.cs | 2 +- .../DomainEvents/Message.cs | 2 +- .../DomainEvents/StoredDomainEvent.cs | 15 ++-- .../StoredDomainNotification.cs | 25 +++---- CleanArchitecture.Domain/Entities/Tenant.cs | 10 +-- CleanArchitecture.Domain/Entities/User.cs | 25 ++++--- .../Events/Tenant/TenantCreatedEvent.cs | 4 +- .../Events/Tenant/TenantUpdatedEvent.cs | 4 +- .../Extensions/ServiceCollectionExtension.cs | 2 +- ...anArchitecture.Infrastructure.Tests.csproj | 10 +-- .../CleanArchitecture.Infrastructure.csproj | 12 ++-- .../StoredDomainEventConfiguration.cs | 2 +- .../StoredDomainNotificationConfiguration.cs | 2 +- .../Configurations/UserConfiguration.cs | 1 - .../Database/EventStoreDbContext.cs | 2 +- .../EventSourcing/EventStore.cs | 6 +- .../EventSourcing/EventStoreContext.cs | 15 ++-- .../EventSourcing/IEventStoreContext.cs | 2 +- .../Extensions/ServiceCollectionExtensions.cs | 18 ++--- .../GlobalSuppressions.cs | 12 +++- .../InMemoryBus.cs | 2 +- ...230701135523_AddDomainNotificationStore.cs | 68 +++++++++---------- .../20230701135441_AddEventStore.cs | 60 ++++++++-------- .../Repositories/BaseRepository.cs | 2 +- .../CleanArchitecture.IntegrationTests.csproj | 20 +++--- .../Controller/TenantControllerTests.cs | 66 +++++++++--------- .../Controller/UserControllerTests.cs | 6 +- .../Fixtures/TenantTestFixture.cs | 2 +- .../Fixtures/TestFixtureBase.cs | 3 +- .../gRPC/GetTenantsByIdsTestFixture.cs | 8 +-- .../Fixtures/gRPC/GetUsersByIdsTestFixture.cs | 6 +- .../CleanArchitectureWebApplicationFactory.cs | 14 ++-- .../gRPC/GetTenantsByIdsTests.cs | 4 +- .../CleanArchitecture.Proto.csproj | 14 ++-- .../CleanArchitecture.Shared.csproj | 8 +-- .../Users/UserViewModel.cs | 2 +- .../CleanArchitecture.gRPC.Tests.csproj | 16 ++--- .../Fixtures/TenantTestFixture.cs | 22 +++--- .../Tenants/GetTenantsByIdsTests.cs | 22 +++--- CleanArchitecture.gRPC/CleanArchitecture.cs | 17 ++--- .../CleanArchitecture.gRPC.csproj | 6 +- .../Contexts/TenantsContext.cs | 6 +- .../Contexts/UsersContext.cs | 2 +- .../Extensions/ServiceCollectionExtensions.cs | 4 +- CleanArchitecture.gRPC/ICleanArchitecture.cs | 2 +- .../Interfaces/IUsersContext.cs | 2 +- CleanArchitecture.gRPC/Models/GRPCSettings.cs | 2 +- 98 files changed, 487 insertions(+), 486 deletions(-) diff --git a/CleanArchitecture.Api/CleanArchitecture.Api.csproj b/CleanArchitecture.Api/CleanArchitecture.Api.csproj index 46e1257..ca184ec 100644 --- a/CleanArchitecture.Api/CleanArchitecture.Api.csproj +++ b/CleanArchitecture.Api/CleanArchitecture.Api.csproj @@ -7,26 +7,26 @@ - - - - - + + + + + all runtime; build; native; contentfiles; analyzers; buildtransitive - - - - + + + + - - - - + + + + diff --git a/CleanArchitecture.Api/Controllers/TenantController.cs b/CleanArchitecture.Api/Controllers/TenantController.cs index 639e2c8..0576122 100644 --- a/CleanArchitecture.Api/Controllers/TenantController.cs +++ b/CleanArchitecture.Api/Controllers/TenantController.cs @@ -18,14 +18,14 @@ namespace CleanArchitecture.Api.Controllers; public sealed class TenantController : ApiController { private readonly ITenantService _tenantService; - + public TenantController( INotificationHandler notifications, ITenantService tenantService) : base(notifications) { _tenantService = tenantService; } - + [HttpGet] [SwaggerOperation("Get a list of all tenants")] [SwaggerResponse(200, "Request successful", typeof(ResponseMessage>))] @@ -34,7 +34,7 @@ public sealed class TenantController : ApiController var tenants = await _tenantService.GetAllTenantsAsync(); return Response(tenants); } - + [HttpGet("{id:guid}")] [SwaggerOperation("Get a tenant by id")] [SwaggerResponse(200, "Request successful", typeof(ResponseMessage))] @@ -45,7 +45,7 @@ public sealed class TenantController : ApiController var tenant = await _tenantService.GetTenantByIdAsync(id, isDeleted); return Response(tenant); } - + [HttpPost] [SwaggerOperation("Create a new tenant")] [SwaggerResponse(200, "Request successful", typeof(ResponseMessage))] @@ -54,7 +54,7 @@ public sealed class TenantController : ApiController var tenantId = await _tenantService.CreateTenantAsync(tenant); return Response(tenantId); } - + [HttpPut] [SwaggerOperation("Update an existing tenant")] [SwaggerResponse(200, "Request successful", typeof(ResponseMessage))] @@ -63,7 +63,7 @@ public sealed class TenantController : ApiController await _tenantService.UpdateTenantAsync(tenant); return Response(tenant); } - + [HttpDelete("{id:guid}")] [SwaggerOperation("Delete an existing tenant")] [SwaggerResponse(200, "Request successful", typeof(ResponseMessage))] diff --git a/CleanArchitecture.Api/Extensions/ServiceCollectionExtension.cs b/CleanArchitecture.Api/Extensions/ServiceCollectionExtension.cs index a71b3ed..7224bcc 100644 --- a/CleanArchitecture.Api/Extensions/ServiceCollectionExtension.cs +++ b/CleanArchitecture.Api/Extensions/ServiceCollectionExtension.cs @@ -60,9 +60,12 @@ public static class ServiceCollectionExtension services.AddHttpContextAccessor(); services.AddAuthentication( - options => { options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme; }) + options => { options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme; }) .AddJwtBearer( - jwtOptions => { jwtOptions.TokenValidationParameters = CreateTokenValidationParameters(configuration); }); + jwtOptions => + { + jwtOptions.TokenValidationParameters = CreateTokenValidationParameters(configuration); + }); services .AddOptions() @@ -90,4 +93,4 @@ public static class ServiceCollectionExtension return result; } -} +} \ No newline at end of file diff --git a/CleanArchitecture.Api/Models/DetailedError.cs b/CleanArchitecture.Api/Models/DetailedError.cs index a4f3bd7..f7a1fed 100644 --- a/CleanArchitecture.Api/Models/DetailedError.cs +++ b/CleanArchitecture.Api/Models/DetailedError.cs @@ -4,9 +4,7 @@ namespace CleanArchitecture.Api.Models; public sealed class DetailedError { - [JsonPropertyName("code")] - public string Code { get; init; } = string.Empty; + [JsonPropertyName("code")] public string Code { get; init; } = string.Empty; - [JsonPropertyName("data")] - public object? Data { get; init; } + [JsonPropertyName("data")] public object? Data { get; init; } } \ No newline at end of file diff --git a/CleanArchitecture.Api/Models/ResponseMessage.cs b/CleanArchitecture.Api/Models/ResponseMessage.cs index e425f55..2f971bd 100644 --- a/CleanArchitecture.Api/Models/ResponseMessage.cs +++ b/CleanArchitecture.Api/Models/ResponseMessage.cs @@ -6,15 +6,12 @@ namespace CleanArchitecture.Api.Models; public sealed class ResponseMessage { - [JsonPropertyName("success")] - public bool Success { get; init; } + [JsonPropertyName("success")] public bool Success { get; init; } - [JsonPropertyName("errors")] - public IEnumerable? Errors { get; init; } + [JsonPropertyName("errors")] public IEnumerable? Errors { get; init; } [JsonPropertyName("detailedErrors")] public IEnumerable DetailedErrors { get; init; } = Enumerable.Empty(); - [JsonPropertyName("data")] - public T? Data { get; init; } + [JsonPropertyName("data")] public T? Data { get; init; } } \ No newline at end of file diff --git a/CleanArchitecture.Api/Program.cs b/CleanArchitecture.Api/Program.cs index f940491..b9819e7 100644 --- a/CleanArchitecture.Api/Program.cs +++ b/CleanArchitecture.Api/Program.cs @@ -61,9 +61,9 @@ var app = builder.Build(); using (var scope = app.Services.CreateScope()) { var services = scope.ServiceProvider; - ApplicationDbContext appDbContext = services.GetRequiredService(); - EventStoreDbContext storeDbContext = services.GetRequiredService(); - DomainNotificationStoreDbContext domainStoreDbContext = services.GetRequiredService(); + var appDbContext = services.GetRequiredService(); + var storeDbContext = services.GetRequiredService(); + var domainStoreDbContext = services.GetRequiredService(); appDbContext.EnsureMigrationsApplied(); diff --git a/CleanArchitecture.Application.Tests/CleanArchitecture.Application.Tests.csproj b/CleanArchitecture.Application.Tests/CleanArchitecture.Application.Tests.csproj index 225be5b..6f2f43b 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 63c7502..d04b652 100644 --- a/CleanArchitecture.Application.Tests/Fixtures/Queries/QueryHandlerBaseFixture.cs +++ b/CleanArchitecture.Application.Tests/Fixtures/Queries/QueryHandlerBaseFixture.cs @@ -11,9 +11,9 @@ public class QueryHandlerBaseFixture public QueryHandlerBaseFixture VerifyExistingNotification(string key, string errorCode, string message) { Bus.Received(1).RaiseEventAsync(Arg.Is(notification => - notification.Key == key && - notification.Code == errorCode && - notification.Value == message)); + notification.Key == key && + notification.Code == errorCode && + notification.Value == message)); return this; } diff --git a/CleanArchitecture.Application.Tests/Fixtures/Queries/Tenants/GetAllTenantsTestFixture.cs b/CleanArchitecture.Application.Tests/Fixtures/Queries/Tenants/GetAllTenantsTestFixture.cs index 90763e6..ef21f15 100644 --- a/CleanArchitecture.Application.Tests/Fixtures/Queries/Tenants/GetAllTenantsTestFixture.cs +++ b/CleanArchitecture.Application.Tests/Fixtures/Queries/Tenants/GetAllTenantsTestFixture.cs @@ -10,16 +10,16 @@ namespace CleanArchitecture.Application.Tests.Fixtures.Queries.Tenants; public sealed class GetAllTenantsTestFixture : QueryHandlerBaseFixture { - public GetAllTenantsQueryHandler QueryHandler { get; } - private ITenantRepository TenantRepository { get; } - public GetAllTenantsTestFixture() { TenantRepository = Substitute.For(); - - QueryHandler = new(TenantRepository); + + QueryHandler = new GetAllTenantsQueryHandler(TenantRepository); } - + + public GetAllTenantsQueryHandler QueryHandler { get; } + private ITenantRepository TenantRepository { get; } + public Tenant SetupTenant(bool deleted = false) { var tenant = new Tenant(Guid.NewGuid(), "Tenant 1"); @@ -31,7 +31,7 @@ public sealed class GetAllTenantsTestFixture : QueryHandlerBaseFixture var tenantList = new List { tenant }.BuildMock(); TenantRepository.GetAllNoTracking().Returns(tenantList); - + return tenant; } } \ No newline at end of file diff --git a/CleanArchitecture.Application.Tests/Fixtures/Queries/Tenants/GetTenantByIdTestFixture.cs b/CleanArchitecture.Application.Tests/Fixtures/Queries/Tenants/GetTenantByIdTestFixture.cs index 4ba31ef..ac12b58 100644 --- a/CleanArchitecture.Application.Tests/Fixtures/Queries/Tenants/GetTenantByIdTestFixture.cs +++ b/CleanArchitecture.Application.Tests/Fixtures/Queries/Tenants/GetTenantByIdTestFixture.cs @@ -10,18 +10,18 @@ namespace CleanArchitecture.Application.Tests.Fixtures.Queries.Tenants; public sealed class GetTenantByIdTestFixture : QueryHandlerBaseFixture { - public GetTenantByIdQueryHandler QueryHandler { get; } - private ITenantRepository TenantRepository { get; } - public GetTenantByIdTestFixture() { TenantRepository = Substitute.For(); - - QueryHandler = new( + + QueryHandler = new GetTenantByIdQueryHandler( TenantRepository, Bus); } + public GetTenantByIdQueryHandler QueryHandler { get; } + private ITenantRepository TenantRepository { get; } + public Tenant SetupTenant(bool deleted = false) { var tenant = new Tenant(Guid.NewGuid(), "Tenant 1"); @@ -33,7 +33,7 @@ public sealed class GetTenantByIdTestFixture : QueryHandlerBaseFixture var tenantList = new List { tenant }.BuildMock(); TenantRepository.GetAllNoTracking().Returns(tenantList); - + return tenant; } } \ No newline at end of file diff --git a/CleanArchitecture.Application.Tests/Fixtures/Queries/Users/GetAllUsersTestFixture.cs b/CleanArchitecture.Application.Tests/Fixtures/Queries/Users/GetAllUsersTestFixture.cs index 027e8ff..efdabee 100644 --- a/CleanArchitecture.Application.Tests/Fixtures/Queries/Users/GetAllUsersTestFixture.cs +++ b/CleanArchitecture.Application.Tests/Fixtures/Queries/Users/GetAllUsersTestFixture.cs @@ -24,13 +24,13 @@ public sealed class GetAllUsersTestFixture : QueryHandlerBaseFixture public void SetupUserAsync() { var user = new User( - ExistingUserId, - Guid.NewGuid(), - "max@mustermann.com", - "Max", - "Mustermann", - "Password", - UserRole.User); + ExistingUserId, + Guid.NewGuid(), + "max@mustermann.com", + "Max", + "Mustermann", + "Password", + UserRole.User); var query = new[] { user }.BuildMock(); @@ -40,13 +40,13 @@ public sealed class GetAllUsersTestFixture : QueryHandlerBaseFixture public void SetupDeletedUserAsync() { var user = new User( - ExistingUserId, - Guid.NewGuid(), - "max@mustermann.com", - "Max", - "Mustermann", - "Password", - UserRole.User); + ExistingUserId, + Guid.NewGuid(), + "max@mustermann.com", + "Max", + "Mustermann", + "Password", + UserRole.User); user.Delete(); diff --git a/CleanArchitecture.Application.Tests/Fixtures/Queries/Users/GetUserByIdTestFixture.cs b/CleanArchitecture.Application.Tests/Fixtures/Queries/Users/GetUserByIdTestFixture.cs index 6f306d6..11b407f 100644 --- a/CleanArchitecture.Application.Tests/Fixtures/Queries/Users/GetUserByIdTestFixture.cs +++ b/CleanArchitecture.Application.Tests/Fixtures/Queries/Users/GetUserByIdTestFixture.cs @@ -25,13 +25,13 @@ public sealed class GetUserByIdTestFixture : QueryHandlerBaseFixture public void SetupUserAsync() { var user = new User( - ExistingUserId, - Guid.NewGuid(), - "max@mustermann.com", - "Max", - "Mustermann", - "Password", - UserRole.User); + ExistingUserId, + Guid.NewGuid(), + "max@mustermann.com", + "Max", + "Mustermann", + "Password", + UserRole.User); var query = new[] { user }.BuildMock(); @@ -41,13 +41,13 @@ public sealed class GetUserByIdTestFixture : QueryHandlerBaseFixture public void SetupDeletedUserAsync() { var user = new User( - ExistingUserId, - Guid.NewGuid(), - "max@mustermann.com", - "Max", - "Mustermann", - "Password", - UserRole.User); + ExistingUserId, + Guid.NewGuid(), + "max@mustermann.com", + "Max", + "Mustermann", + "Password", + UserRole.User); user.Delete(); diff --git a/CleanArchitecture.Application.Tests/Queries/Tenants/GetAllTenantsQueryHandlerTests.cs b/CleanArchitecture.Application.Tests/Queries/Tenants/GetAllTenantsQueryHandlerTests.cs index 882eedf..6940687 100644 --- a/CleanArchitecture.Application.Tests/Queries/Tenants/GetAllTenantsQueryHandlerTests.cs +++ b/CleanArchitecture.Application.Tests/Queries/Tenants/GetAllTenantsQueryHandlerTests.cs @@ -10,12 +10,12 @@ namespace CleanArchitecture.Application.Tests.Queries.Tenants; public sealed class GetAllTenantsQueryHandlerTests { private readonly GetAllTenantsTestFixture _fixture = new(); - + [Fact] public async Task Should_Get_Existing_Tenant() { var tenant = _fixture.SetupTenant(); - + var result = await _fixture.QueryHandler.Handle( new GetAllTenantsQuery(), default); @@ -24,12 +24,12 @@ public sealed class GetAllTenantsQueryHandlerTests tenant.Should().BeEquivalentTo(result.First()); } - + [Fact] public async Task Should_Not_Get_Deleted_Tenant() { _fixture.SetupTenant(true); - + var result = await _fixture.QueryHandler.Handle( new GetAllTenantsQuery(), default); diff --git a/CleanArchitecture.Application.Tests/Queries/Tenants/GetTenantByIdQueryHandlerTests.cs b/CleanArchitecture.Application.Tests/Queries/Tenants/GetTenantByIdQueryHandlerTests.cs index 222d456..230864b 100644 --- a/CleanArchitecture.Application.Tests/Queries/Tenants/GetTenantByIdQueryHandlerTests.cs +++ b/CleanArchitecture.Application.Tests/Queries/Tenants/GetTenantByIdQueryHandlerTests.cs @@ -15,7 +15,7 @@ public sealed class GetTenantByIdQueryHandlerTests public async Task Should_Get_Existing_Tenant() { var tenant = _fixture.SetupTenant(); - + var result = await _fixture.QueryHandler.Handle( new GetTenantByIdQuery(tenant.Id, false), default); @@ -24,12 +24,12 @@ public sealed class GetTenantByIdQueryHandlerTests tenant.Should().BeEquivalentTo(result); } - + [Fact] public async Task Should_Get_Deleted_Tenant() { var tenant = _fixture.SetupTenant(true); - + var result = await _fixture.QueryHandler.Handle( new GetTenantByIdQuery(tenant.Id, true), default); @@ -38,12 +38,12 @@ public sealed class GetTenantByIdQueryHandlerTests tenant.Should().BeEquivalentTo(result); } - + [Fact] public async Task Should_Not_Get_Deleted_Tenant() { var tenant = _fixture.SetupTenant(true); - + var result = await _fixture.QueryHandler.Handle( new GetTenantByIdQuery(tenant.Id, false), default); diff --git a/CleanArchitecture.Application/CleanArchitecture.Application.csproj b/CleanArchitecture.Application/CleanArchitecture.Application.csproj index 2c7102f..3b320a5 100644 --- a/CleanArchitecture.Application/CleanArchitecture.Application.csproj +++ b/CleanArchitecture.Application/CleanArchitecture.Application.csproj @@ -6,14 +6,13 @@ - + - - + + - diff --git a/CleanArchitecture.Application/Extensions/ServiceCollectionExtension.cs b/CleanArchitecture.Application/Extensions/ServiceCollectionExtension.cs index 03d5044..9da4c3b 100644 --- a/CleanArchitecture.Application/Extensions/ServiceCollectionExtension.cs +++ b/CleanArchitecture.Application/Extensions/ServiceCollectionExtension.cs @@ -30,8 +30,9 @@ public static class ServiceCollectionExtension // Tenant services.AddScoped, GetTenantByIdQueryHandler>(); - services.AddScoped>, GetAllTenantsQueryHandler>(); - + services + .AddScoped>, GetAllTenantsQueryHandler>(); + return services; } } \ No newline at end of file diff --git a/CleanArchitecture.Application/Queries/Tenants/GetAll/GetAllTenantsQuery.cs b/CleanArchitecture.Application/Queries/Tenants/GetAll/GetAllTenantsQuery.cs index 09e81a7..e87ddec 100644 --- a/CleanArchitecture.Application/Queries/Tenants/GetAll/GetAllTenantsQuery.cs +++ b/CleanArchitecture.Application/Queries/Tenants/GetAll/GetAllTenantsQuery.cs @@ -4,4 +4,4 @@ using MediatR; namespace CleanArchitecture.Application.Queries.Tenants.GetAll; -public sealed record GetAllTenantsQuery() : IRequest>; \ No newline at end of file +public sealed record GetAllTenantsQuery : IRequest>; \ No newline at end of file diff --git a/CleanArchitecture.Application/Queries/Tenants/GetTenantById/GetTenantByIdQueryHandler.cs b/CleanArchitecture.Application/Queries/Tenants/GetTenantById/GetTenantByIdQueryHandler.cs index 9157fe4..2e22455 100644 --- a/CleanArchitecture.Application/Queries/Tenants/GetTenantById/GetTenantByIdQueryHandler.cs +++ b/CleanArchitecture.Application/Queries/Tenants/GetTenantById/GetTenantByIdQueryHandler.cs @@ -14,8 +14,8 @@ namespace CleanArchitecture.Application.Queries.Tenants.GetTenantById; public sealed class GetTenantByIdQueryHandler : IRequestHandler { - private readonly ITenantRepository _tenantRepository; private readonly IMediatorHandler _bus; + private readonly ITenantRepository _tenantRepository; public GetTenantByIdQueryHandler(ITenantRepository tenantRepository, IMediatorHandler bus) { diff --git a/CleanArchitecture.Application/Services/TenantService.cs b/CleanArchitecture.Application/Services/TenantService.cs index 62279c0..c10fe20 100644 --- a/CleanArchitecture.Application/Services/TenantService.cs +++ b/CleanArchitecture.Application/Services/TenantService.cs @@ -24,11 +24,11 @@ public sealed class TenantService : ITenantService public async Task CreateTenantAsync(CreateTenantViewModel tenant) { var tenantId = Guid.NewGuid(); - + await _bus.SendCommandAsync(new CreateTenantCommand( tenantId, tenant.Name)); - + return tenantId; } diff --git a/CleanArchitecture.Application/ViewModels/Tenants/TenantViewModel.cs b/CleanArchitecture.Application/ViewModels/Tenants/TenantViewModel.cs index 586768f..af6bb43 100644 --- a/CleanArchitecture.Application/ViewModels/Tenants/TenantViewModel.cs +++ b/CleanArchitecture.Application/ViewModels/Tenants/TenantViewModel.cs @@ -11,7 +11,7 @@ public sealed class TenantViewModel public Guid Id { get; set; } public string Name { get; set; } = string.Empty; public IEnumerable Users { get; set; } = new List(); - + public static TenantViewModel FromTenant(Tenant tenant) { return new TenantViewModel diff --git a/CleanArchitecture.Application/gRPC/TenantsApiImplementation.cs b/CleanArchitecture.Application/gRPC/TenantsApiImplementation.cs index d356c9e..414206c 100644 --- a/CleanArchitecture.Application/gRPC/TenantsApiImplementation.cs +++ b/CleanArchitecture.Application/gRPC/TenantsApiImplementation.cs @@ -23,7 +23,7 @@ public sealed class TenantsApiImplementation : TenantsApi.TenantsApiBase ServerCallContext context) { var idsAsGuids = new List(request.Ids.Count); - + foreach (var id in request.Ids) { if (Guid.TryParse(id, out var parsed)) @@ -44,7 +44,7 @@ public sealed class TenantsApiImplementation : TenantsApi.TenantsApiBase .ToListAsync(); var result = new GetTenantsByIdsResult(); - + result.Tenants.AddRange(tenants); return result; diff --git a/CleanArchitecture.Domain.Tests/CleanArchitecture.Domain.Tests.csproj b/CleanArchitecture.Domain.Tests/CleanArchitecture.Domain.Tests.csproj index 9a66f90..ce47c49 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/Tenant/CreateTenant/CreateTenantCommandHandlerTests.cs b/CleanArchitecture.Domain.Tests/CommandHandler/Tenant/CreateTenant/CreateTenantCommandHandlerTests.cs index 77e2038..eb8d848 100644 --- a/CleanArchitecture.Domain.Tests/CommandHandler/Tenant/CreateTenant/CreateTenantCommandHandlerTests.cs +++ b/CleanArchitecture.Domain.Tests/CommandHandler/Tenant/CreateTenant/CreateTenantCommandHandlerTests.cs @@ -26,12 +26,12 @@ public sealed class CreateTenantCommandHandlerTests x.AggregateId == command.AggregateId && x.Name == command.Name); } - + [Fact] public void Should_Not_Create_Tenant_Insufficient_Permissions() { _fixture.SetupUser(); - + var command = new CreateTenantCommand( Guid.NewGuid(), "Test Tenant"); @@ -46,14 +46,14 @@ public sealed class CreateTenantCommandHandlerTests ErrorCodes.InsufficientPermissions, $"No permission to create tenant {command.AggregateId}"); } - + [Fact] public void Should_Not_Create_Tenant_Already_Exists() { var command = new CreateTenantCommand( Guid.NewGuid(), "Test Tenant"); - + _fixture.SetupExistingTenant(command.AggregateId); _fixture.CommandHandler.Handle(command, default!).Wait(); diff --git a/CleanArchitecture.Domain.Tests/CommandHandler/Tenant/CreateTenant/CreateTenantCommandTestFixture.cs b/CleanArchitecture.Domain.Tests/CommandHandler/Tenant/CreateTenant/CreateTenantCommandTestFixture.cs index 9c9a48c..d2ca1f2 100644 --- a/CleanArchitecture.Domain.Tests/CommandHandler/Tenant/CreateTenant/CreateTenantCommandTestFixture.cs +++ b/CleanArchitecture.Domain.Tests/CommandHandler/Tenant/CreateTenant/CreateTenantCommandTestFixture.cs @@ -8,15 +8,11 @@ namespace CleanArchitecture.Domain.Tests.CommandHandler.Tenant.CreateTenant; public sealed class CreateTenantCommandTestFixture : CommandHandlerFixtureBase { - public CreateTenantCommandHandler CommandHandler { get;} - - private ITenantRepository TenantRepository { get; } - public CreateTenantCommandTestFixture() { TenantRepository = Substitute.For(); - - CommandHandler = new( + + CommandHandler = new CreateTenantCommandHandler( Bus, UnitOfWork, NotificationHandler, @@ -24,6 +20,10 @@ public sealed class CreateTenantCommandTestFixture : CommandHandlerFixtureBase User); } + public CreateTenantCommandHandler CommandHandler { get; } + + private ITenantRepository TenantRepository { get; } + public void SetupUser() { User.GetUserRole().Returns(UserRole.User); diff --git a/CleanArchitecture.Domain.Tests/CommandHandler/Tenant/CreateTenant/CreateTenantCommandValidationTests.cs b/CleanArchitecture.Domain.Tests/CommandHandler/Tenant/CreateTenant/CreateTenantCommandValidationTests.cs index 25aec37..2156932 100644 --- a/CleanArchitecture.Domain.Tests/CommandHandler/Tenant/CreateTenant/CreateTenantCommandValidationTests.cs +++ b/CleanArchitecture.Domain.Tests/CommandHandler/Tenant/CreateTenant/CreateTenantCommandValidationTests.cs @@ -11,7 +11,7 @@ public sealed class CreateTenantCommandValidationTests : public CreateTenantCommandValidationTests() : base(new CreateTenantCommandValidation()) { } - + [Fact] public void Should_Be_Valid() { @@ -19,7 +19,7 @@ public sealed class CreateTenantCommandValidationTests : ShouldBeValid(command); } - + [Fact] public void Should_Be_Invalid_For_Empty_Tenant_Id() { @@ -30,7 +30,7 @@ public sealed class CreateTenantCommandValidationTests : DomainErrorCodes.Tenant.TenantEmptyId, "Tenant id may not be empty"); } - + [Fact] public void Should_Be_Invalid_For_Empty_Tenant_Name() { @@ -46,7 +46,7 @@ public sealed class CreateTenantCommandValidationTests : Guid? id = null, string? name = null) { - return new( + return new CreateTenantCommand( id ?? Guid.NewGuid(), name ?? "Test Tenant"); } diff --git a/CleanArchitecture.Domain.Tests/CommandHandler/Tenant/DeleteTenant/DeleteTenantCommandHandlerTests.cs b/CleanArchitecture.Domain.Tests/CommandHandler/Tenant/DeleteTenant/DeleteTenantCommandHandlerTests.cs index 27b5a2d..cfd6851 100644 --- a/CleanArchitecture.Domain.Tests/CommandHandler/Tenant/DeleteTenant/DeleteTenantCommandHandlerTests.cs +++ b/CleanArchitecture.Domain.Tests/CommandHandler/Tenant/DeleteTenant/DeleteTenantCommandHandlerTests.cs @@ -16,7 +16,7 @@ public sealed class DeleteTenantCommandHandlerTests var tenant = _fixture.SetupTenant(); var command = new DeleteTenantCommand(tenant.Id); - + _fixture.CommandHandler.Handle(command, default).Wait(); _fixture @@ -24,14 +24,14 @@ public sealed class DeleteTenantCommandHandlerTests .VerifyCommit() .VerifyRaisedEvent(x => x.AggregateId == tenant.Id); } - + [Fact] public void Should_Not_Delete_Non_Existing_Tenant() { _fixture.SetupTenant(); var command = new DeleteTenantCommand(Guid.NewGuid()); - + _fixture.CommandHandler.Handle(command, default).Wait(); _fixture diff --git a/CleanArchitecture.Domain.Tests/CommandHandler/Tenant/DeleteTenant/DeleteTenantCommandTestFixture.cs b/CleanArchitecture.Domain.Tests/CommandHandler/Tenant/DeleteTenant/DeleteTenantCommandTestFixture.cs index 9ed5c2c..1b8367a 100644 --- a/CleanArchitecture.Domain.Tests/CommandHandler/Tenant/DeleteTenant/DeleteTenantCommandTestFixture.cs +++ b/CleanArchitecture.Domain.Tests/CommandHandler/Tenant/DeleteTenant/DeleteTenantCommandTestFixture.cs @@ -7,17 +7,12 @@ namespace CleanArchitecture.Domain.Tests.CommandHandler.Tenant.DeleteTenant; public sealed class DeleteTenantCommandTestFixture : CommandHandlerFixtureBase { - public DeleteTenantCommandHandler CommandHandler { get;} - - private ITenantRepository TenantRepository { get; } - private IUserRepository UserRepository { get; } - public DeleteTenantCommandTestFixture() { TenantRepository = Substitute.For(); UserRepository = Substitute.For(); - - CommandHandler = new( + + CommandHandler = new DeleteTenantCommandHandler( Bus, UnitOfWork, NotificationHandler, @@ -26,14 +21,19 @@ public sealed class DeleteTenantCommandTestFixture : CommandHandlerFixtureBase User); } + public DeleteTenantCommandHandler CommandHandler { get; } + + private ITenantRepository TenantRepository { get; } + private IUserRepository UserRepository { get; } + public Entities.Tenant SetupTenant() { var tenant = new Entities.Tenant(Guid.NewGuid(), "TestTenant"); - + TenantRepository .GetByIdAsync(Arg.Is(y => y == tenant.Id)) .Returns(tenant); - + return tenant; - } + } } \ No newline at end of file diff --git a/CleanArchitecture.Domain.Tests/CommandHandler/Tenant/DeleteTenant/DeleteTenantCommandValidationTests.cs b/CleanArchitecture.Domain.Tests/CommandHandler/Tenant/DeleteTenant/DeleteTenantCommandValidationTests.cs index 48d3877..b36e294 100644 --- a/CleanArchitecture.Domain.Tests/CommandHandler/Tenant/DeleteTenant/DeleteTenantCommandValidationTests.cs +++ b/CleanArchitecture.Domain.Tests/CommandHandler/Tenant/DeleteTenant/DeleteTenantCommandValidationTests.cs @@ -11,7 +11,7 @@ public sealed class DeleteTenantCommandValidationTests : public DeleteTenantCommandValidationTests() : base(new DeleteTenantCommandValidation()) { } - + [Fact] public void Should_Be_Valid() { @@ -19,7 +19,7 @@ public sealed class DeleteTenantCommandValidationTests : ShouldBeValid(command); } - + [Fact] public void Should_Be_Invalid_For_Empty_Tenant_Id() { @@ -30,9 +30,9 @@ public sealed class DeleteTenantCommandValidationTests : DomainErrorCodes.Tenant.TenantEmptyId, "Tenant id may not be empty"); } - + private static DeleteTenantCommand CreateTestCommand(Guid? tenantId = null) { - return new(tenantId ?? Guid.NewGuid()); + return new DeleteTenantCommand(tenantId ?? Guid.NewGuid()); } } \ No newline at end of file diff --git a/CleanArchitecture.Domain.Tests/CommandHandler/Tenant/UpdateTenant/UpdateTenantCommandHandlerTests.cs b/CleanArchitecture.Domain.Tests/CommandHandler/Tenant/UpdateTenant/UpdateTenantCommandHandlerTests.cs index 8fa4424..e391a89 100644 --- a/CleanArchitecture.Domain.Tests/CommandHandler/Tenant/UpdateTenant/UpdateTenantCommandHandlerTests.cs +++ b/CleanArchitecture.Domain.Tests/CommandHandler/Tenant/UpdateTenant/UpdateTenantCommandHandlerTests.cs @@ -16,7 +16,7 @@ public sealed class UpdateTenantCommandHandlerTests var command = new UpdateTenantCommand( Guid.NewGuid(), "Tenant Name"); - + _fixture.SetupExistingTenant(command.AggregateId); _fixture.CommandHandler.Handle(command, default!).Wait(); @@ -28,14 +28,14 @@ public sealed class UpdateTenantCommandHandlerTests x.AggregateId == command.AggregateId && x.Name == command.Name); } - + [Fact] public void Should_Not_Update_Tenant_Insufficient_Permissions() { var command = new UpdateTenantCommand( Guid.NewGuid(), "Tenant Name"); - + _fixture.SetupUser(); _fixture.CommandHandler.Handle(command, default!).Wait(); @@ -48,14 +48,14 @@ public sealed class UpdateTenantCommandHandlerTests ErrorCodes.InsufficientPermissions, $"No permission to update tenant {command.AggregateId}"); } - + [Fact] public void Should_Not_Update_Tenant_Not_Existing() { var command = new UpdateTenantCommand( Guid.NewGuid(), "Tenant Name"); - + _fixture.CommandHandler.Handle(command, default!).Wait(); _fixture diff --git a/CleanArchitecture.Domain.Tests/CommandHandler/Tenant/UpdateTenant/UpdateTenantCommandTestFixture.cs b/CleanArchitecture.Domain.Tests/CommandHandler/Tenant/UpdateTenant/UpdateTenantCommandTestFixture.cs index 82879d6..dbd1695 100644 --- a/CleanArchitecture.Domain.Tests/CommandHandler/Tenant/UpdateTenant/UpdateTenantCommandTestFixture.cs +++ b/CleanArchitecture.Domain.Tests/CommandHandler/Tenant/UpdateTenant/UpdateTenantCommandTestFixture.cs @@ -1,7 +1,6 @@ using System; using CleanArchitecture.Domain.Commands.Tenants.UpdateTenant; using CleanArchitecture.Domain.Enums; -using CleanArchitecture.Domain.Interfaces; using CleanArchitecture.Domain.Interfaces.Repositories; using NSubstitute; @@ -9,22 +8,22 @@ namespace CleanArchitecture.Domain.Tests.CommandHandler.Tenant.UpdateTenant; public sealed class UpdateTenantCommandTestFixture : CommandHandlerFixtureBase { - public UpdateTenantCommandHandler CommandHandler { get;} - - private ITenantRepository TenantRepository { get; } - public UpdateTenantCommandTestFixture() { TenantRepository = Substitute.For(); - - CommandHandler = new( + + CommandHandler = new UpdateTenantCommandHandler( Bus, UnitOfWork, NotificationHandler, TenantRepository, User); } - + + public UpdateTenantCommandHandler CommandHandler { get; } + + private ITenantRepository TenantRepository { get; } + public void SetupUser() { User.GetUserRole().Returns(UserRole.User); diff --git a/CleanArchitecture.Domain.Tests/CommandHandler/Tenant/UpdateTenant/UpdateTenantCommandValidationTests.cs b/CleanArchitecture.Domain.Tests/CommandHandler/Tenant/UpdateTenant/UpdateTenantCommandValidationTests.cs index 4aac523..c7c4f11 100644 --- a/CleanArchitecture.Domain.Tests/CommandHandler/Tenant/UpdateTenant/UpdateTenantCommandValidationTests.cs +++ b/CleanArchitecture.Domain.Tests/CommandHandler/Tenant/UpdateTenant/UpdateTenantCommandValidationTests.cs @@ -11,7 +11,7 @@ public sealed class UpdateTenantCommandValidationTests : public UpdateTenantCommandValidationTests() : base(new UpdateTenantCommandValidation()) { } - + [Fact] public void Should_Be_Valid() { @@ -19,7 +19,7 @@ public sealed class UpdateTenantCommandValidationTests : ShouldBeValid(command); } - + [Fact] public void Should_Be_Invalid_For_Empty_Tenant_Id() { @@ -30,7 +30,7 @@ public sealed class UpdateTenantCommandValidationTests : DomainErrorCodes.Tenant.TenantEmptyId, "Tenant id may not be empty"); } - + [Fact] public void Should_Be_Invalid_For_Empty_Tenant_Name() { @@ -46,7 +46,7 @@ public sealed class UpdateTenantCommandValidationTests : Guid? id = null, string? name = null) { - return new( + return new UpdateTenantCommand( id ?? Guid.NewGuid(), name ?? "Test Tenant"); } diff --git a/CleanArchitecture.Domain.Tests/CommandHandler/User/ChangePassword/ChangePasswordCommandValidationTests.cs b/CleanArchitecture.Domain.Tests/CommandHandler/User/ChangePassword/ChangePasswordCommandValidationTests.cs index 4a1a41a..b75d5f4 100644 --- a/CleanArchitecture.Domain.Tests/CommandHandler/User/ChangePassword/ChangePasswordCommandValidationTests.cs +++ b/CleanArchitecture.Domain.Tests/CommandHandler/User/ChangePassword/ChangePasswordCommandValidationTests.cs @@ -90,7 +90,7 @@ public sealed class ChangePasswordCommandValidationTests : private static ChangePasswordCommand CreateTestCommand( string? password = null, string? newPassword = null) { - return new( + return new ChangePasswordCommand( password ?? "z8]tnayvd5FNLU9:]AQm", newPassword ?? "z8]tnayvd5FNLU9:]AQw"); } diff --git a/CleanArchitecture.Domain.Tests/CommandHandler/User/CreateUser/CreateUserCommandHandlerTests.cs b/CleanArchitecture.Domain.Tests/CommandHandler/User/CreateUser/CreateUserCommandHandlerTests.cs index 5382f23..0151fd4 100644 --- a/CleanArchitecture.Domain.Tests/CommandHandler/User/CreateUser/CreateUserCommandHandlerTests.cs +++ b/CleanArchitecture.Domain.Tests/CommandHandler/User/CreateUser/CreateUserCommandHandlerTests.cs @@ -15,7 +15,7 @@ public sealed class CreateUserCommandHandlerTests { // Todo: Fix tests _fixture.SetupCurrentUser(); - + var user = _fixture.SetupUser(); _fixture.SetupTenant(user.TenantId); @@ -39,7 +39,7 @@ public sealed class CreateUserCommandHandlerTests public void Should_Not_Create_Already_Existing_User() { _fixture.SetupCurrentUser(); - + var user = _fixture.SetupUser(); var command = new CreateUserCommand( @@ -60,12 +60,12 @@ public sealed class CreateUserCommandHandlerTests DomainErrorCodes.User.UserAlreadyExists, $"There is already a user with Id {command.UserId}"); } - + [Fact] public void Should_Not_Create_User_Tenant_Does_Not_Exist() { _fixture.SetupCurrentUser(); - + _fixture.SetupUser(); var command = new CreateUserCommand( @@ -86,7 +86,7 @@ public sealed class CreateUserCommandHandlerTests ErrorCodes.ObjectNotFound, $"There is no tenant with Id {command.TenantId}"); } - + [Fact] public void Should_Not_Create_User_Insufficient_Permissions() { diff --git a/CleanArchitecture.Domain.Tests/CommandHandler/User/CreateUser/CreateUserCommandValidationTests.cs b/CleanArchitecture.Domain.Tests/CommandHandler/User/CreateUser/CreateUserCommandValidationTests.cs index 015b40d..05e3d7c 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.User.UserLongPassword); } - + [Fact] public void Should_Be_Invalid_For_Empty_Tenant_Id() { @@ -193,7 +193,7 @@ public sealed class CreateUserCommandValidationTests : string? lastName = null, string? password = null) { - return new( + return new CreateUserCommand( userId ?? Guid.NewGuid(), tenantId ?? Guid.NewGuid(), email ?? "test@email.com", diff --git a/CleanArchitecture.Domain.Tests/CommandHandler/User/DeleteUser/DeleteUserCommandValidationTests.cs b/CleanArchitecture.Domain.Tests/CommandHandler/User/DeleteUser/DeleteUserCommandValidationTests.cs index 9464154..8ea63cf 100644 --- a/CleanArchitecture.Domain.Tests/CommandHandler/User/DeleteUser/DeleteUserCommandValidationTests.cs +++ b/CleanArchitecture.Domain.Tests/CommandHandler/User/DeleteUser/DeleteUserCommandValidationTests.cs @@ -33,6 +33,6 @@ public sealed class DeleteUserCommandValidationTests : private static DeleteUserCommand CreateTestCommand(Guid? userId = null) { - return new(userId ?? Guid.NewGuid()); + return new DeleteUserCommand(userId ?? Guid.NewGuid()); } } \ 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 e6b4675..7b1ca35 100644 --- a/CleanArchitecture.Domain.Tests/CommandHandler/User/LoginUser/LoginUserCommandValidationTests.cs +++ b/CleanArchitecture.Domain.Tests/CommandHandler/User/LoginUser/LoginUserCommandValidationTests.cs @@ -125,7 +125,7 @@ public sealed class LoginUserCommandValidationTests : string? email = null, string? password = null) { - return new( + return new LoginUserCommand( email ?? "test@email.com", password ?? "Po=PF]PC6t.?8?ks)A6W"); } diff --git a/CleanArchitecture.Domain.Tests/CommandHandler/User/UpdateUser/UpdateUserCommandValidationTests.cs b/CleanArchitecture.Domain.Tests/CommandHandler/User/UpdateUser/UpdateUserCommandValidationTests.cs index 161aabc..42de98f 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 : DomainErrorCodes.User.UserLastNameExceedsMaxLength, $"LastName may not be longer than {MaxLengths.User.LastName} characters"); } - + [Fact] public void Should_Be_Invalid_For_Empty_Tenant_Id() { @@ -129,7 +129,7 @@ public sealed class UpdateUserCommandValidationTests : string? lastName = null, UserRole? role = null) { - return new( + return new UpdateUserCommand( userId ?? Guid.NewGuid(), email ?? "test@email.com", firstName ?? "test", diff --git a/CleanArchitecture.Domain/ApiUser.cs b/CleanArchitecture.Domain/ApiUser.cs index df1db8a..3e0ea81 100644 --- a/CleanArchitecture.Domain/ApiUser.cs +++ b/CleanArchitecture.Domain/ApiUser.cs @@ -59,17 +59,20 @@ public sealed class ApiUser : IUser { return _name; } + var identity = _httpContextAccessor.HttpContext?.User.Identity; if (identity is null) { _name = string.Empty; return string.Empty; } + if (!string.IsNullOrWhiteSpace(identity.Name)) { _name = identity.Name; return identity.Name; } + var claim = _httpContextAccessor.HttpContext!.User.Claims .FirstOrDefault(c => string.Equals(c.Type, ClaimTypes.Name, StringComparison.OrdinalIgnoreCase))? .Value; diff --git a/CleanArchitecture.Domain/CleanArchitecture.Domain.csproj b/CleanArchitecture.Domain/CleanArchitecture.Domain.csproj index 0d9cbb5..756c41c 100644 --- a/CleanArchitecture.Domain/CleanArchitecture.Domain.csproj +++ b/CleanArchitecture.Domain/CleanArchitecture.Domain.csproj @@ -6,14 +6,14 @@ - - - - - + + + + + - - - + + + diff --git a/CleanArchitecture.Domain/Commands/CommandHandlerBase.cs b/CleanArchitecture.Domain/Commands/CommandHandlerBase.cs index 6ab6b8f..e1f7ba0 100644 --- a/CleanArchitecture.Domain/Commands/CommandHandlerBase.cs +++ b/CleanArchitecture.Domain/Commands/CommandHandlerBase.cs @@ -9,9 +9,9 @@ namespace CleanArchitecture.Domain.Commands; public abstract class CommandHandlerBase { - protected readonly IMediatorHandler Bus; private readonly DomainNotificationHandler _notifications; private readonly IUnitOfWork _unitOfWork; + protected readonly IMediatorHandler Bus; protected CommandHandlerBase( IMediatorHandler bus, diff --git a/CleanArchitecture.Domain/Commands/Tenants/CreateTenant/CreateTenantCommand.cs b/CleanArchitecture.Domain/Commands/Tenants/CreateTenant/CreateTenantCommand.cs index 3a97121..febc727 100644 --- a/CleanArchitecture.Domain/Commands/Tenants/CreateTenant/CreateTenantCommand.cs +++ b/CleanArchitecture.Domain/Commands/Tenants/CreateTenant/CreateTenantCommand.cs @@ -5,14 +5,14 @@ namespace CleanArchitecture.Domain.Commands.Tenants.CreateTenant; public sealed class CreateTenantCommand : CommandBase { private static readonly CreateTenantCommandValidation s_validation = new(); - - public string Name { get; } - + public CreateTenantCommand(Guid tenantId, string name) : base(tenantId) { Name = name; } + public string Name { get; } + public override bool IsValid() { ValidationResult = s_validation.Validate(this); diff --git a/CleanArchitecture.Domain/Commands/Tenants/CreateTenant/CreateTenantCommandHandler.cs b/CleanArchitecture.Domain/Commands/Tenants/CreateTenant/CreateTenantCommandHandler.cs index c96c8f0..16d0821 100644 --- a/CleanArchitecture.Domain/Commands/Tenants/CreateTenant/CreateTenantCommandHandler.cs +++ b/CleanArchitecture.Domain/Commands/Tenants/CreateTenant/CreateTenantCommandHandler.cs @@ -34,7 +34,7 @@ public sealed class CreateTenantCommandHandler : CommandHandlerBase, { return; } - + if (_user.GetUserRole() != UserRole.Admin) { await NotifyAsync( @@ -60,7 +60,7 @@ public sealed class CreateTenantCommandHandler : CommandHandlerBase, var tenant = new Tenant( request.AggregateId, request.Name); - + _tenantRepository.Add(tenant); if (await CommitAsync()) diff --git a/CleanArchitecture.Domain/Commands/Tenants/CreateTenant/CreateTenantCommandValidation.cs b/CleanArchitecture.Domain/Commands/Tenants/CreateTenant/CreateTenantCommandValidation.cs index 4838663..9ac7602 100644 --- a/CleanArchitecture.Domain/Commands/Tenants/CreateTenant/CreateTenantCommandValidation.cs +++ b/CleanArchitecture.Domain/Commands/Tenants/CreateTenant/CreateTenantCommandValidation.cs @@ -11,7 +11,7 @@ public sealed class CreateTenantCommandValidation : AbstractValidator cmd.AggregateId) @@ -19,7 +19,7 @@ public sealed class CreateTenantCommandValidation : AbstractValidator cmd.Name) diff --git a/CleanArchitecture.Domain/Commands/Tenants/DeleteTenant/DeleteTenantCommand.cs b/CleanArchitecture.Domain/Commands/Tenants/DeleteTenant/DeleteTenantCommand.cs index 9e112ca..e83ddc8 100644 --- a/CleanArchitecture.Domain/Commands/Tenants/DeleteTenant/DeleteTenantCommand.cs +++ b/CleanArchitecture.Domain/Commands/Tenants/DeleteTenant/DeleteTenantCommand.cs @@ -5,7 +5,7 @@ namespace CleanArchitecture.Domain.Commands.Tenants.DeleteTenant; public sealed class DeleteTenantCommand : CommandBase { private static readonly DeleteTenantCommandValidation s_validation = new(); - + public DeleteTenantCommand(Guid tenantId) : base(tenantId) { } diff --git a/CleanArchitecture.Domain/Commands/Tenants/DeleteTenant/DeleteTenantCommandHandler.cs b/CleanArchitecture.Domain/Commands/Tenants/DeleteTenant/DeleteTenantCommandHandler.cs index 6c6b91a..3a57bed 100644 --- a/CleanArchitecture.Domain/Commands/Tenants/DeleteTenant/DeleteTenantCommandHandler.cs +++ b/CleanArchitecture.Domain/Commands/Tenants/DeleteTenant/DeleteTenantCommandHandler.cs @@ -15,8 +15,8 @@ public sealed class DeleteTenantCommandHandler : CommandHandlerBase, IRequestHandler { private readonly ITenantRepository _tenantRepository; - private readonly IUserRepository _userRepository; private readonly IUser _user; + private readonly IUserRepository _userRepository; public DeleteTenantCommandHandler( IMediatorHandler bus, @@ -37,7 +37,7 @@ public sealed class DeleteTenantCommandHandler : CommandHandlerBase, { return; } - + // Todo: Test following if (_user.GetUserRole() != UserRole.Admin) @@ -67,9 +67,9 @@ public sealed class DeleteTenantCommandHandler : CommandHandlerBase, var tenantUsers = _userRepository .GetAll() .Where(x => x.TenantId == request.AggregateId); - + _userRepository.RemoveRange(tenantUsers); - + _tenantRepository.Remove(tenant); if (await CommitAsync()) diff --git a/CleanArchitecture.Domain/Commands/Tenants/DeleteTenant/DeleteTenantCommandValidation.cs b/CleanArchitecture.Domain/Commands/Tenants/DeleteTenant/DeleteTenantCommandValidation.cs index dadddd8..7b80034 100644 --- a/CleanArchitecture.Domain/Commands/Tenants/DeleteTenant/DeleteTenantCommandValidation.cs +++ b/CleanArchitecture.Domain/Commands/Tenants/DeleteTenant/DeleteTenantCommandValidation.cs @@ -9,7 +9,7 @@ public sealed class DeleteTenantCommandValidation : AbstractValidator cmd.AggregateId) diff --git a/CleanArchitecture.Domain/Commands/Tenants/UpdateTenant/UpdateTenantCommand.cs b/CleanArchitecture.Domain/Commands/Tenants/UpdateTenant/UpdateTenantCommand.cs index 3962e9b..cafbfef 100644 --- a/CleanArchitecture.Domain/Commands/Tenants/UpdateTenant/UpdateTenantCommand.cs +++ b/CleanArchitecture.Domain/Commands/Tenants/UpdateTenant/UpdateTenantCommand.cs @@ -5,14 +5,14 @@ namespace CleanArchitecture.Domain.Commands.Tenants.UpdateTenant; public sealed class UpdateTenantCommand : CommandBase { private static readonly UpdateTenantCommandValidation s_validation = new(); - - public string Name { get; } - + public UpdateTenantCommand(Guid tenantId, string name) : base(tenantId) { Name = name; } + public string Name { get; } + public override bool IsValid() { ValidationResult = s_validation.Validate(this); diff --git a/CleanArchitecture.Domain/Commands/Tenants/UpdateTenant/UpdateTenantCommandHandler.cs b/CleanArchitecture.Domain/Commands/Tenants/UpdateTenant/UpdateTenantCommandHandler.cs index 4ef9929..dc1e076 100644 --- a/CleanArchitecture.Domain/Commands/Tenants/UpdateTenant/UpdateTenantCommandHandler.cs +++ b/CleanArchitecture.Domain/Commands/Tenants/UpdateTenant/UpdateTenantCommandHandler.cs @@ -33,7 +33,7 @@ public sealed class UpdateTenantCommandHandler : CommandHandlerBase, { return; } - + if (_user.GetUserRole() != UserRole.Admin) { await NotifyAsync( @@ -57,9 +57,9 @@ public sealed class UpdateTenantCommandHandler : CommandHandlerBase, return; } - + tenant.SetName(request.Name); - + if (await CommitAsync()) { await Bus.RaiseEventAsync(new TenantUpdatedEvent( diff --git a/CleanArchitecture.Domain/Commands/Tenants/UpdateTenant/UpdateTenantCommandValidation.cs b/CleanArchitecture.Domain/Commands/Tenants/UpdateTenant/UpdateTenantCommandValidation.cs index 905e638..c9abd29 100644 --- a/CleanArchitecture.Domain/Commands/Tenants/UpdateTenant/UpdateTenantCommandValidation.cs +++ b/CleanArchitecture.Domain/Commands/Tenants/UpdateTenant/UpdateTenantCommandValidation.cs @@ -11,7 +11,7 @@ public sealed class UpdateTenantCommandValidation : AbstractValidator cmd.AggregateId) @@ -19,7 +19,7 @@ public sealed class UpdateTenantCommandValidation : AbstractValidator cmd.Name) diff --git a/CleanArchitecture.Domain/Commands/Users/CreateUser/CreateUserCommandHandler.cs b/CleanArchitecture.Domain/Commands/Users/CreateUser/CreateUserCommandHandler.cs index 4407656..8eb5388 100644 --- a/CleanArchitecture.Domain/Commands/Users/CreateUser/CreateUserCommandHandler.cs +++ b/CleanArchitecture.Domain/Commands/Users/CreateUser/CreateUserCommandHandler.cs @@ -15,9 +15,9 @@ namespace CleanArchitecture.Domain.Commands.Users.CreateUser; public sealed class CreateUserCommandHandler : CommandHandlerBase, IRequestHandler { - private readonly IUserRepository _userRepository; private readonly ITenantRepository _tenantRepository; private readonly IUser _user; + private readonly IUserRepository _userRepository; public CreateUserCommandHandler( IMediatorHandler bus, @@ -38,9 +38,9 @@ public sealed class CreateUserCommandHandler : CommandHandlerBase, { return; } - + var currentUser = await _userRepository.GetByIdAsync(_user.GetUserId()); - + if (currentUser is null || currentUser.Role != UserRole.Admin) { await NotifyAsync( diff --git a/CleanArchitecture.Domain/Commands/Users/UpdateUser/UpdateUserCommandHandler.cs b/CleanArchitecture.Domain/Commands/Users/UpdateUser/UpdateUserCommandHandler.cs index eeda6e8..74b86fc 100644 --- a/CleanArchitecture.Domain/Commands/Users/UpdateUser/UpdateUserCommandHandler.cs +++ b/CleanArchitecture.Domain/Commands/Users/UpdateUser/UpdateUserCommandHandler.cs @@ -75,7 +75,7 @@ public sealed class UpdateUserCommandHandler : CommandHandlerBase, if (_user.GetUserRole() == UserRole.Admin) { user.SetRole(request.Role); - + // Todo: Test // Todo: Check if tenant exists first user.SetTenant(request.TenantId); diff --git a/CleanArchitecture.Domain/Commands/Users/UpdateUser/UpdateUserCommandValidation.cs b/CleanArchitecture.Domain/Commands/Users/UpdateUser/UpdateUserCommandValidation.cs index d5f9301..9380fb2 100644 --- a/CleanArchitecture.Domain/Commands/Users/UpdateUser/UpdateUserCommandValidation.cs +++ b/CleanArchitecture.Domain/Commands/Users/UpdateUser/UpdateUserCommandValidation.cs @@ -23,7 +23,7 @@ public sealed class UpdateUserCommandValidation : AbstractValidator cmd.TenantId) diff --git a/CleanArchitecture.Domain/DomainEvents/Message.cs b/CleanArchitecture.Domain/DomainEvents/Message.cs index 606c259..58622a3 100644 --- a/CleanArchitecture.Domain/DomainEvents/Message.cs +++ b/CleanArchitecture.Domain/DomainEvents/Message.cs @@ -19,4 +19,4 @@ public abstract class Message : IRequest public Guid AggregateId { get; private set; } public string MessageType { get; protected set; } -} +} \ No newline at end of file diff --git a/CleanArchitecture.Domain/DomainEvents/StoredDomainEvent.cs b/CleanArchitecture.Domain/DomainEvents/StoredDomainEvent.cs index 1074410..895bc9c 100644 --- a/CleanArchitecture.Domain/DomainEvents/StoredDomainEvent.cs +++ b/CleanArchitecture.Domain/DomainEvents/StoredDomainEvent.cs @@ -4,11 +4,6 @@ namespace CleanArchitecture.Domain.DomainEvents; public class StoredDomainEvent : DomainEvent { - public Guid Id { get; private set; } - public string Data { get; private set; } = string.Empty; - public string User { get; private set; } = string.Empty; - public string CorrelationId { get; private set; } = string.Empty; - public StoredDomainEvent( DomainEvent domainEvent, string data, @@ -24,5 +19,11 @@ public class StoredDomainEvent : DomainEvent // EF Constructor protected StoredDomainEvent() : base(Guid.NewGuid()) - { } -} + { + } + + public Guid Id { get; private set; } + public string Data { get; private set; } = string.Empty; + public string User { get; private set; } = string.Empty; + public string CorrelationId { get; private set; } = string.Empty; +} \ No newline at end of file diff --git a/CleanArchitecture.Domain/DomainNotifications/StoredDomainNotification.cs b/CleanArchitecture.Domain/DomainNotifications/StoredDomainNotification.cs index 3a6199c..b3fc594 100644 --- a/CleanArchitecture.Domain/DomainNotifications/StoredDomainNotification.cs +++ b/CleanArchitecture.Domain/DomainNotifications/StoredDomainNotification.cs @@ -5,21 +5,16 @@ namespace CleanArchitecture.Domain.DomainNotifications; public class StoredDomainNotification : DomainNotification { - public Guid Id { get; private set; } - public string SerializedData { get; private set; } = string.Empty; - public string User { get; private set; } = string.Empty; - public string CorrelationId { get; private set; } = string.Empty; - public StoredDomainNotification( DomainNotification domainNotification, string data, string user, string correlationId) : base( - domainNotification.Key, - domainNotification.Value, - domainNotification.Code, - null, - domainNotification.AggregateId) + domainNotification.Key, + domainNotification.Value, + domainNotification.Code, + null, + domainNotification.AggregateId) { Id = Guid.NewGuid(); User = user; @@ -31,5 +26,11 @@ public class StoredDomainNotification : DomainNotification // EF Constructor protected StoredDomainNotification() : base(string.Empty, string.Empty, string.Empty) - { } -} + { + } + + public Guid Id { get; private set; } + public string SerializedData { get; private set; } = string.Empty; + public string User { get; private set; } = string.Empty; + public string CorrelationId { get; private set; } = string.Empty; +} \ No newline at end of file diff --git a/CleanArchitecture.Domain/Entities/Tenant.cs b/CleanArchitecture.Domain/Entities/Tenant.cs index 39cefb6..74b97fd 100644 --- a/CleanArchitecture.Domain/Entities/Tenant.cs +++ b/CleanArchitecture.Domain/Entities/Tenant.cs @@ -5,17 +5,17 @@ namespace CleanArchitecture.Domain.Entities; public class Tenant : Entity { - public string Name { get; private set; } - - public virtual ICollection Users { get; private set; } = new HashSet(); - public Tenant( Guid id, string name) : base(id) { Name = name; } - + + public string Name { get; private set; } + + public virtual ICollection Users { get; private set; } = new HashSet(); + public void SetName(string name) { Name = name; diff --git a/CleanArchitecture.Domain/Entities/User.cs b/CleanArchitecture.Domain/Entities/User.cs index 56131d8..c897574 100644 --- a/CleanArchitecture.Domain/Entities/User.cs +++ b/CleanArchitecture.Domain/Entities/User.cs @@ -1,22 +1,10 @@ using System; -using System.Diagnostics.CodeAnalysis; using CleanArchitecture.Domain.Enums; namespace CleanArchitecture.Domain.Entities; public class User : Entity { - public string Email { get; private set; } - public string FirstName { get; private set; } - public string LastName { get; private set; } - public string Password { get; private set; } - public UserRole Role { get; private set; } - - public string FullName => $"{FirstName}, {LastName}"; - - public Guid TenantId { get; private set; } - public virtual Tenant Tenant { get; private set; } = null!; - public User( Guid id, Guid tenantId, @@ -34,6 +22,17 @@ public class User : Entity Role = role; } + public string Email { get; private set; } + public string FirstName { get; private set; } + public string LastName { get; private set; } + public string Password { get; private set; } + public UserRole Role { get; private set; } + + public string FullName => $"{FirstName}, {LastName}"; + + public Guid TenantId { get; private set; } + public virtual Tenant Tenant { get; private set; } = null!; + public void SetEmail(string email) { Email = email; @@ -58,7 +57,7 @@ public class User : Entity { Role = role; } - + public void SetTenant(Guid tenantId) { TenantId = tenantId; diff --git a/CleanArchitecture.Domain/Events/Tenant/TenantCreatedEvent.cs b/CleanArchitecture.Domain/Events/Tenant/TenantCreatedEvent.cs index b79f34e..fcdca66 100644 --- a/CleanArchitecture.Domain/Events/Tenant/TenantCreatedEvent.cs +++ b/CleanArchitecture.Domain/Events/Tenant/TenantCreatedEvent.cs @@ -5,10 +5,10 @@ namespace CleanArchitecture.Domain.Events.Tenant; public sealed class TenantCreatedEvent : DomainEvent { - public string Name { get; set; } - public TenantCreatedEvent(Guid tenantId, string name) : base(tenantId) { Name = name; } + + public string Name { get; set; } } \ No newline at end of file diff --git a/CleanArchitecture.Domain/Events/Tenant/TenantUpdatedEvent.cs b/CleanArchitecture.Domain/Events/Tenant/TenantUpdatedEvent.cs index 00b66b3..0c099d2 100644 --- a/CleanArchitecture.Domain/Events/Tenant/TenantUpdatedEvent.cs +++ b/CleanArchitecture.Domain/Events/Tenant/TenantUpdatedEvent.cs @@ -5,10 +5,10 @@ namespace CleanArchitecture.Domain.Events.Tenant; public sealed class TenantUpdatedEvent : DomainEvent { - public string Name { get; set; } - public TenantUpdatedEvent(Guid tenantId, string name) : base(tenantId) { Name = name; } + + public string Name { get; set; } } \ No newline at end of file diff --git a/CleanArchitecture.Domain/Extensions/ServiceCollectionExtension.cs b/CleanArchitecture.Domain/Extensions/ServiceCollectionExtension.cs index a82ac0a..eddd6e7 100644 --- a/CleanArchitecture.Domain/Extensions/ServiceCollectionExtension.cs +++ b/CleanArchitecture.Domain/Extensions/ServiceCollectionExtension.cs @@ -41,7 +41,7 @@ public static class ServiceCollectionExtension services.AddScoped, UserEventHandler>(); services.AddScoped, UserEventHandler>(); services.AddScoped, UserEventHandler>(); - + // Tenant services.AddScoped, TenantEventHandler>(); services.AddScoped, TenantEventHandler>(); diff --git a/CleanArchitecture.Infrastructure.Tests/CleanArchitecture.Infrastructure.Tests.csproj b/CleanArchitecture.Infrastructure.Tests/CleanArchitecture.Infrastructure.Tests.csproj index 41e3ba7..940558b 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/CleanArchitecture.Infrastructure.csproj b/CleanArchitecture.Infrastructure/CleanArchitecture.Infrastructure.csproj index 1df52f3..4aab3e5 100644 --- a/CleanArchitecture.Infrastructure/CleanArchitecture.Infrastructure.csproj +++ b/CleanArchitecture.Infrastructure/CleanArchitecture.Infrastructure.csproj @@ -6,19 +6,19 @@ - + - - - - + + + + all runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/CleanArchitecture.Infrastructure/Configurations/EventSourcing/StoredDomainEventConfiguration.cs b/CleanArchitecture.Infrastructure/Configurations/EventSourcing/StoredDomainEventConfiguration.cs index 05091d5..4347992 100644 --- a/CleanArchitecture.Infrastructure/Configurations/EventSourcing/StoredDomainEventConfiguration.cs +++ b/CleanArchitecture.Infrastructure/Configurations/EventSourcing/StoredDomainEventConfiguration.cs @@ -22,4 +22,4 @@ public sealed class StoredDomainEventConfiguration : IEntityTypeConfiguration c.SerializedData) .HasColumnName("Data"); } -} +} \ No newline at end of file diff --git a/CleanArchitecture.Infrastructure/Configurations/UserConfiguration.cs b/CleanArchitecture.Infrastructure/Configurations/UserConfiguration.cs index abe3cef..08642b0 100644 --- a/CleanArchitecture.Infrastructure/Configurations/UserConfiguration.cs +++ b/CleanArchitecture.Infrastructure/Configurations/UserConfiguration.cs @@ -1,4 +1,3 @@ -using System; using CleanArchitecture.Domain.Constants; using CleanArchitecture.Domain.Entities; using CleanArchitecture.Domain.Enums; diff --git a/CleanArchitecture.Infrastructure/Database/EventStoreDbContext.cs b/CleanArchitecture.Infrastructure/Database/EventStoreDbContext.cs index fb6a4b3..e7e7c65 100644 --- a/CleanArchitecture.Infrastructure/Database/EventStoreDbContext.cs +++ b/CleanArchitecture.Infrastructure/Database/EventStoreDbContext.cs @@ -18,4 +18,4 @@ public class EventStoreDbContext : DbContext base.OnModelCreating(modelBuilder); } -} +} \ No newline at end of file diff --git a/CleanArchitecture.Infrastructure/EventSourcing/EventStore.cs b/CleanArchitecture.Infrastructure/EventSourcing/EventStore.cs index 9231524..361156d 100644 --- a/CleanArchitecture.Infrastructure/EventSourcing/EventStore.cs +++ b/CleanArchitecture.Infrastructure/EventSourcing/EventStore.cs @@ -9,9 +9,9 @@ namespace CleanArchitecture.Infrastructure.EventSourcing; public sealed class DomainEventStore : IDomainEventStore { - private readonly EventStoreDbContext _eventStoreDbContext; - private readonly DomainNotificationStoreDbContext _domainNotificationStoreDbContext; private readonly IEventStoreContext _context; + private readonly DomainNotificationStoreDbContext _domainNotificationStoreDbContext; + private readonly EventStoreDbContext _eventStoreDbContext; public DomainEventStore( EventStoreDbContext eventStoreDbContext, @@ -53,4 +53,4 @@ public sealed class DomainEventStore : IDomainEventStore break; } } -} +} \ No newline at end of file diff --git a/CleanArchitecture.Infrastructure/EventSourcing/EventStoreContext.cs b/CleanArchitecture.Infrastructure/EventSourcing/EventStoreContext.cs index b28104b..f4610b4 100644 --- a/CleanArchitecture.Infrastructure/EventSourcing/EventStoreContext.cs +++ b/CleanArchitecture.Infrastructure/EventSourcing/EventStoreContext.cs @@ -14,7 +14,8 @@ public sealed class EventStoreContext : IEventStoreContext _user = user; if (httpContextAccessor?.HttpContext is null || - !httpContextAccessor.HttpContext.Request.Headers.TryGetValue("X-CLEAN-ARCHITECTURE-CORRELATION-ID", out var id)) + !httpContextAccessor.HttpContext.Request.Headers.TryGetValue("X-CLEAN-ARCHITECTURE-CORRELATION-ID", + out var id)) { _correlationId = $"internal - {Guid.NewGuid()}"; } @@ -24,7 +25,13 @@ public sealed class EventStoreContext : IEventStoreContext } } - public string GetCorrelationId() => _correlationId; + public string GetCorrelationId() + { + return _correlationId; + } - public string GetUserEmail() => _user?.GetUserEmail() ?? string.Empty; -} + public string GetUserEmail() + { + return _user?.GetUserEmail() ?? string.Empty; + } +} \ No newline at end of file diff --git a/CleanArchitecture.Infrastructure/EventSourcing/IEventStoreContext.cs b/CleanArchitecture.Infrastructure/EventSourcing/IEventStoreContext.cs index ab493f9..9bd64ca 100644 --- a/CleanArchitecture.Infrastructure/EventSourcing/IEventStoreContext.cs +++ b/CleanArchitecture.Infrastructure/EventSourcing/IEventStoreContext.cs @@ -4,4 +4,4 @@ public interface IEventStoreContext { public string GetUserEmail(); public string GetCorrelationId(); -} +} \ No newline at end of file diff --git a/CleanArchitecture.Infrastructure/Extensions/ServiceCollectionExtensions.cs b/CleanArchitecture.Infrastructure/Extensions/ServiceCollectionExtensions.cs index aec00dd..ad8ebac 100644 --- a/CleanArchitecture.Infrastructure/Extensions/ServiceCollectionExtensions.cs +++ b/CleanArchitecture.Infrastructure/Extensions/ServiceCollectionExtensions.cs @@ -16,18 +16,18 @@ public static class ServiceCollectionExtensions { public static IServiceCollection AddInfrastructure( this IServiceCollection services, - IConfiguration configuration, - string migrationsAssemblyName, - string connectionStringName = "DefaultConnection") + IConfiguration configuration, + string migrationsAssemblyName, + string connectionStringName = "DefaultConnection") { // Add event store db context services.AddDbContext( - options => - { - options.UseSqlServer( - configuration.GetConnectionString(connectionStringName), - b => b.MigrationsAssembly(migrationsAssemblyName)); - }); + options => + { + options.UseSqlServer( + configuration.GetConnectionString(connectionStringName), + b => b.MigrationsAssembly(migrationsAssemblyName)); + }); services.AddDbContext( options => diff --git a/CleanArchitecture.Infrastructure/GlobalSuppressions.cs b/CleanArchitecture.Infrastructure/GlobalSuppressions.cs index e510236..d7915ca 100644 --- a/CleanArchitecture.Infrastructure/GlobalSuppressions.cs +++ b/CleanArchitecture.Infrastructure/GlobalSuppressions.cs @@ -5,6 +5,12 @@ using System.Diagnostics.CodeAnalysis; -[assembly: SuppressMessage("Style", "IDE0161:Convert to file-scoped namespace", Justification = "", Scope = "namespace", Target = "~N:CleanArchitecture.Infrastructure.Migrations")] -[assembly: SuppressMessage("Style", "IDE0161:Convert to file-scoped namespace", Justification = "", Scope = "namespace", Target = "~N:CleanArchitecture.Infrastructure.Migrations.EventStoreDb")] -[assembly: SuppressMessage("Style", "IDE0161:Convert to file-scoped namespace", Justification = "", Scope = "namespace", Target = "~N:CleanArchitecture.Infrastructure.Migrations.DomainNotificationStoreDb")] +[assembly: + SuppressMessage("Style", "IDE0161:Convert to file-scoped namespace", Justification = "", + Scope = "namespace", Target = "~N:CleanArchitecture.Infrastructure.Migrations")] +[assembly: + SuppressMessage("Style", "IDE0161:Convert to file-scoped namespace", Justification = "", + Scope = "namespace", Target = "~N:CleanArchitecture.Infrastructure.Migrations.EventStoreDb")] +[assembly: + SuppressMessage("Style", "IDE0161:Convert to file-scoped namespace", Justification = "", + Scope = "namespace", Target = "~N:CleanArchitecture.Infrastructure.Migrations.DomainNotificationStoreDb")] \ No newline at end of file diff --git a/CleanArchitecture.Infrastructure/InMemoryBus.cs b/CleanArchitecture.Infrastructure/InMemoryBus.cs index 0da1dae..703cbb2 100644 --- a/CleanArchitecture.Infrastructure/InMemoryBus.cs +++ b/CleanArchitecture.Infrastructure/InMemoryBus.cs @@ -8,8 +8,8 @@ namespace CleanArchitecture.Infrastructure; public sealed class InMemoryBus : IMediatorHandler { - private readonly IMediator _mediator; private readonly IDomainEventStore _domainEventStore; + private readonly IMediator _mediator; public InMemoryBus( IMediator mediator, diff --git a/CleanArchitecture.Infrastructure/Migrations/DomainNotificationStoreDb/20230701135523_AddDomainNotificationStore.cs b/CleanArchitecture.Infrastructure/Migrations/DomainNotificationStoreDb/20230701135523_AddDomainNotificationStore.cs index bb09c31..f582c38 100644 --- a/CleanArchitecture.Infrastructure/Migrations/DomainNotificationStoreDb/20230701135523_AddDomainNotificationStore.cs +++ b/CleanArchitecture.Infrastructure/Migrations/DomainNotificationStoreDb/20230701135523_AddDomainNotificationStore.cs @@ -1,43 +1,39 @@ -using System; +#nullable disable + +using System; using Microsoft.EntityFrameworkCore.Migrations; -#nullable disable +namespace CleanArchitecture.Infrastructure.Migrations.DomainNotificationStoreDb; -namespace CleanArchitecture.Infrastructure.Migrations.DomainNotificationStoreDb +/// +public partial class AddDomainNotificationStore : Migration { /// - public partial class AddDomainNotificationStore : Migration + protected override void Up(MigrationBuilder migrationBuilder) { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.CreateTable( - name: "StoredDomainNotifications", - columns: table => new - { - Id = table.Column(type: "uniqueidentifier", nullable: false), - Data = table.Column(type: "nvarchar(max)", nullable: false), - User = table.Column(type: "nvarchar(100)", maxLength: 100, nullable: false), - CorrelationId = table.Column(type: "nvarchar(100)", maxLength: 100, nullable: false), - AggregateId = table.Column(type: "uniqueidentifier", nullable: false), - MessageType = table.Column(type: "nvarchar(100)", maxLength: 100, nullable: false), - Timestamp = table.Column(type: "datetime2", nullable: false), - Key = table.Column(type: "nvarchar(100)", maxLength: 100, nullable: false), - Value = table.Column(type: "nvarchar(1024)", maxLength: 1024, nullable: false), - Code = table.Column(type: "nvarchar(100)", maxLength: 100, nullable: false), - Version = table.Column(type: "int", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_StoredDomainNotifications", x => x.Id); - }); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropTable( - name: "StoredDomainNotifications"); - } + migrationBuilder.CreateTable( + "StoredDomainNotifications", + table => new + { + Id = table.Column("uniqueidentifier", nullable: false), + Data = table.Column("nvarchar(max)", nullable: false), + User = table.Column("nvarchar(100)", maxLength: 100, nullable: false), + CorrelationId = table.Column("nvarchar(100)", maxLength: 100, nullable: false), + AggregateId = table.Column("uniqueidentifier", nullable: false), + MessageType = table.Column("nvarchar(100)", maxLength: 100, nullable: false), + Timestamp = table.Column("datetime2", nullable: false), + Key = table.Column("nvarchar(100)", maxLength: 100, nullable: false), + Value = table.Column("nvarchar(1024)", maxLength: 1024, nullable: false), + Code = table.Column("nvarchar(100)", maxLength: 100, nullable: false), + Version = table.Column("int", nullable: false) + }, + constraints: table => { table.PrimaryKey("PK_StoredDomainNotifications", x => x.Id); }); } -} + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + "StoredDomainNotifications"); + } +} \ No newline at end of file diff --git a/CleanArchitecture.Infrastructure/Migrations/EventStoreDb/20230701135441_AddEventStore.cs b/CleanArchitecture.Infrastructure/Migrations/EventStoreDb/20230701135441_AddEventStore.cs index b30585f..8a4c9c8 100644 --- a/CleanArchitecture.Infrastructure/Migrations/EventStoreDb/20230701135441_AddEventStore.cs +++ b/CleanArchitecture.Infrastructure/Migrations/EventStoreDb/20230701135441_AddEventStore.cs @@ -1,39 +1,35 @@ -using System; +#nullable disable + +using System; using Microsoft.EntityFrameworkCore.Migrations; -#nullable disable +namespace CleanArchitecture.Infrastructure.Migrations.EventStoreDb; -namespace CleanArchitecture.Infrastructure.Migrations.EventStoreDb +/// +public partial class AddEventStore : Migration { /// - public partial class AddEventStore : Migration + protected override void Up(MigrationBuilder migrationBuilder) { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.CreateTable( - name: "StoredDomainEvents", - columns: table => new - { - Id = table.Column(type: "uniqueidentifier", nullable: false), - Data = table.Column(type: "nvarchar(max)", nullable: false), - User = table.Column(type: "nvarchar(100)", maxLength: 100, nullable: false), - CorrelationId = table.Column(type: "nvarchar(100)", maxLength: 100, nullable: false), - AggregateId = table.Column(type: "uniqueidentifier", nullable: false), - Action = table.Column(type: "varchar(100)", nullable: false), - CreationDate = table.Column(type: "datetime2", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_StoredDomainEvents", x => x.Id); - }); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropTable( - name: "StoredDomainEvents"); - } + migrationBuilder.CreateTable( + "StoredDomainEvents", + table => new + { + Id = table.Column("uniqueidentifier", nullable: false), + Data = table.Column("nvarchar(max)", nullable: false), + User = table.Column("nvarchar(100)", maxLength: 100, nullable: false), + CorrelationId = table.Column("nvarchar(100)", maxLength: 100, nullable: false), + AggregateId = table.Column("uniqueidentifier", nullable: false), + Action = table.Column("varchar(100)", nullable: false), + CreationDate = table.Column("datetime2", nullable: false) + }, + constraints: table => { table.PrimaryKey("PK_StoredDomainEvents", x => x.Id); }); } -} + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + "StoredDomainEvents"); + } +} \ No newline at end of file diff --git a/CleanArchitecture.Infrastructure/Repositories/BaseRepository.cs b/CleanArchitecture.Infrastructure/Repositories/BaseRepository.cs index 4be4db9..88b7349 100644 --- a/CleanArchitecture.Infrastructure/Repositories/BaseRepository.cs +++ b/CleanArchitecture.Infrastructure/Repositories/BaseRepository.cs @@ -72,7 +72,7 @@ public class BaseRepository : IRepository where TEntity : Enti DbSet.Update(entity); } } - + public void RemoveRange(IEnumerable entities, bool hardDelete = false) { if (hardDelete) diff --git a/CleanArchitecture.IntegrationTests/CleanArchitecture.IntegrationTests.csproj b/CleanArchitecture.IntegrationTests/CleanArchitecture.IntegrationTests.csproj index d008535..3aa4f40 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/TenantControllerTests.cs b/CleanArchitecture.IntegrationTests/Controller/TenantControllerTests.cs index a3d60e5..2f9dd8a 100644 --- a/CleanArchitecture.IntegrationTests/Controller/TenantControllerTests.cs +++ b/CleanArchitecture.IntegrationTests/Controller/TenantControllerTests.cs @@ -30,17 +30,17 @@ public sealed class TenantControllerTests : IClassFixture await _fixture.AuthenticateUserAsync(); var response = await _fixture.ServerClient.GetAsync($"/api/v1/Tenant/{_fixture.CreatedTenantId}"); - + response.StatusCode.Should().Be(HttpStatusCode.OK); - + var message = await response.Content.ReadAsJsonAsync(); - + message?.Data.Should().NotBeNull(); - + message!.Data!.Id.Should().Be(_fixture.CreatedTenantId); message.Data.Name.Should().Be("Test Tenant"); } - + [Fact] [Priority(5)] public async Task Should_Get_All_Tenants() @@ -48,89 +48,89 @@ public sealed class TenantControllerTests : IClassFixture await _fixture.AuthenticateUserAsync(); var response = await _fixture.ServerClient.GetAsync("api/v1/Tenant"); - + response.StatusCode.Should().Be(HttpStatusCode.OK); - + var message = await response.Content.ReadAsJsonAsync>(); - + message?.Data.Should().NotBeEmpty(); message!.Data.Should().HaveCountGreaterOrEqualTo(2); message.Data! .FirstOrDefault(x => x.Id == _fixture.CreatedTenantId) .Should().NotBeNull(); } - + [Fact] [Priority(10)] public async Task Should_Create_Tenant() { await _fixture.AuthenticateUserAsync(); - + var request = new CreateTenantViewModel("Test Tenant 2"); - + var response = await _fixture.ServerClient.PostAsJsonAsync("/api/v1/Tenant", request); - + response.StatusCode.Should().Be(HttpStatusCode.OK); - + var message = await response.Content.ReadAsJsonAsync(); var tenantId = message?.Data; - + // Check if tenant exists var tenantResponse = await _fixture.ServerClient.GetAsync($"/api/v1/Tenant/{tenantId}"); - + tenantResponse.StatusCode.Should().Be(HttpStatusCode.OK); var tenantMessage = await tenantResponse.Content.ReadAsJsonAsync(); - + tenantMessage?.Data.Should().NotBeNull(); - + tenantMessage!.Data!.Id.Should().Be(tenantId!.Value); tenantMessage.Data.Name.Should().Be(request.Name); } - + [Fact] [Priority(15)] public async Task Should_Update_Tenant() { await _fixture.AuthenticateUserAsync(); - + var request = new UpdateTenantViewModel(_fixture.CreatedTenantId, "Test Tenant 3"); - + var response = await _fixture.ServerClient.PutAsJsonAsync("/api/v1/Tenant", request); - + response.StatusCode.Should().Be(HttpStatusCode.OK); - + var message = await response.Content.ReadAsJsonAsync(); - + message?.Data.Should().NotBeNull(); message!.Data.Should().BeEquivalentTo(request); - + // Check if tenant is updated var tenantResponse = await _fixture.ServerClient.GetAsync($"/api/v1/Tenant/{_fixture.CreatedTenantId}"); - + tenantResponse.StatusCode.Should().Be(HttpStatusCode.OK); - + var tenantMessage = await response.Content.ReadAsJsonAsync(); - + tenantMessage?.Data.Should().NotBeNull(); - + tenantMessage!.Data!.Id.Should().Be(_fixture.CreatedTenantId); tenantMessage.Data.Name.Should().Be(request.Name); } - + [Fact] [Priority(20)] public async Task Should_Delete_Tenant() { await _fixture.AuthenticateUserAsync(); - + var response = await _fixture.ServerClient.DeleteAsync($"/api/v1/Tenant/{_fixture.CreatedTenantId}"); - + response.StatusCode.Should().Be(HttpStatusCode.OK); - + // Check if tenant is deleted var tenantResponse = await _fixture.ServerClient.GetAsync($"/api/v1/Tenant/{_fixture.CreatedTenantId}"); - + tenantResponse.StatusCode.Should().Be(HttpStatusCode.NotFound); } } \ No newline at end of file diff --git a/CleanArchitecture.IntegrationTests/Controller/UserControllerTests.cs b/CleanArchitecture.IntegrationTests/Controller/UserControllerTests.cs index 4fd17ef..b3dd980 100644 --- a/CleanArchitecture.IntegrationTests/Controller/UserControllerTests.cs +++ b/CleanArchitecture.IntegrationTests/Controller/UserControllerTests.cs @@ -26,13 +26,13 @@ public sealed class UserControllerTests : IClassFixture } // Todo: Refactor tests to work alone - + [Fact] [Priority(0)] public async Task Should_Create_User() { await _fixture.AuthenticateUserAsync(); - + var user = new CreateUserViewModel( _fixture.CreatedUserEmail, "Test", @@ -237,7 +237,7 @@ public sealed class UserControllerTests : IClassFixture var content = message!.Data; content.Should().Be(_fixture.CreatedUserId); - + // Todo: Check if stuff is done } } \ No newline at end of file diff --git a/CleanArchitecture.IntegrationTests/Fixtures/TenantTestFixture.cs b/CleanArchitecture.IntegrationTests/Fixtures/TenantTestFixture.cs index 0ab2498..dffe6cc 100644 --- a/CleanArchitecture.IntegrationTests/Fixtures/TenantTestFixture.cs +++ b/CleanArchitecture.IntegrationTests/Fixtures/TenantTestFixture.cs @@ -7,7 +7,7 @@ namespace CleanArchitecture.IntegrationTests.Fixtures; public sealed class TenantTestFixture : TestFixtureBase { public Guid CreatedTenantId { get; } = Guid.NewGuid(); - + protected override void SeedTestData(ApplicationDbContext context) { context.Tenants.Add(new Tenant( diff --git a/CleanArchitecture.IntegrationTests/Fixtures/TestFixtureBase.cs b/CleanArchitecture.IntegrationTests/Fixtures/TestFixtureBase.cs index 80c2273..0be073f 100644 --- a/CleanArchitecture.IntegrationTests/Fixtures/TestFixtureBase.cs +++ b/CleanArchitecture.IntegrationTests/Fixtures/TestFixtureBase.cs @@ -1,5 +1,4 @@ using System; -using System.Net; using System.Net.Http; using System.Threading.Tasks; using CleanArchitecture.Application.ViewModels.Users; @@ -36,7 +35,7 @@ public class TestFixtureBase IServiceProvider scopedServices) { } - + // Todo: Fix auth public virtual async Task AuthenticateUserAsync() { diff --git a/CleanArchitecture.IntegrationTests/Fixtures/gRPC/GetTenantsByIdsTestFixture.cs b/CleanArchitecture.IntegrationTests/Fixtures/gRPC/GetTenantsByIdsTestFixture.cs index 5c9c3bd..b6f2672 100644 --- a/CleanArchitecture.IntegrationTests/Fixtures/gRPC/GetTenantsByIdsTestFixture.cs +++ b/CleanArchitecture.IntegrationTests/Fixtures/gRPC/GetTenantsByIdsTestFixture.cs @@ -7,9 +7,6 @@ namespace CleanArchitecture.IntegrationTests.Fixtures.gRPC; public sealed class GetTenantsByIdsTestFixture : TestFixtureBase { - public GrpcChannel GrpcChannel { get; } - public Guid CreatedTenantId { get; } = Guid.NewGuid(); - public GetTenantsByIdsTestFixture() { GrpcChannel = GrpcChannel.ForAddress("http://localhost", new GrpcChannelOptions @@ -18,6 +15,9 @@ public sealed class GetTenantsByIdsTestFixture : TestFixtureBase }); } + public GrpcChannel GrpcChannel { get; } + public Guid CreatedTenantId { get; } = Guid.NewGuid(); + protected override void SeedTestData(ApplicationDbContext context) { base.SeedTestData(context); @@ -30,7 +30,7 @@ public sealed class GetTenantsByIdsTestFixture : TestFixtureBase public Tenant CreateTenant() { - return new( + return new Tenant( CreatedTenantId, "Test Tenant"); } diff --git a/CleanArchitecture.IntegrationTests/Fixtures/gRPC/GetUsersByIdsTestFixture.cs b/CleanArchitecture.IntegrationTests/Fixtures/gRPC/GetUsersByIdsTestFixture.cs index 22ce59b..c39c29b 100644 --- a/CleanArchitecture.IntegrationTests/Fixtures/gRPC/GetUsersByIdsTestFixture.cs +++ b/CleanArchitecture.IntegrationTests/Fixtures/gRPC/GetUsersByIdsTestFixture.cs @@ -9,9 +9,6 @@ namespace CleanArchitecture.IntegrationTests.Fixtures.gRPC; public sealed class GetUsersByIdsTestFixture : TestFixtureBase { - public GrpcChannel GrpcChannel { get; } - public Guid CreatedUserId { get; } = Guid.NewGuid(); - public GetUsersByIdsTestFixture() { GrpcChannel = GrpcChannel.ForAddress("http://localhost", new GrpcChannelOptions @@ -20,6 +17,9 @@ public sealed class GetUsersByIdsTestFixture : TestFixtureBase }); } + public GrpcChannel GrpcChannel { get; } + public Guid CreatedUserId { get; } = Guid.NewGuid(); + protected override void SeedTestData(ApplicationDbContext context) { base.SeedTestData(context); diff --git a/CleanArchitecture.IntegrationTests/Infrastructure/CleanArchitectureWebApplicationFactory.cs b/CleanArchitecture.IntegrationTests/Infrastructure/CleanArchitectureWebApplicationFactory.cs index 63578e2..9a52c4d 100644 --- a/CleanArchitecture.IntegrationTests/Infrastructure/CleanArchitectureWebApplicationFactory.cs +++ b/CleanArchitecture.IntegrationTests/Infrastructure/CleanArchitectureWebApplicationFactory.cs @@ -47,19 +47,19 @@ public sealed class CleanArchitectureWebApplicationFactory : WebApplicationFacto services.SetupTestDatabase(_connection); services.SetupTestDatabase(_connection); - ServiceProvider sp = services.BuildServiceProvider(); + var sp = services.BuildServiceProvider(); - using IServiceScope scope = sp.CreateScope(); - IServiceProvider scopedServices = scope.ServiceProvider; + using var scope = sp.CreateScope(); + var scopedServices = scope.ServiceProvider; - ApplicationDbContext applicationDbContext = scopedServices.GetRequiredService(); - EventStoreDbContext storeDbContext = scopedServices.GetRequiredService(); - DomainNotificationStoreDbContext domainStoreDbContext = scopedServices.GetRequiredService(); + var applicationDbContext = scopedServices.GetRequiredService(); + var storeDbContext = scopedServices.GetRequiredService(); + var domainStoreDbContext = scopedServices.GetRequiredService(); applicationDbContext.EnsureMigrationsApplied(); var creator2 = (RelationalDatabaseCreator)storeDbContext.Database - .GetService(); + .GetService(); creator2.CreateTables(); var creator3 = (RelationalDatabaseCreator)domainStoreDbContext diff --git a/CleanArchitecture.IntegrationTests/gRPC/GetTenantsByIdsTests.cs b/CleanArchitecture.IntegrationTests/gRPC/GetTenantsByIdsTests.cs index 5c939a3..d6c2e8b 100644 --- a/CleanArchitecture.IntegrationTests/gRPC/GetTenantsByIdsTests.cs +++ b/CleanArchitecture.IntegrationTests/gRPC/GetTenantsByIdsTests.cs @@ -26,9 +26,9 @@ public sealed class GetTenantsByIdsTests : IClassFixture - - - - + + + + - - - + + + diff --git a/CleanArchitecture.Shared/CleanArchitecture.Shared.csproj b/CleanArchitecture.Shared/CleanArchitecture.Shared.csproj index 882f5b9..7547394 100644 --- a/CleanArchitecture.Shared/CleanArchitecture.Shared.csproj +++ b/CleanArchitecture.Shared/CleanArchitecture.Shared.csproj @@ -1,9 +1,9 @@ - - net7.0 - enable - + + net7.0 + enable + diff --git a/CleanArchitecture.Shared/Users/UserViewModel.cs b/CleanArchitecture.Shared/Users/UserViewModel.cs index c183ded..d08d316 100644 --- a/CleanArchitecture.Shared/Users/UserViewModel.cs +++ b/CleanArchitecture.Shared/Users/UserViewModel.cs @@ -7,4 +7,4 @@ public sealed record UserViewModel( string Email, string FirstName, string LastName, - bool IsDeleted); + bool IsDeleted); \ No newline at end of file diff --git a/CleanArchitecture.gRPC.Tests/CleanArchitecture.gRPC.Tests.csproj b/CleanArchitecture.gRPC.Tests/CleanArchitecture.gRPC.Tests.csproj index e4730d8..dfc5b4f 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,9 +24,9 @@ - - - + + + diff --git a/CleanArchitecture.gRPC.Tests/Fixtures/TenantTestFixture.cs b/CleanArchitecture.gRPC.Tests/Fixtures/TenantTestFixture.cs index 4fb0177..3910cc2 100644 --- a/CleanArchitecture.gRPC.Tests/Fixtures/TenantTestFixture.cs +++ b/CleanArchitecture.gRPC.Tests/Fixtures/TenantTestFixture.cs @@ -10,24 +10,24 @@ namespace CleanArchitecture.gRPC.Tests.Fixtures; public sealed class TenantTestFixture { - public TenantsApiImplementation TenantsApiImplementation { get; } - private ITenantRepository TenantRepository { get; } - - public IEnumerable ExistingTenants { get; } - public TenantTestFixture() { TenantRepository = Substitute.For(); ExistingTenants = new List { - new Tenant(Guid.NewGuid(), "Tenant 1"), - new Tenant(Guid.NewGuid(), "Tenant 2"), - new Tenant(Guid.NewGuid(), "Tenant 3"), + new(Guid.NewGuid(), "Tenant 1"), + new(Guid.NewGuid(), "Tenant 2"), + new(Guid.NewGuid(), "Tenant 3") }; - + TenantRepository.GetAllNoTracking().Returns(ExistingTenants.BuildMock()); - - TenantsApiImplementation = new(TenantRepository); + + TenantsApiImplementation = new TenantsApiImplementation(TenantRepository); } + + public TenantsApiImplementation TenantsApiImplementation { get; } + private ITenantRepository TenantRepository { get; } + + public IEnumerable ExistingTenants { get; } } \ No newline at end of file diff --git a/CleanArchitecture.gRPC.Tests/Tenants/GetTenantsByIdsTests.cs b/CleanArchitecture.gRPC.Tests/Tenants/GetTenantsByIdsTests.cs index 5a60273..61e51f5 100644 --- a/CleanArchitecture.gRPC.Tests/Tenants/GetTenantsByIdsTests.cs +++ b/CleanArchitecture.gRPC.Tests/Tenants/GetTenantsByIdsTests.cs @@ -24,7 +24,7 @@ public sealed class GetTenantsByIdsTests : IClassFixture var result = await _fixture.TenantsApiImplementation.GetByIds( SetupRequest(Enumerable.Empty()), default!); - + result.Tenants.Should().HaveCount(0); } @@ -37,36 +37,36 @@ public sealed class GetTenantsByIdsTests : IClassFixture .Take(2) .Select(tenant => tenant.Id) .ToList(); - + ids.Add(nonExistingId); - + var result = await _fixture.TenantsApiImplementation.GetByIds( SetupRequest(ids), default!); - + result.Tenants.Should().HaveCount(2); foreach (var tenant in result.Tenants) { var tenantId = Guid.Parse(tenant.Id); - + tenantId.Should().NotBe(nonExistingId); - + var mockTenant = _fixture.ExistingTenants.First(t => t.Id == tenantId); - + mockTenant.Should().NotBeNull(); - + tenant.Name.Should().Be(mockTenant.Name); } } - + private static GetTenantsByIdsRequest SetupRequest(IEnumerable ids) { var request = new GetTenantsByIdsRequest(); - + request.Ids.AddRange(ids.Select(id => id.ToString())); request.Ids.Add("Not a guid"); - + return request; } } \ No newline at end of file diff --git a/CleanArchitecture.gRPC/CleanArchitecture.cs b/CleanArchitecture.gRPC/CleanArchitecture.cs index 8aa7853..3a54670 100644 --- a/CleanArchitecture.gRPC/CleanArchitecture.cs +++ b/CleanArchitecture.gRPC/CleanArchitecture.cs @@ -4,18 +4,15 @@ namespace CleanArchitecture.gRPC; public sealed class CleanArchitecture : ICleanArchitecture { - private readonly IUsersContext _users; - private readonly ITenantsContext _tenants; - - public IUsersContext Users => _users; - public ITenantsContext Tenants => _tenants; - public CleanArchitecture( IUsersContext users, ITenantsContext tenants) { - _users = users; - _tenants = tenants; - + Users = users; + Tenants = tenants; } -} + + public IUsersContext Users { get; } + + public ITenantsContext Tenants { get; } +} \ No newline at end of file diff --git a/CleanArchitecture.gRPC/CleanArchitecture.gRPC.csproj b/CleanArchitecture.gRPC/CleanArchitecture.gRPC.csproj index 1bec38c..068e549 100644 --- a/CleanArchitecture.gRPC/CleanArchitecture.gRPC.csproj +++ b/CleanArchitecture.gRPC/CleanArchitecture.gRPC.csproj @@ -6,9 +6,9 @@ - - - + + + diff --git a/CleanArchitecture.gRPC/Contexts/TenantsContext.cs b/CleanArchitecture.gRPC/Contexts/TenantsContext.cs index a8235ad..f6038e4 100644 --- a/CleanArchitecture.gRPC/Contexts/TenantsContext.cs +++ b/CleanArchitecture.gRPC/Contexts/TenantsContext.cs @@ -20,11 +20,11 @@ public sealed class TenantsContext : ITenantsContext public async Task> GetTenantsByIds(IEnumerable ids) { var request = new GetTenantsByIdsRequest(); - + request.Ids.AddRange(ids.Select(id => id.ToString())); - + var result = await _client.GetByIdsAsync(request); - + return result.Tenants.Select(tenant => new TenantViewModel( Guid.Parse(tenant.Id), tenant.Name)); diff --git a/CleanArchitecture.gRPC/Contexts/UsersContext.cs b/CleanArchitecture.gRPC/Contexts/UsersContext.cs index a00cbb7..a6b02d2 100644 --- a/CleanArchitecture.gRPC/Contexts/UsersContext.cs +++ b/CleanArchitecture.gRPC/Contexts/UsersContext.cs @@ -32,4 +32,4 @@ public sealed class UsersContext : IUsersContext user.LastName, user.IsDeleted)); } -} +} \ No newline at end of file diff --git a/CleanArchitecture.gRPC/Extensions/ServiceCollectionExtensions.cs b/CleanArchitecture.gRPC/Extensions/ServiceCollectionExtensions.cs index 89a9084..114c410 100644 --- a/CleanArchitecture.gRPC/Extensions/ServiceCollectionExtensions.cs +++ b/CleanArchitecture.gRPC/Extensions/ServiceCollectionExtensions.cs @@ -53,7 +53,7 @@ public static class ServiceCollectionExtensions services.AddSingleton(); services.AddSingleton(); - + return services; } -} +} \ No newline at end of file diff --git a/CleanArchitecture.gRPC/ICleanArchitecture.cs b/CleanArchitecture.gRPC/ICleanArchitecture.cs index 9e04af9..9c92a1c 100644 --- a/CleanArchitecture.gRPC/ICleanArchitecture.cs +++ b/CleanArchitecture.gRPC/ICleanArchitecture.cs @@ -6,4 +6,4 @@ public interface ICleanArchitecture { IUsersContext Users { get; } ITenantsContext Tenants { get; } -} +} \ No newline at end of file diff --git a/CleanArchitecture.gRPC/Interfaces/IUsersContext.cs b/CleanArchitecture.gRPC/Interfaces/IUsersContext.cs index f1f2f04..c8b01f3 100644 --- a/CleanArchitecture.gRPC/Interfaces/IUsersContext.cs +++ b/CleanArchitecture.gRPC/Interfaces/IUsersContext.cs @@ -8,4 +8,4 @@ namespace CleanArchitecture.gRPC.Interfaces; public interface IUsersContext { Task> GetUsersByIds(IEnumerable ids); -} +} \ No newline at end of file diff --git a/CleanArchitecture.gRPC/Models/GRPCSettings.cs b/CleanArchitecture.gRPC/Models/GRPCSettings.cs index fd7a02c..e784168 100644 --- a/CleanArchitecture.gRPC/Models/GRPCSettings.cs +++ b/CleanArchitecture.gRPC/Models/GRPCSettings.cs @@ -3,4 +3,4 @@ public sealed class GRPCSettings { public string CleanArchitectureUrl { get; set; } = string.Empty; -} +} \ No newline at end of file From 3969db2bba88bc53c2754c2178afce8e7be590d8 Mon Sep 17 00:00:00 2001 From: alex289 Date: Thu, 31 Aug 2023 11:46:01 +0200 Subject: [PATCH 07/10] feat: Check for tenant when updating users tenant --- .../CreateTenantCommandHandlerTests.cs | 6 +- .../DeleteTenantCommandHandlerTests.cs | 19 ++++++ .../DeleteTenantCommandTestFixture.cs | 6 ++ .../UpdateTenantCommandHandlerTests.cs | 6 +- .../UpdateUserCommandHandlerTests.cs | 61 +++++++++++++++++++ .../UpdateUserCommandTestFixture.cs | 22 ++++++- .../DeleteTenantCommandHandler.cs | 2 - .../UpdateUser/UpdateUserCommandHandler.cs | 16 ++++- .../Users/GetUsersByIdsTests.cs | 1 - 9 files changed, 126 insertions(+), 13 deletions(-) diff --git a/CleanArchitecture.Domain.Tests/CommandHandler/Tenant/CreateTenant/CreateTenantCommandHandlerTests.cs b/CleanArchitecture.Domain.Tests/CommandHandler/Tenant/CreateTenant/CreateTenantCommandHandlerTests.cs index eb8d848..2dce689 100644 --- a/CleanArchitecture.Domain.Tests/CommandHandler/Tenant/CreateTenant/CreateTenantCommandHandlerTests.cs +++ b/CleanArchitecture.Domain.Tests/CommandHandler/Tenant/CreateTenant/CreateTenantCommandHandlerTests.cs @@ -17,7 +17,7 @@ public sealed class CreateTenantCommandHandlerTests Guid.NewGuid(), "Test Tenant"); - _fixture.CommandHandler.Handle(command, default!).Wait(); + _fixture.CommandHandler.Handle(command, default).Wait(); _fixture .VerifyNoDomainNotification() @@ -36,7 +36,7 @@ public sealed class CreateTenantCommandHandlerTests Guid.NewGuid(), "Test Tenant"); - _fixture.CommandHandler.Handle(command, default!).Wait(); + _fixture.CommandHandler.Handle(command, default).Wait(); _fixture .VerifyNoCommit() @@ -56,7 +56,7 @@ public sealed class CreateTenantCommandHandlerTests _fixture.SetupExistingTenant(command.AggregateId); - _fixture.CommandHandler.Handle(command, default!).Wait(); + _fixture.CommandHandler.Handle(command, default).Wait(); _fixture .VerifyNoCommit() diff --git a/CleanArchitecture.Domain.Tests/CommandHandler/Tenant/DeleteTenant/DeleteTenantCommandHandlerTests.cs b/CleanArchitecture.Domain.Tests/CommandHandler/Tenant/DeleteTenant/DeleteTenantCommandHandlerTests.cs index cfd6851..8f0d378 100644 --- a/CleanArchitecture.Domain.Tests/CommandHandler/Tenant/DeleteTenant/DeleteTenantCommandHandlerTests.cs +++ b/CleanArchitecture.Domain.Tests/CommandHandler/Tenant/DeleteTenant/DeleteTenantCommandHandlerTests.cs @@ -42,4 +42,23 @@ public sealed class DeleteTenantCommandHandlerTests ErrorCodes.ObjectNotFound, $"There is no tenant with Id {command.AggregateId}"); } + + [Fact] + public void Should_Not_Delete_Tenant_Insufficient_Permissions() + { + var tenant = _fixture.SetupTenant(); + _fixture.SetupUser(); + + var command = new DeleteTenantCommand(tenant.Id); + + _fixture.CommandHandler.Handle(command, default).Wait(); + + _fixture + .VerifyNoCommit() + .VerifyNoRaisedEvent() + .VerifyAnyDomainNotification() + .VerifyExistingNotification( + ErrorCodes.InsufficientPermissions, + $"No permission to delete tenant {command.AggregateId}"); + } } \ No newline at end of file diff --git a/CleanArchitecture.Domain.Tests/CommandHandler/Tenant/DeleteTenant/DeleteTenantCommandTestFixture.cs b/CleanArchitecture.Domain.Tests/CommandHandler/Tenant/DeleteTenant/DeleteTenantCommandTestFixture.cs index 1b8367a..b929df9 100644 --- a/CleanArchitecture.Domain.Tests/CommandHandler/Tenant/DeleteTenant/DeleteTenantCommandTestFixture.cs +++ b/CleanArchitecture.Domain.Tests/CommandHandler/Tenant/DeleteTenant/DeleteTenantCommandTestFixture.cs @@ -1,5 +1,6 @@ using System; using CleanArchitecture.Domain.Commands.Tenants.DeleteTenant; +using CleanArchitecture.Domain.Enums; using CleanArchitecture.Domain.Interfaces.Repositories; using NSubstitute; @@ -36,4 +37,9 @@ public sealed class DeleteTenantCommandTestFixture : CommandHandlerFixtureBase return tenant; } + + public void SetupUser() + { + User.GetUserRole().Returns(UserRole.User); + } } \ No newline at end of file diff --git a/CleanArchitecture.Domain.Tests/CommandHandler/Tenant/UpdateTenant/UpdateTenantCommandHandlerTests.cs b/CleanArchitecture.Domain.Tests/CommandHandler/Tenant/UpdateTenant/UpdateTenantCommandHandlerTests.cs index e391a89..77a186a 100644 --- a/CleanArchitecture.Domain.Tests/CommandHandler/Tenant/UpdateTenant/UpdateTenantCommandHandlerTests.cs +++ b/CleanArchitecture.Domain.Tests/CommandHandler/Tenant/UpdateTenant/UpdateTenantCommandHandlerTests.cs @@ -19,7 +19,7 @@ public sealed class UpdateTenantCommandHandlerTests _fixture.SetupExistingTenant(command.AggregateId); - _fixture.CommandHandler.Handle(command, default!).Wait(); + _fixture.CommandHandler.Handle(command, default).Wait(); _fixture .VerifyCommit() @@ -38,7 +38,7 @@ public sealed class UpdateTenantCommandHandlerTests _fixture.SetupUser(); - _fixture.CommandHandler.Handle(command, default!).Wait(); + _fixture.CommandHandler.Handle(command, default).Wait(); _fixture .VerifyNoCommit() @@ -56,7 +56,7 @@ public sealed class UpdateTenantCommandHandlerTests Guid.NewGuid(), "Tenant Name"); - _fixture.CommandHandler.Handle(command, default!).Wait(); + _fixture.CommandHandler.Handle(command, default).Wait(); _fixture .VerifyNoCommit() diff --git a/CleanArchitecture.Domain.Tests/CommandHandler/User/UpdateUser/UpdateUserCommandHandlerTests.cs b/CleanArchitecture.Domain.Tests/CommandHandler/User/UpdateUser/UpdateUserCommandHandlerTests.cs index 683d677..7542c97 100644 --- a/CleanArchitecture.Domain.Tests/CommandHandler/User/UpdateUser/UpdateUserCommandHandlerTests.cs +++ b/CleanArchitecture.Domain.Tests/CommandHandler/User/UpdateUser/UpdateUserCommandHandlerTests.cs @@ -26,6 +26,8 @@ public sealed class UpdateUserCommandHandlerTests UserRole.User, Guid.NewGuid()); + _fixture.SetupTenant(command.TenantId); + await _fixture.CommandHandler.Handle(command, default); _fixture @@ -46,6 +48,8 @@ public sealed class UpdateUserCommandHandlerTests "Email", UserRole.User, Guid.NewGuid()); + + _fixture.SetupTenant(command.TenantId); await _fixture.CommandHandler.Handle(command, default); @@ -70,6 +74,8 @@ public sealed class UpdateUserCommandHandlerTests "Email", UserRole.User, Guid.NewGuid()); + + _fixture.SetupTenant(command.TenantId); _fixture.UserRepository .GetByEmailAsync(command.Email) @@ -92,4 +98,59 @@ public sealed class UpdateUserCommandHandlerTests DomainErrorCodes.User.UserAlreadyExists, $"There is already a user with email {command.Email}"); } + + [Fact] + public async Task Should_Not_Update_Non_Existing_Tenant() + { + var user = _fixture.SetupUser(); + + var command = new UpdateUserCommand( + user.Id, + "test@email.com", + "Test", + "Email", + UserRole.User, + Guid.NewGuid()); + + await _fixture.CommandHandler.Handle(command, default); + + _fixture + .VerifyNoCommit() + .VerifyNoRaisedEvent() + .VerifyAnyDomainNotification() + .VerifyExistingNotification( + ErrorCodes.ObjectNotFound, + $"There is no tenant with Id {command.TenantId}"); + } + + [Fact] + public async Task Should_Not_Update_Admin_Properties() + { + var user = _fixture.SetupUser(); + _fixture.SetupCurrentUser(user.Id); + + var command = new UpdateUserCommand( + user.Id, + "test@email.com", + "Test", + "Email", + UserRole.Admin, + Guid.NewGuid()); + + _fixture.SetupTenant(command.TenantId); + + await _fixture.CommandHandler.Handle(command, default); + + _fixture.UserRepository.Received(1).Update(Arg.Is(u => + u.TenantId == user.TenantId && + u.Role == user.Role && + u.Id == command.UserId && + u.Email == command.Email && + u.FirstName == command.FirstName)); + + _fixture + .VerifyNoDomainNotification() + .VerifyCommit() + .VerifyRaisedEvent(x => x.AggregateId == command.UserId); + } } \ No newline at end of file diff --git a/CleanArchitecture.Domain.Tests/CommandHandler/User/UpdateUser/UpdateUserCommandTestFixture.cs b/CleanArchitecture.Domain.Tests/CommandHandler/User/UpdateUser/UpdateUserCommandTestFixture.cs index aaf9faf..6f144a0 100644 --- a/CleanArchitecture.Domain.Tests/CommandHandler/User/UpdateUser/UpdateUserCommandTestFixture.cs +++ b/CleanArchitecture.Domain.Tests/CommandHandler/User/UpdateUser/UpdateUserCommandTestFixture.cs @@ -11,17 +11,20 @@ public sealed class UpdateUserCommandTestFixture : CommandHandlerFixtureBase public UpdateUserCommandTestFixture() { UserRepository = Substitute.For(); + TenantRepository = Substitute.For(); CommandHandler = new UpdateUserCommandHandler( Bus, UnitOfWork, NotificationHandler, UserRepository, - User); + User, + TenantRepository); } public UpdateUserCommandHandler CommandHandler { get; } public IUserRepository UserRepository { get; } + private ITenantRepository TenantRepository { get; } public Entities.User SetupUser() { @@ -40,4 +43,21 @@ public sealed class UpdateUserCommandTestFixture : CommandHandlerFixtureBase return user; } + + public Entities.Tenant SetupTenant(Guid tenantId) + { + var tenant = new Entities.Tenant(tenantId, "Name"); + + TenantRepository + .ExistsAsync(Arg.Is(y => y == tenant.Id)) + .Returns(true); + + return tenant; + } + + public void SetupCurrentUser(Guid userId) + { + User.GetUserId().Returns(userId); + User.GetUserRole().Returns(UserRole.User); + } } \ No newline at end of file diff --git a/CleanArchitecture.Domain/Commands/Tenants/DeleteTenant/DeleteTenantCommandHandler.cs b/CleanArchitecture.Domain/Commands/Tenants/DeleteTenant/DeleteTenantCommandHandler.cs index 3a57bed..1303d7b 100644 --- a/CleanArchitecture.Domain/Commands/Tenants/DeleteTenant/DeleteTenantCommandHandler.cs +++ b/CleanArchitecture.Domain/Commands/Tenants/DeleteTenant/DeleteTenantCommandHandler.cs @@ -38,8 +38,6 @@ public sealed class DeleteTenantCommandHandler : CommandHandlerBase, return; } - // Todo: Test following - if (_user.GetUserRole() != UserRole.Admin) { await NotifyAsync( diff --git a/CleanArchitecture.Domain/Commands/Users/UpdateUser/UpdateUserCommandHandler.cs b/CleanArchitecture.Domain/Commands/Users/UpdateUser/UpdateUserCommandHandler.cs index 74b86fc..cbfec13 100644 --- a/CleanArchitecture.Domain/Commands/Users/UpdateUser/UpdateUserCommandHandler.cs +++ b/CleanArchitecture.Domain/Commands/Users/UpdateUser/UpdateUserCommandHandler.cs @@ -15,16 +15,19 @@ public sealed class UpdateUserCommandHandler : CommandHandlerBase, { private readonly IUser _user; private readonly IUserRepository _userRepository; + private readonly ITenantRepository _tenantRepository; public UpdateUserCommandHandler( IMediatorHandler bus, IUnitOfWork unitOfWork, INotificationHandler notifications, IUserRepository userRepository, - IUser user) : base(bus, unitOfWork, notifications) + IUser user, + ITenantRepository tenantRepository) : base(bus, unitOfWork, notifications) { _userRepository = userRepository; _user = user; + _tenantRepository = tenantRepository; } public async Task Handle(UpdateUserCommand request, CancellationToken cancellationToken) @@ -76,8 +79,15 @@ public sealed class UpdateUserCommandHandler : CommandHandlerBase, { user.SetRole(request.Role); - // Todo: Test - // Todo: Check if tenant exists first + if (!await _tenantRepository.ExistsAsync(request.TenantId)) + { + await NotifyAsync( + new DomainNotification( + request.MessageType, + $"There is no tenant with Id {request.TenantId}", + ErrorCodes.ObjectNotFound)); + return; + } user.SetTenant(request.TenantId); } diff --git a/CleanArchitecture.gRPC.Tests/Users/GetUsersByIdsTests.cs b/CleanArchitecture.gRPC.Tests/Users/GetUsersByIdsTests.cs index 4744376..dbac281 100644 --- a/CleanArchitecture.gRPC.Tests/Users/GetUsersByIdsTests.cs +++ b/CleanArchitecture.gRPC.Tests/Users/GetUsersByIdsTests.cs @@ -40,7 +40,6 @@ public sealed class GetUsersByIdsTests : IClassFixture ids.Add(nonExistingId); -// Todo: Use default instead of null everywhere var result = await _fixture.UsersApiImplementation.GetByIds( SetupRequest(ids), default!); From a04cc8bdd07678d1946efbf5f0e4c906a6adbf1c Mon Sep 17 00:00:00 2001 From: alex289 Date: Thu, 31 Aug 2023 16:00:25 +0200 Subject: [PATCH 08/10] test: Refactor authed user --- .../CleanArchitecture.IntegrationTests.csproj | 1 + .../Controller/TenantControllerTests.cs | 10 - .../Controller/UserControllerTests.cs | 190 ++++++++---------- .../Fixtures/AuthTestFixure.cs | 8 + .../Fixtures/TenantTestFixture.cs | 2 + .../Fixtures/TestFixtureBase.cs | 37 ++-- .../Fixtures/UserTestFixture.cs | 14 +- .../Auth/TestAuthenticationExtensions.cs | 17 ++ .../Auth/TestAuthenticationHandler.cs | 29 +++ .../Auth/TestAuthenticationOptions.cs | 24 +++ .../CleanArchitectureWebApplicationFactory.cs | 15 +- .../UtilityTests/AuthTests.cs | 8 +- .../UtilityTests/HealthChecksTests.cs | 6 +- 13 files changed, 207 insertions(+), 154 deletions(-) create mode 100644 CleanArchitecture.IntegrationTests/Fixtures/AuthTestFixure.cs create mode 100644 CleanArchitecture.IntegrationTests/Infrastructure/Auth/TestAuthenticationExtensions.cs create mode 100644 CleanArchitecture.IntegrationTests/Infrastructure/Auth/TestAuthenticationHandler.cs create mode 100644 CleanArchitecture.IntegrationTests/Infrastructure/Auth/TestAuthenticationOptions.cs diff --git a/CleanArchitecture.IntegrationTests/CleanArchitecture.IntegrationTests.csproj b/CleanArchitecture.IntegrationTests/CleanArchitecture.IntegrationTests.csproj index 3aa4f40..56723e6 100644 --- a/CleanArchitecture.IntegrationTests/CleanArchitecture.IntegrationTests.csproj +++ b/CleanArchitecture.IntegrationTests/CleanArchitecture.IntegrationTests.csproj @@ -31,4 +31,5 @@ + diff --git a/CleanArchitecture.IntegrationTests/Controller/TenantControllerTests.cs b/CleanArchitecture.IntegrationTests/Controller/TenantControllerTests.cs index 2f9dd8a..9e7f49f 100644 --- a/CleanArchitecture.IntegrationTests/Controller/TenantControllerTests.cs +++ b/CleanArchitecture.IntegrationTests/Controller/TenantControllerTests.cs @@ -27,8 +27,6 @@ public sealed class TenantControllerTests : IClassFixture [Priority(0)] public async Task Should_Get_Tenant_By_Id() { - await _fixture.AuthenticateUserAsync(); - var response = await _fixture.ServerClient.GetAsync($"/api/v1/Tenant/{_fixture.CreatedTenantId}"); response.StatusCode.Should().Be(HttpStatusCode.OK); @@ -45,8 +43,6 @@ public sealed class TenantControllerTests : IClassFixture [Priority(5)] public async Task Should_Get_All_Tenants() { - await _fixture.AuthenticateUserAsync(); - var response = await _fixture.ServerClient.GetAsync("api/v1/Tenant"); response.StatusCode.Should().Be(HttpStatusCode.OK); @@ -64,8 +60,6 @@ public sealed class TenantControllerTests : IClassFixture [Priority(10)] public async Task Should_Create_Tenant() { - await _fixture.AuthenticateUserAsync(); - var request = new CreateTenantViewModel("Test Tenant 2"); var response = await _fixture.ServerClient.PostAsJsonAsync("/api/v1/Tenant", request); @@ -92,8 +86,6 @@ public sealed class TenantControllerTests : IClassFixture [Priority(15)] public async Task Should_Update_Tenant() { - await _fixture.AuthenticateUserAsync(); - var request = new UpdateTenantViewModel(_fixture.CreatedTenantId, "Test Tenant 3"); var response = await _fixture.ServerClient.PutAsJsonAsync("/api/v1/Tenant", request); @@ -122,8 +114,6 @@ public sealed class TenantControllerTests : IClassFixture [Priority(20)] public async Task Should_Delete_Tenant() { - await _fixture.AuthenticateUserAsync(); - var response = await _fixture.ServerClient.DeleteAsync($"/api/v1/Tenant/{_fixture.CreatedTenantId}"); response.StatusCode.Should().Be(HttpStatusCode.OK); diff --git a/CleanArchitecture.IntegrationTests/Controller/UserControllerTests.cs b/CleanArchitecture.IntegrationTests/Controller/UserControllerTests.cs index b3dd980..b78553f 100644 --- a/CleanArchitecture.IntegrationTests/Controller/UserControllerTests.cs +++ b/CleanArchitecture.IntegrationTests/Controller/UserControllerTests.cs @@ -8,6 +8,7 @@ using CleanArchitecture.Domain.Constants; using CleanArchitecture.Domain.Enums; using CleanArchitecture.IntegrationTests.Extensions; using CleanArchitecture.IntegrationTests.Fixtures; +using CleanArchitecture.IntegrationTests.Infrastructure.Auth; using FluentAssertions; using Xunit; using Xunit.Priority; @@ -24,20 +25,60 @@ public sealed class UserControllerTests : IClassFixture { _fixture = fixture; } - - // Todo: Refactor tests to work alone - + [Fact] [Priority(0)] + public async Task Should_Get_All_User() + { + var response = await _fixture.ServerClient.GetAsync("/api/v1/user"); + + response.StatusCode.Should().Be(HttpStatusCode.OK); + + var message = await response.Content.ReadAsJsonAsync>(); + + message?.Data.Should().NotBeNull(); + + var content = message!.Data!.ToList(); + + content.Count.Should().Be(2); + + var currentUser = content.First(x => x.Id == TestAuthenticationOptions.TestUserId); + + currentUser.Role.Should().Be(UserRole.Admin); + currentUser.Email.Should().Be(TestAuthenticationOptions.Email); + currentUser.FirstName.Should().Be(TestAuthenticationOptions.FirstName); + currentUser.LastName.Should().Be(TestAuthenticationOptions.LastName); + } + + [Fact] + [Priority(5)] + public async Task Should_Get_User_By_Id() + { + var response = await _fixture.ServerClient.GetAsync("/api/v1/user/" + TestAuthenticationOptions.TestUserId); + + response.StatusCode.Should().Be(HttpStatusCode.OK); + + var message = await response.Content.ReadAsJsonAsync(); + + message?.Data.Should().NotBeNull(); + + var content = message!.Data!; + + content.Id.Should().Be(TestAuthenticationOptions.TestUserId); + content.Email.Should().Be(TestAuthenticationOptions.Email); + content.FirstName.Should().Be(TestAuthenticationOptions.FirstName); + content.LastName.Should().Be(TestAuthenticationOptions.LastName); + } + + [Fact] + [Priority(10)] public async Task Should_Create_User() { - await _fixture.AuthenticateUserAsync(); - var user = new CreateUserViewModel( - _fixture.CreatedUserEmail, + "some@user.com", "Test", "Email", - _fixture.CreatedUserPassword, + "1234#KSAD23s", Ids.Seed.TenantId); var response = await _fixture.ServerClient.PostAsJsonAsync("/api/v1/user", user); @@ -45,54 +86,27 @@ public sealed class UserControllerTests : IClassFixture response.StatusCode.Should().Be(HttpStatusCode.OK); var message = await response.Content.ReadAsJsonAsync(); - message?.Data.Should().NotBeEmpty(); - - _fixture.CreatedUserId = message!.Data; } [Fact] - [Priority(5)] + [Priority(15)] public async Task Should_Login_User() { var user = new LoginUserViewModel( - _fixture.CreatedUserEmail, - _fixture.CreatedUserPassword); + "admin@email.com", + "!Password123#"); 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("/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.FirstName.Should().Be("Test"); - content.LastName.Should().Be("Email"); - } - - [Fact] - [Priority(10)] + [Priority(20)] public async Task Should_Get_The_Current_Active_Users() { var response = await _fixture.ServerClient.GetAsync("/api/v1/user/me"); @@ -105,18 +119,18 @@ public sealed class UserControllerTests : IClassFixture var content = message!.Data!; - content.Id.Should().Be(_fixture.CreatedUserId); - content.Email.Should().Be("test@email.com"); - content.FirstName.Should().Be("Test"); - content.LastName.Should().Be("Email"); + content.Id.Should().Be(TestAuthenticationOptions.TestUserId); + content.Email.Should().Be(TestAuthenticationOptions.Email); + content.FirstName.Should().Be(TestAuthenticationOptions.FirstName); + content.LastName.Should().Be(TestAuthenticationOptions.LastName); } [Fact] - [Priority(15)] + [Priority(25)] public async Task Should_Update_User() { var user = new UpdateUserViewModel( - _fixture.CreatedUserId, + Ids.Seed.UserId, "newtest@email.com", "NewTest", "NewEmail", @@ -134,37 +148,32 @@ public sealed class UserControllerTests : IClassFixture var content = message!.Data; content.Should().BeEquivalentTo(user); + + // Check if user is really updated + var userResponse = await _fixture.ServerClient.GetAsync("/api/v1/user/" + user.Id); + + userResponse.StatusCode.Should().Be(HttpStatusCode.OK); + + var userMessage = await userResponse.Content.ReadAsJsonAsync(); + + userMessage?.Data.Should().NotBeNull(); + + var userContent = userMessage!.Data!; + + userContent.Id.Should().Be(user.Id); + userContent.Email.Should().Be(user.Email); + userContent.FirstName.Should().Be(user.FirstName); + userContent.LastName.Should().Be(user.LastName); + userContent.Role.Should().Be(user.Role); } [Fact] - [Priority(20)] - public async Task Should_Get_Updated_Users() - { - 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("newtest@email.com"); - content.FirstName.Should().Be("NewTest"); - content.LastName.Should().Be("NewEmail"); - - _fixture.CreatedUserEmail = content.Email; - } - - [Fact] - [Priority(25)] + [Priority(30)] public async Task Should_Change_User_Password() { var user = new ChangePasswordViewModel( - _fixture.CreatedUserPassword, - _fixture.CreatedUserPassword + "1"); + "!Password123#", + "!Password123#1"); var response = await _fixture.ServerClient.PostAsJsonAsync("/api/v1/user/changePassword", user); @@ -180,8 +189,8 @@ public sealed class UserControllerTests : IClassFixture // Verify the user can login with the new password var login = new LoginUserViewModel( - _fixture.CreatedUserEmail, - _fixture.CreatedUserPassword + "1"); + TestAuthenticationOptions.Email, + user.NewPassword); var loginResponse = await _fixture.ServerClient.PostAsJsonAsync("/api/v1/user/login", login); @@ -192,42 +201,11 @@ public sealed class UserControllerTests : IClassFixture loginMessage?.Data.Should().NotBeEmpty(); } - [Fact] - [Priority(30)] - public async Task Should_Get_All_User() - { - var response = await _fixture.ServerClient.GetAsync("/api/v1/user"); - - response.StatusCode.Should().Be(HttpStatusCode.OK); - - var message = await response.Content.ReadAsJsonAsync>(); - - message?.Data.Should().NotBeNull(); - - 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.FirstName.Should().Be("NewTest"); - currentUser.LastName.Should().Be("NewEmail"); - - var adminUser = content.First(x => x.Role == UserRole.Admin); - - adminUser.Email.Should().Be("admin@email.com"); - adminUser.FirstName.Should().Be("Admin"); - adminUser.LastName.Should().Be("User"); - } - [Fact] [Priority(35)] public async Task Should_Delete_User() { - var response = await _fixture.ServerClient.DeleteAsync("/api/v1/user/" + _fixture.CreatedUserId); + var response = await _fixture.ServerClient.DeleteAsync("/api/v1/user/" + TestAuthenticationOptions.TestUserId); response.StatusCode.Should().Be(HttpStatusCode.OK); @@ -236,8 +214,10 @@ public sealed class UserControllerTests : IClassFixture message?.Data.Should().NotBeEmpty(); var content = message!.Data; - content.Should().Be(_fixture.CreatedUserId); + content.Should().Be(TestAuthenticationOptions.TestUserId); - // Todo: Check if stuff is done + var userResponse = await _fixture.ServerClient.GetAsync("/api/v1/user/" + TestAuthenticationOptions.TestUserId); + + userResponse.StatusCode.Should().Be(HttpStatusCode.NotFound); } } \ No newline at end of file diff --git a/CleanArchitecture.IntegrationTests/Fixtures/AuthTestFixure.cs b/CleanArchitecture.IntegrationTests/Fixtures/AuthTestFixure.cs new file mode 100644 index 0000000..f86ede5 --- /dev/null +++ b/CleanArchitecture.IntegrationTests/Fixtures/AuthTestFixure.cs @@ -0,0 +1,8 @@ +namespace CleanArchitecture.IntegrationTests.Fixtures; + +public sealed class AuthTestFixure : TestFixtureBase +{ + public AuthTestFixure() : base(false) + { + } +} \ No newline at end of file diff --git a/CleanArchitecture.IntegrationTests/Fixtures/TenantTestFixture.cs b/CleanArchitecture.IntegrationTests/Fixtures/TenantTestFixture.cs index dffe6cc..437d39f 100644 --- a/CleanArchitecture.IntegrationTests/Fixtures/TenantTestFixture.cs +++ b/CleanArchitecture.IntegrationTests/Fixtures/TenantTestFixture.cs @@ -10,6 +10,8 @@ public sealed class TenantTestFixture : TestFixtureBase protected override void SeedTestData(ApplicationDbContext context) { + base.SeedTestData(context); + context.Tenants.Add(new Tenant( CreatedTenantId, "Test Tenant")); diff --git a/CleanArchitecture.IntegrationTests/Fixtures/TestFixtureBase.cs b/CleanArchitecture.IntegrationTests/Fixtures/TestFixtureBase.cs index 0be073f..29bcd0f 100644 --- a/CleanArchitecture.IntegrationTests/Fixtures/TestFixtureBase.cs +++ b/CleanArchitecture.IntegrationTests/Fixtures/TestFixtureBase.cs @@ -1,10 +1,11 @@ using System; using System.Net.Http; -using System.Threading.Tasks; -using CleanArchitecture.Application.ViewModels.Users; +using CleanArchitecture.Domain.Constants; +using CleanArchitecture.Domain.Entities; +using CleanArchitecture.Domain.Enums; using CleanArchitecture.Infrastructure.Database; -using CleanArchitecture.IntegrationTests.Extensions; using CleanArchitecture.IntegrationTests.Infrastructure; +using CleanArchitecture.IntegrationTests.Infrastructure.Auth; using Microsoft.AspNetCore.Mvc.Testing; using Microsoft.Extensions.DependencyInjection; @@ -12,11 +13,12 @@ namespace CleanArchitecture.IntegrationTests.Fixtures; public class TestFixtureBase { - public TestFixtureBase() + public TestFixtureBase(bool useTestAuthentication = true) { Factory = new CleanArchitectureWebApplicationFactory( SeedTestData, - RegisterCustomServicesHandler); + RegisterCustomServicesHandler, + useTestAuthentication); ServerClient = Factory.CreateClient(); ServerClient.Timeout = TimeSpan.FromMinutes(5); @@ -27,6 +29,17 @@ public class TestFixtureBase protected virtual void SeedTestData(ApplicationDbContext context) { + context.Users.Add(new User( + TestAuthenticationOptions.TestUserId, + Ids.Seed.TenantId, + TestAuthenticationOptions.Email, + TestAuthenticationOptions.FirstName, + TestAuthenticationOptions.LastName, + // !Password123# + "$2a$12$Blal/uiFIJdYsCLTMUik/egLbfg3XhbnxBC6Sb5IKz2ZYhiU/MzL2", + UserRole.Admin)); + + context.SaveChanges(); } protected virtual void RegisterCustomServicesHandler( @@ -35,18 +48,4 @@ public class TestFixtureBase IServiceProvider scopedServices) { } - - // Todo: Fix auth - public virtual async Task AuthenticateUserAsync() - { - ServerClient.DefaultRequestHeaders.Clear(); - var user = new LoginUserViewModel( - "admin@email.com", - "!Password123#"); - - var response = await ServerClient.PostAsJsonAsync("/api/v1/user/login", user); - - var message = await response.Content.ReadAsJsonAsync(); - ServerClient.DefaultRequestHeaders.Add("Authorization", $"Bearer {message!.Data}"); - } } \ No newline at end of file diff --git a/CleanArchitecture.IntegrationTests/Fixtures/UserTestFixture.cs b/CleanArchitecture.IntegrationTests/Fixtures/UserTestFixture.cs index e2421d2..67baaa6 100644 --- a/CleanArchitecture.IntegrationTests/Fixtures/UserTestFixture.cs +++ b/CleanArchitecture.IntegrationTests/Fixtures/UserTestFixture.cs @@ -1,17 +1,5 @@ -using System; - -namespace CleanArchitecture.IntegrationTests.Fixtures; +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.Clear(); - ServerClient.DefaultRequestHeaders.Add("Authorization", $"Bearer {CreatedUserToken}"); - } } \ No newline at end of file diff --git a/CleanArchitecture.IntegrationTests/Infrastructure/Auth/TestAuthenticationExtensions.cs b/CleanArchitecture.IntegrationTests/Infrastructure/Auth/TestAuthenticationExtensions.cs new file mode 100644 index 0000000..64fb224 --- /dev/null +++ b/CleanArchitecture.IntegrationTests/Infrastructure/Auth/TestAuthenticationExtensions.cs @@ -0,0 +1,17 @@ +using System; +using Microsoft.AspNetCore.Authentication; + +namespace CleanArchitecture.IntegrationTests.Infrastructure.Auth; + +public static class TestAuthenticationExtensions +{ + public static AuthenticationBuilder AddTestAuthentication( + this AuthenticationBuilder builder, + Action configureOptions) + { + return builder.AddScheme( + "Testing", + "Test Authentication", + configureOptions); + } +} \ No newline at end of file diff --git a/CleanArchitecture.IntegrationTests/Infrastructure/Auth/TestAuthenticationHandler.cs b/CleanArchitecture.IntegrationTests/Infrastructure/Auth/TestAuthenticationHandler.cs new file mode 100644 index 0000000..db98295 --- /dev/null +++ b/CleanArchitecture.IntegrationTests/Infrastructure/Auth/TestAuthenticationHandler.cs @@ -0,0 +1,29 @@ +using System.Security.Claims; +using System.Text.Encodings.Web; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authentication; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; + +namespace CleanArchitecture.IntegrationTests.Infrastructure.Auth; + +public sealed class TestAuthenticationHandler : AuthenticationHandler +{ + public TestAuthenticationHandler( + IOptionsMonitor options, + ILoggerFactory logger, + UrlEncoder encoder, + ISystemClock clock) : base(options, logger, encoder, clock) + { + } + + protected override Task HandleAuthenticateAsync() + { + var authenticationTicket = new AuthenticationTicket( + new ClaimsPrincipal(Options.Identity), + new AuthenticationProperties(), + "Testing"); + + return Task.FromResult(AuthenticateResult.Success(authenticationTicket)); + } +} \ No newline at end of file diff --git a/CleanArchitecture.IntegrationTests/Infrastructure/Auth/TestAuthenticationOptions.cs b/CleanArchitecture.IntegrationTests/Infrastructure/Auth/TestAuthenticationOptions.cs new file mode 100644 index 0000000..d301b34 --- /dev/null +++ b/CleanArchitecture.IntegrationTests/Infrastructure/Auth/TestAuthenticationOptions.cs @@ -0,0 +1,24 @@ +using System; +using System.Security.Claims; +using CleanArchitecture.Domain.Enums; +using Microsoft.AspNetCore.Authentication; + +namespace CleanArchitecture.IntegrationTests.Infrastructure.Auth; + +public sealed class TestAuthenticationOptions : AuthenticationSchemeOptions +{ + public static Guid TestUserId = new("561e4300-94d6-4c3f-adf5-31c1bdbc64df"); + public const string Email = "integration@tests.com"; + public const string FirstName = "Integration"; + public const string LastName = "Tests"; + + public ClaimsIdentity Identity { get; } = new ClaimsIdentity( + new[] + { + new Claim(ClaimTypes.Email, Email), + new Claim(ClaimTypes.Role, UserRole.Admin.ToString()), + new Claim(ClaimTypes.NameIdentifier, TestUserId.ToString()), + new Claim(ClaimTypes.Name, $"{FirstName} {LastName}") + }, + "test"); +} \ No newline at end of file diff --git a/CleanArchitecture.IntegrationTests/Infrastructure/CleanArchitectureWebApplicationFactory.cs b/CleanArchitecture.IntegrationTests/Infrastructure/CleanArchitectureWebApplicationFactory.cs index 9a52c4d..1c771cc 100644 --- a/CleanArchitecture.IntegrationTests/Infrastructure/CleanArchitectureWebApplicationFactory.cs +++ b/CleanArchitecture.IntegrationTests/Infrastructure/CleanArchitectureWebApplicationFactory.cs @@ -2,6 +2,7 @@ using CleanArchitecture.Infrastructure.Database; using CleanArchitecture.Infrastructure.Extensions; using CleanArchitecture.IntegrationTests.Extensions; +using CleanArchitecture.IntegrationTests.Infrastructure.Auth; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Mvc.Testing; using Microsoft.Data.Sqlite; @@ -24,13 +25,16 @@ public sealed class CleanArchitectureWebApplicationFactory : WebApplicationFacto private readonly SqliteConnection _connection = new("DataSource=:memory:"); private readonly RegisterCustomServicesHandler? _registerCustomServicesHandler; + private readonly bool _addTestAuthentication; public CleanArchitectureWebApplicationFactory( AddCustomSeedDataHandler? addCustomSeedDataHandler, - RegisterCustomServicesHandler? registerCustomServicesHandler) + RegisterCustomServicesHandler? registerCustomServicesHandler, + bool addTestAuthentication) { _addCustomSeedDataHandler = addCustomSeedDataHandler; _registerCustomServicesHandler = registerCustomServicesHandler; + _addTestAuthentication = addTestAuthentication; } protected override void ConfigureWebHost(IWebHostBuilder builder) @@ -47,6 +51,15 @@ public sealed class CleanArchitectureWebApplicationFactory : WebApplicationFacto services.SetupTestDatabase(_connection); services.SetupTestDatabase(_connection); + if (_addTestAuthentication) + { + services.AddAuthentication(options => + { + options.DefaultAuthenticateScheme = "Testing"; + options.DefaultChallengeScheme = "Testing"; + }).AddTestAuthentication(options => { }); + } + var sp = services.BuildServiceProvider(); using var scope = sp.CreateScope(); diff --git a/CleanArchitecture.IntegrationTests/UtilityTests/AuthTests.cs b/CleanArchitecture.IntegrationTests/UtilityTests/AuthTests.cs index a0e7773..7f3f26b 100644 --- a/CleanArchitecture.IntegrationTests/UtilityTests/AuthTests.cs +++ b/CleanArchitecture.IntegrationTests/UtilityTests/AuthTests.cs @@ -9,11 +9,11 @@ namespace CleanArchitecture.IntegrationTests.UtilityTests; [Collection("IntegrationTests")] [TestCaseOrderer(PriorityOrderer.Name, PriorityOrderer.Assembly)] -public sealed class AuthTests : IClassFixture +public sealed class AuthTests : IClassFixture { - private readonly TestFixtureBase _fixture; + private readonly AuthTestFixure _fixture; - public AuthTests(TestFixtureBase fixture) + public AuthTests(AuthTestFixure fixture) { _fixture = fixture; } @@ -22,6 +22,8 @@ public sealed class AuthTests : IClassFixture [InlineData("/api/v1/user")] [InlineData("/api/v1/user/me")] [InlineData("/api/v1/user/d74b112a-ece0-443d-9b4f-85bc418822ca")] + [InlineData("/api/v1/tenant")] + [InlineData("/api/v1/tenant/d74b112a-ece0-443d-9b4f-85bc418822ca")] public async Task Should_Get_Unauthorized_If_Trying_To_Call_Endpoint_Without_Token( string url) { diff --git a/CleanArchitecture.IntegrationTests/UtilityTests/HealthChecksTests.cs b/CleanArchitecture.IntegrationTests/UtilityTests/HealthChecksTests.cs index c13a61b..779f085 100644 --- a/CleanArchitecture.IntegrationTests/UtilityTests/HealthChecksTests.cs +++ b/CleanArchitecture.IntegrationTests/UtilityTests/HealthChecksTests.cs @@ -11,11 +11,11 @@ namespace CleanArchitecture.IntegrationTests.UtilityTests; [Collection("IntegrationTests")] [TestCaseOrderer(PriorityOrderer.Name, PriorityOrderer.Assembly)] -public sealed class HealthChecksTests : IClassFixture +public sealed class HealthChecksTests : IClassFixture { - private readonly TestFixtureBase _fixture; + private readonly AuthTestFixure _fixture; - public HealthChecksTests(TestFixtureBase fixture) + public HealthChecksTests(AuthTestFixure fixture) { _fixture = fixture; } From 9ba530417cfad29c3a43b54b98977a590490233c Mon Sep 17 00:00:00 2001 From: alex289 Date: Thu, 31 Aug 2023 18:19:17 +0200 Subject: [PATCH 09/10] chore: Move properties over ctor --- .../Tenants/GetAllTenantsTestFixture.cs | 6 ++-- .../Tenants/GetTenantByIdTestFixture.cs | 6 ++-- .../Queries/Users/GetAllUsersTestFixture.cs | 8 +++--- .../Queries/Users/GetUserByIdTestFixture.cs | 8 +++--- .../CreateTenantCommandTestFixture.cs | 8 +++--- .../DeleteTenantCommandTestFixture.cs | 12 ++++---- .../UpdateTenantCommandTestFixture.cs | 8 +++--- .../ChangePasswordCommandTestFixture.cs | 6 ++-- .../CreateUserCommandTestFixture.cs | 11 ++++---- .../DeleteUserCommandTestFixture.cs | 6 ++-- .../LoginUser/LoginUserCommandTestFixture.cs | 8 +++--- .../UpdateUserCommandHandlerTests.cs | 16 +++++------ .../UpdateUserCommandTestFixture.cs | 10 +++---- .../CommandHandlerFixtureBase.cs | 10 +++---- .../Commands/CommandBase.cs | 10 +++---- .../CreateTenant/CreateTenantCommand.cs | 4 +-- .../UpdateTenant/UpdateTenantCommand.cs | 4 +-- .../ChangePassword/ChangePasswordCommand.cs | 6 ++-- .../Users/CreateUser/CreateUserCommand.cs | 14 +++++----- .../Users/DeleteUser/DeleteUserCommand.cs | 4 +-- .../Users/LoginUser/LoginUserCommand.cs | 6 ++-- .../Users/UpdateUser/UpdateUserCommand.cs | 14 +++++----- .../UpdateUser/UpdateUserCommandHandler.cs | 3 +- .../DomainEvents/DomainEvent.cs | 4 +-- .../DomainEvents/Message.cs | 6 ++-- .../DomainEvents/StoredDomainEvent.cs | 10 +++---- .../StoredDomainNotification.cs | 10 +++---- CleanArchitecture.Domain/Entities/Entity.cs | 6 ++-- CleanArchitecture.Domain/Entities/Tenant.cs | 8 +++--- CleanArchitecture.Domain/Entities/User.cs | 22 +++++++-------- .../Events/Tenant/TenantCreatedEvent.cs | 4 +-- .../Events/Tenant/TenantUpdatedEvent.cs | 4 +-- .../Notifications/DomainNotification.cs | 12 ++++---- .../Database/ApplicationDbContext.cs | 6 ++-- .../DomainNotificationStoreDbContext.cs | 4 +-- .../Database/EventStoreDbContext.cs | 4 +-- .../Controller/UserControllerTests.cs | 28 +++++++++---------- .../Fixtures/TenantTestFixture.cs | 2 +- .../Fixtures/TestFixtureBase.cs | 6 ++-- .../gRPC/GetTenantsByIdsTestFixture.cs | 6 ++-- .../Fixtures/gRPC/GetUsersByIdsTestFixture.cs | 6 ++-- .../Auth/TestAuthenticationOptions.cs | 4 +-- .../CleanArchitectureWebApplicationFactory.cs | 2 +- .../Fixtures/TenantTestFixture.cs | 10 +++---- .../Fixtures/UserTestFixture.cs | 12 ++++---- 45 files changed, 182 insertions(+), 182 deletions(-) diff --git a/CleanArchitecture.Application.Tests/Fixtures/Queries/Tenants/GetAllTenantsTestFixture.cs b/CleanArchitecture.Application.Tests/Fixtures/Queries/Tenants/GetAllTenantsTestFixture.cs index ef21f15..b13ac69 100644 --- a/CleanArchitecture.Application.Tests/Fixtures/Queries/Tenants/GetAllTenantsTestFixture.cs +++ b/CleanArchitecture.Application.Tests/Fixtures/Queries/Tenants/GetAllTenantsTestFixture.cs @@ -10,6 +10,9 @@ namespace CleanArchitecture.Application.Tests.Fixtures.Queries.Tenants; public sealed class GetAllTenantsTestFixture : QueryHandlerBaseFixture { + public GetAllTenantsQueryHandler QueryHandler { get; } + private ITenantRepository TenantRepository { get; } + public GetAllTenantsTestFixture() { TenantRepository = Substitute.For(); @@ -17,9 +20,6 @@ public sealed class GetAllTenantsTestFixture : QueryHandlerBaseFixture QueryHandler = new GetAllTenantsQueryHandler(TenantRepository); } - public GetAllTenantsQueryHandler QueryHandler { get; } - private ITenantRepository TenantRepository { get; } - public Tenant SetupTenant(bool deleted = false) { var tenant = new Tenant(Guid.NewGuid(), "Tenant 1"); diff --git a/CleanArchitecture.Application.Tests/Fixtures/Queries/Tenants/GetTenantByIdTestFixture.cs b/CleanArchitecture.Application.Tests/Fixtures/Queries/Tenants/GetTenantByIdTestFixture.cs index ac12b58..e5d0622 100644 --- a/CleanArchitecture.Application.Tests/Fixtures/Queries/Tenants/GetTenantByIdTestFixture.cs +++ b/CleanArchitecture.Application.Tests/Fixtures/Queries/Tenants/GetTenantByIdTestFixture.cs @@ -10,6 +10,9 @@ namespace CleanArchitecture.Application.Tests.Fixtures.Queries.Tenants; public sealed class GetTenantByIdTestFixture : QueryHandlerBaseFixture { + public GetTenantByIdQueryHandler QueryHandler { get; } + private ITenantRepository TenantRepository { get; } + public GetTenantByIdTestFixture() { TenantRepository = Substitute.For(); @@ -19,9 +22,6 @@ public sealed class GetTenantByIdTestFixture : QueryHandlerBaseFixture Bus); } - public GetTenantByIdQueryHandler QueryHandler { get; } - private ITenantRepository TenantRepository { get; } - public Tenant SetupTenant(bool deleted = false) { var tenant = new Tenant(Guid.NewGuid(), "Tenant 1"); diff --git a/CleanArchitecture.Application.Tests/Fixtures/Queries/Users/GetAllUsersTestFixture.cs b/CleanArchitecture.Application.Tests/Fixtures/Queries/Users/GetAllUsersTestFixture.cs index efdabee..61b70d7 100644 --- a/CleanArchitecture.Application.Tests/Fixtures/Queries/Users/GetAllUsersTestFixture.cs +++ b/CleanArchitecture.Application.Tests/Fixtures/Queries/Users/GetAllUsersTestFixture.cs @@ -10,6 +10,10 @@ namespace CleanArchitecture.Application.Tests.Fixtures.Queries.Users; public sealed class GetAllUsersTestFixture : QueryHandlerBaseFixture { + private IUserRepository UserRepository { get; } + public GetAllUsersQueryHandler Handler { get; } + public Guid ExistingUserId { get; } = Guid.NewGuid(); + public GetAllUsersTestFixture() { UserRepository = Substitute.For(); @@ -17,10 +21,6 @@ public sealed class GetAllUsersTestFixture : QueryHandlerBaseFixture Handler = new GetAllUsersQueryHandler(UserRepository); } - private IUserRepository UserRepository { get; } - public GetAllUsersQueryHandler Handler { get; } - public Guid ExistingUserId { get; } = Guid.NewGuid(); - public void SetupUserAsync() { var user = new User( diff --git a/CleanArchitecture.Application.Tests/Fixtures/Queries/Users/GetUserByIdTestFixture.cs b/CleanArchitecture.Application.Tests/Fixtures/Queries/Users/GetUserByIdTestFixture.cs index 11b407f..9464fed 100644 --- a/CleanArchitecture.Application.Tests/Fixtures/Queries/Users/GetUserByIdTestFixture.cs +++ b/CleanArchitecture.Application.Tests/Fixtures/Queries/Users/GetUserByIdTestFixture.cs @@ -11,6 +11,10 @@ namespace CleanArchitecture.Application.Tests.Fixtures.Queries.Users; public sealed class GetUserByIdTestFixture : QueryHandlerBaseFixture { + private IUserRepository UserRepository { get; } + public GetUserByIdQueryHandler Handler { get; } + public Guid ExistingUserId { get; } = Guid.NewGuid(); + public GetUserByIdTestFixture() { UserRepository = Substitute.For(); @@ -18,10 +22,6 @@ public sealed class GetUserByIdTestFixture : QueryHandlerBaseFixture Handler = new GetUserByIdQueryHandler(UserRepository, Bus); } - private IUserRepository UserRepository { get; } - public GetUserByIdQueryHandler Handler { get; } - public Guid ExistingUserId { get; } = Guid.NewGuid(); - public void SetupUserAsync() { var user = new User( diff --git a/CleanArchitecture.Domain.Tests/CommandHandler/Tenant/CreateTenant/CreateTenantCommandTestFixture.cs b/CleanArchitecture.Domain.Tests/CommandHandler/Tenant/CreateTenant/CreateTenantCommandTestFixture.cs index d2ca1f2..3fc9210 100644 --- a/CleanArchitecture.Domain.Tests/CommandHandler/Tenant/CreateTenant/CreateTenantCommandTestFixture.cs +++ b/CleanArchitecture.Domain.Tests/CommandHandler/Tenant/CreateTenant/CreateTenantCommandTestFixture.cs @@ -8,6 +8,10 @@ namespace CleanArchitecture.Domain.Tests.CommandHandler.Tenant.CreateTenant; public sealed class CreateTenantCommandTestFixture : CommandHandlerFixtureBase { + public CreateTenantCommandHandler CommandHandler { get; } + + private ITenantRepository TenantRepository { get; } + public CreateTenantCommandTestFixture() { TenantRepository = Substitute.For(); @@ -20,10 +24,6 @@ public sealed class CreateTenantCommandTestFixture : CommandHandlerFixtureBase User); } - public CreateTenantCommandHandler CommandHandler { get; } - - private ITenantRepository TenantRepository { get; } - public void SetupUser() { User.GetUserRole().Returns(UserRole.User); diff --git a/CleanArchitecture.Domain.Tests/CommandHandler/Tenant/DeleteTenant/DeleteTenantCommandTestFixture.cs b/CleanArchitecture.Domain.Tests/CommandHandler/Tenant/DeleteTenant/DeleteTenantCommandTestFixture.cs index b929df9..0617c3b 100644 --- a/CleanArchitecture.Domain.Tests/CommandHandler/Tenant/DeleteTenant/DeleteTenantCommandTestFixture.cs +++ b/CleanArchitecture.Domain.Tests/CommandHandler/Tenant/DeleteTenant/DeleteTenantCommandTestFixture.cs @@ -8,6 +8,11 @@ namespace CleanArchitecture.Domain.Tests.CommandHandler.Tenant.DeleteTenant; public sealed class DeleteTenantCommandTestFixture : CommandHandlerFixtureBase { + public DeleteTenantCommandHandler CommandHandler { get; } + + private ITenantRepository TenantRepository { get; } + private IUserRepository UserRepository { get; } + public DeleteTenantCommandTestFixture() { TenantRepository = Substitute.For(); @@ -22,11 +27,6 @@ public sealed class DeleteTenantCommandTestFixture : CommandHandlerFixtureBase User); } - public DeleteTenantCommandHandler CommandHandler { get; } - - private ITenantRepository TenantRepository { get; } - private IUserRepository UserRepository { get; } - public Entities.Tenant SetupTenant() { var tenant = new Entities.Tenant(Guid.NewGuid(), "TestTenant"); @@ -37,7 +37,7 @@ public sealed class DeleteTenantCommandTestFixture : CommandHandlerFixtureBase return tenant; } - + public void SetupUser() { User.GetUserRole().Returns(UserRole.User); diff --git a/CleanArchitecture.Domain.Tests/CommandHandler/Tenant/UpdateTenant/UpdateTenantCommandTestFixture.cs b/CleanArchitecture.Domain.Tests/CommandHandler/Tenant/UpdateTenant/UpdateTenantCommandTestFixture.cs index dbd1695..ddf762a 100644 --- a/CleanArchitecture.Domain.Tests/CommandHandler/Tenant/UpdateTenant/UpdateTenantCommandTestFixture.cs +++ b/CleanArchitecture.Domain.Tests/CommandHandler/Tenant/UpdateTenant/UpdateTenantCommandTestFixture.cs @@ -8,6 +8,10 @@ namespace CleanArchitecture.Domain.Tests.CommandHandler.Tenant.UpdateTenant; public sealed class UpdateTenantCommandTestFixture : CommandHandlerFixtureBase { + public UpdateTenantCommandHandler CommandHandler { get; } + + private ITenantRepository TenantRepository { get; } + public UpdateTenantCommandTestFixture() { TenantRepository = Substitute.For(); @@ -20,10 +24,6 @@ public sealed class UpdateTenantCommandTestFixture : CommandHandlerFixtureBase User); } - public UpdateTenantCommandHandler CommandHandler { get; } - - private ITenantRepository TenantRepository { get; } - public void SetupUser() { User.GetUserRole().Returns(UserRole.User); diff --git a/CleanArchitecture.Domain.Tests/CommandHandler/User/ChangePassword/ChangePasswordCommandTestFixture.cs b/CleanArchitecture.Domain.Tests/CommandHandler/User/ChangePassword/ChangePasswordCommandTestFixture.cs index 314716b..5da9561 100644 --- a/CleanArchitecture.Domain.Tests/CommandHandler/User/ChangePassword/ChangePasswordCommandTestFixture.cs +++ b/CleanArchitecture.Domain.Tests/CommandHandler/User/ChangePassword/ChangePasswordCommandTestFixture.cs @@ -9,6 +9,9 @@ namespace CleanArchitecture.Domain.Tests.CommandHandler.User.ChangePassword; public sealed class ChangePasswordCommandTestFixture : CommandHandlerFixtureBase { + public ChangePasswordCommandHandler CommandHandler { get; } + private IUserRepository UserRepository { get; } + public ChangePasswordCommandTestFixture() { UserRepository = Substitute.For(); @@ -21,9 +24,6 @@ public sealed class ChangePasswordCommandTestFixture : CommandHandlerFixtureBase User); } - public ChangePasswordCommandHandler CommandHandler { get; } - private IUserRepository UserRepository { get; } - public Entities.User SetupUser() { var user = new Entities.User( diff --git a/CleanArchitecture.Domain.Tests/CommandHandler/User/CreateUser/CreateUserCommandTestFixture.cs b/CleanArchitecture.Domain.Tests/CommandHandler/User/CreateUser/CreateUserCommandTestFixture.cs index 6d6e5dc..812257f 100644 --- a/CleanArchitecture.Domain.Tests/CommandHandler/User/CreateUser/CreateUserCommandTestFixture.cs +++ b/CleanArchitecture.Domain.Tests/CommandHandler/User/CreateUser/CreateUserCommandTestFixture.cs @@ -9,6 +9,11 @@ namespace CleanArchitecture.Domain.Tests.CommandHandler.User.CreateUser; public sealed class CreateUserCommandTestFixture : CommandHandlerFixtureBase { + public CreateUserCommandHandler CommandHandler { get; } + private IUserRepository UserRepository { get; } + private ITenantRepository TenantRepository { get; } + private IUser User { get; } + public CreateUserCommandTestFixture() { UserRepository = Substitute.For(); @@ -24,12 +29,6 @@ public sealed class CreateUserCommandTestFixture : CommandHandlerFixtureBase User); } - // Todo: Properties over ctor - public CreateUserCommandHandler CommandHandler { get; } - private IUserRepository UserRepository { get; } - private ITenantRepository TenantRepository { get; } - private IUser User { get; } - public Entities.User SetupUser() { var user = new Entities.User( diff --git a/CleanArchitecture.Domain.Tests/CommandHandler/User/DeleteUser/DeleteUserCommandTestFixture.cs b/CleanArchitecture.Domain.Tests/CommandHandler/User/DeleteUser/DeleteUserCommandTestFixture.cs index 89c4eee..96f7d5d 100644 --- a/CleanArchitecture.Domain.Tests/CommandHandler/User/DeleteUser/DeleteUserCommandTestFixture.cs +++ b/CleanArchitecture.Domain.Tests/CommandHandler/User/DeleteUser/DeleteUserCommandTestFixture.cs @@ -8,6 +8,9 @@ namespace CleanArchitecture.Domain.Tests.CommandHandler.User.DeleteUser; public sealed class DeleteUserCommandTestFixture : CommandHandlerFixtureBase { + public DeleteUserCommandHandler CommandHandler { get; } + private IUserRepository UserRepository { get; } + public DeleteUserCommandTestFixture() { UserRepository = Substitute.For(); @@ -20,9 +23,6 @@ public sealed class DeleteUserCommandTestFixture : CommandHandlerFixtureBase User); } - public DeleteUserCommandHandler CommandHandler { get; } - private IUserRepository UserRepository { get; } - public Entities.User SetupUser() { var user = new Entities.User( diff --git a/CleanArchitecture.Domain.Tests/CommandHandler/User/LoginUser/LoginUserCommandTestFixture.cs b/CleanArchitecture.Domain.Tests/CommandHandler/User/LoginUser/LoginUserCommandTestFixture.cs index 7a08da8..155b6b7 100644 --- a/CleanArchitecture.Domain.Tests/CommandHandler/User/LoginUser/LoginUserCommandTestFixture.cs +++ b/CleanArchitecture.Domain.Tests/CommandHandler/User/LoginUser/LoginUserCommandTestFixture.cs @@ -11,6 +11,10 @@ namespace CleanArchitecture.Domain.Tests.CommandHandler.User.LoginUser; public sealed class LoginUserCommandTestFixture : CommandHandlerFixtureBase { + public LoginUserCommandHandler CommandHandler { get; set; } + public IUserRepository UserRepository { get; set; } + public IOptions TokenSettings { get; set; } + public LoginUserCommandTestFixture() { UserRepository = Substitute.For(); @@ -30,10 +34,6 @@ public sealed class LoginUserCommandTestFixture : CommandHandlerFixtureBase TokenSettings); } - public LoginUserCommandHandler CommandHandler { get; set; } - public IUserRepository UserRepository { get; set; } - public IOptions TokenSettings { get; set; } - public Entities.User SetupUser() { var user = new Entities.User( diff --git a/CleanArchitecture.Domain.Tests/CommandHandler/User/UpdateUser/UpdateUserCommandHandlerTests.cs b/CleanArchitecture.Domain.Tests/CommandHandler/User/UpdateUser/UpdateUserCommandHandlerTests.cs index 7542c97..c1229b7 100644 --- a/CleanArchitecture.Domain.Tests/CommandHandler/User/UpdateUser/UpdateUserCommandHandlerTests.cs +++ b/CleanArchitecture.Domain.Tests/CommandHandler/User/UpdateUser/UpdateUserCommandHandlerTests.cs @@ -48,7 +48,7 @@ public sealed class UpdateUserCommandHandlerTests "Email", UserRole.User, Guid.NewGuid()); - + _fixture.SetupTenant(command.TenantId); await _fixture.CommandHandler.Handle(command, default); @@ -74,7 +74,7 @@ public sealed class UpdateUserCommandHandlerTests "Email", UserRole.User, Guid.NewGuid()); - + _fixture.SetupTenant(command.TenantId); _fixture.UserRepository @@ -98,7 +98,7 @@ public sealed class UpdateUserCommandHandlerTests DomainErrorCodes.User.UserAlreadyExists, $"There is already a user with email {command.Email}"); } - + [Fact] public async Task Should_Not_Update_Non_Existing_Tenant() { @@ -111,7 +111,7 @@ public sealed class UpdateUserCommandHandlerTests "Email", UserRole.User, Guid.NewGuid()); - + await _fixture.CommandHandler.Handle(command, default); _fixture @@ -122,13 +122,13 @@ public sealed class UpdateUserCommandHandlerTests ErrorCodes.ObjectNotFound, $"There is no tenant with Id {command.TenantId}"); } - + [Fact] public async Task Should_Not_Update_Admin_Properties() { var user = _fixture.SetupUser(); _fixture.SetupCurrentUser(user.Id); - + var command = new UpdateUserCommand( user.Id, "test@email.com", @@ -140,8 +140,8 @@ public sealed class UpdateUserCommandHandlerTests _fixture.SetupTenant(command.TenantId); await _fixture.CommandHandler.Handle(command, default); - - _fixture.UserRepository.Received(1).Update(Arg.Is(u => + + _fixture.UserRepository.Received(1).Update(Arg.Is(u => u.TenantId == user.TenantId && u.Role == user.Role && u.Id == command.UserId && diff --git a/CleanArchitecture.Domain.Tests/CommandHandler/User/UpdateUser/UpdateUserCommandTestFixture.cs b/CleanArchitecture.Domain.Tests/CommandHandler/User/UpdateUser/UpdateUserCommandTestFixture.cs index 6f144a0..4e572d4 100644 --- a/CleanArchitecture.Domain.Tests/CommandHandler/User/UpdateUser/UpdateUserCommandTestFixture.cs +++ b/CleanArchitecture.Domain.Tests/CommandHandler/User/UpdateUser/UpdateUserCommandTestFixture.cs @@ -8,6 +8,10 @@ namespace CleanArchitecture.Domain.Tests.CommandHandler.User.UpdateUser; public sealed class UpdateUserCommandTestFixture : CommandHandlerFixtureBase { + public UpdateUserCommandHandler CommandHandler { get; } + public IUserRepository UserRepository { get; } + private ITenantRepository TenantRepository { get; } + public UpdateUserCommandTestFixture() { UserRepository = Substitute.For(); @@ -22,10 +26,6 @@ public sealed class UpdateUserCommandTestFixture : CommandHandlerFixtureBase TenantRepository); } - public UpdateUserCommandHandler CommandHandler { get; } - public IUserRepository UserRepository { get; } - private ITenantRepository TenantRepository { get; } - public Entities.User SetupUser() { var user = new Entities.User( @@ -47,7 +47,7 @@ public sealed class UpdateUserCommandTestFixture : CommandHandlerFixtureBase public Entities.Tenant SetupTenant(Guid tenantId) { var tenant = new Entities.Tenant(tenantId, "Name"); - + TenantRepository .ExistsAsync(Arg.Is(y => y == tenant.Id)) .Returns(true); diff --git a/CleanArchitecture.Domain.Tests/CommandHandlerFixtureBase.cs b/CleanArchitecture.Domain.Tests/CommandHandlerFixtureBase.cs index 27b46ef..1d5087b 100644 --- a/CleanArchitecture.Domain.Tests/CommandHandlerFixtureBase.cs +++ b/CleanArchitecture.Domain.Tests/CommandHandlerFixtureBase.cs @@ -10,6 +10,11 @@ namespace CleanArchitecture.Domain.Tests; public class CommandHandlerFixtureBase { + protected IMediatorHandler Bus { get; } + protected IUnitOfWork UnitOfWork { get; } + protected DomainNotificationHandler NotificationHandler { get; } + protected IUser User { get; } + protected CommandHandlerFixtureBase() { Bus = Substitute.For(); @@ -23,11 +28,6 @@ public class CommandHandlerFixtureBase UnitOfWork.CommitAsync().Returns(true); } - protected IMediatorHandler Bus { get; } - protected IUnitOfWork UnitOfWork { get; } - protected DomainNotificationHandler NotificationHandler { get; } - protected IUser User { get; } - public CommandHandlerFixtureBase VerifyExistingNotification(string errorCode, string message) { Bus.Received(1).RaiseEventAsync( diff --git a/CleanArchitecture.Domain/Commands/CommandBase.cs b/CleanArchitecture.Domain/Commands/CommandBase.cs index 02d78d1..d659231 100644 --- a/CleanArchitecture.Domain/Commands/CommandBase.cs +++ b/CleanArchitecture.Domain/Commands/CommandBase.cs @@ -6,6 +6,11 @@ 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; @@ -13,10 +18,5 @@ public abstract class CommandBase : IRequest 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/Tenants/CreateTenant/CreateTenantCommand.cs b/CleanArchitecture.Domain/Commands/Tenants/CreateTenant/CreateTenantCommand.cs index febc727..b9bc519 100644 --- a/CleanArchitecture.Domain/Commands/Tenants/CreateTenant/CreateTenantCommand.cs +++ b/CleanArchitecture.Domain/Commands/Tenants/CreateTenant/CreateTenantCommand.cs @@ -6,13 +6,13 @@ public sealed class CreateTenantCommand : CommandBase { private static readonly CreateTenantCommandValidation s_validation = new(); + public string Name { get; } + public CreateTenantCommand(Guid tenantId, string name) : base(tenantId) { Name = name; } - public string Name { get; } - public override bool IsValid() { ValidationResult = s_validation.Validate(this); diff --git a/CleanArchitecture.Domain/Commands/Tenants/UpdateTenant/UpdateTenantCommand.cs b/CleanArchitecture.Domain/Commands/Tenants/UpdateTenant/UpdateTenantCommand.cs index cafbfef..fd2356d 100644 --- a/CleanArchitecture.Domain/Commands/Tenants/UpdateTenant/UpdateTenantCommand.cs +++ b/CleanArchitecture.Domain/Commands/Tenants/UpdateTenant/UpdateTenantCommand.cs @@ -6,13 +6,13 @@ public sealed class UpdateTenantCommand : CommandBase { private static readonly UpdateTenantCommandValidation s_validation = new(); + public string Name { get; } + public UpdateTenantCommand(Guid tenantId, string name) : base(tenantId) { Name = name; } - public string Name { get; } - public override bool IsValid() { ValidationResult = s_validation.Validate(this); diff --git a/CleanArchitecture.Domain/Commands/Users/ChangePassword/ChangePasswordCommand.cs b/CleanArchitecture.Domain/Commands/Users/ChangePassword/ChangePasswordCommand.cs index 8d60484..18ba9a2 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 static readonly ChangePasswordCommandValidation s_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 = s_validation.Validate(this); diff --git a/CleanArchitecture.Domain/Commands/Users/CreateUser/CreateUserCommand.cs b/CleanArchitecture.Domain/Commands/Users/CreateUser/CreateUserCommand.cs index fb17c86..3929d0a 100644 --- a/CleanArchitecture.Domain/Commands/Users/CreateUser/CreateUserCommand.cs +++ b/CleanArchitecture.Domain/Commands/Users/CreateUser/CreateUserCommand.cs @@ -6,6 +6,13 @@ public sealed class CreateUserCommand : CommandBase { private static readonly CreateUserCommandValidation s_validation = new(); + public Guid UserId { get; } + public Guid TenantId { get; } + public string Email { get; } + public string FirstName { get; } + public string LastName { get; } + public string Password { get; } + public CreateUserCommand( Guid userId, Guid tenantId, @@ -22,13 +29,6 @@ public sealed class CreateUserCommand : CommandBase Password = password; } - public Guid UserId { get; } - public Guid TenantId { get; } - public string Email { get; } - public string FirstName { get; } - public string LastName { get; } - public string Password { get; } - public override bool IsValid() { ValidationResult = s_validation.Validate(this); diff --git a/CleanArchitecture.Domain/Commands/Users/DeleteUser/DeleteUserCommand.cs b/CleanArchitecture.Domain/Commands/Users/DeleteUser/DeleteUserCommand.cs index 2aa4910..eda35ae 100644 --- a/CleanArchitecture.Domain/Commands/Users/DeleteUser/DeleteUserCommand.cs +++ b/CleanArchitecture.Domain/Commands/Users/DeleteUser/DeleteUserCommand.cs @@ -6,13 +6,13 @@ public sealed class DeleteUserCommand : CommandBase { private static readonly DeleteUserCommandValidation s_validation = new(); + public Guid UserId { get; } + public DeleteUserCommand(Guid userId) : base(userId) { UserId = userId; } - public Guid UserId { get; } - public override bool IsValid() { ValidationResult = s_validation.Validate(this); diff --git a/CleanArchitecture.Domain/Commands/Users/LoginUser/LoginUserCommand.cs b/CleanArchitecture.Domain/Commands/Users/LoginUser/LoginUserCommand.cs index d5bc6e6..838d3a9 100644 --- a/CleanArchitecture.Domain/Commands/Users/LoginUser/LoginUserCommand.cs +++ b/CleanArchitecture.Domain/Commands/Users/LoginUser/LoginUserCommand.cs @@ -8,6 +8,9 @@ public sealed class LoginUserCommand : CommandBase, { private static readonly LoginUserCommandValidation s_validation = new(); + public string Email { get; set; } + public string Password { get; set; } + public LoginUserCommand( string email, @@ -17,9 +20,6 @@ public sealed class LoginUserCommand : CommandBase, Password = password; } - public string Email { get; set; } - public string Password { get; set; } - public override bool IsValid() { ValidationResult = s_validation.Validate(this); diff --git a/CleanArchitecture.Domain/Commands/Users/UpdateUser/UpdateUserCommand.cs b/CleanArchitecture.Domain/Commands/Users/UpdateUser/UpdateUserCommand.cs index 98d6641..9f7f58c 100644 --- a/CleanArchitecture.Domain/Commands/Users/UpdateUser/UpdateUserCommand.cs +++ b/CleanArchitecture.Domain/Commands/Users/UpdateUser/UpdateUserCommand.cs @@ -7,6 +7,13 @@ public sealed class UpdateUserCommand : CommandBase { private static readonly UpdateUserCommandValidation s_validation = new(); + public Guid UserId { get; } + public Guid TenantId { get; } + public string Email { get; } + public string FirstName { get; } + public string LastName { get; } + public UserRole Role { get; } + public UpdateUserCommand( Guid userId, string email, @@ -22,13 +29,6 @@ public sealed class UpdateUserCommand : CommandBase TenantId = tenantId; } - public Guid UserId { get; } - public Guid TenantId { get; } - public string Email { get; } - public string FirstName { get; } - public string LastName { get; } - public UserRole Role { get; } - public override bool IsValid() { ValidationResult = s_validation.Validate(this); diff --git a/CleanArchitecture.Domain/Commands/Users/UpdateUser/UpdateUserCommandHandler.cs b/CleanArchitecture.Domain/Commands/Users/UpdateUser/UpdateUserCommandHandler.cs index cbfec13..09ef27e 100644 --- a/CleanArchitecture.Domain/Commands/Users/UpdateUser/UpdateUserCommandHandler.cs +++ b/CleanArchitecture.Domain/Commands/Users/UpdateUser/UpdateUserCommandHandler.cs @@ -13,9 +13,9 @@ namespace CleanArchitecture.Domain.Commands.Users.UpdateUser; public sealed class UpdateUserCommandHandler : CommandHandlerBase, IRequestHandler { + private readonly ITenantRepository _tenantRepository; private readonly IUser _user; private readonly IUserRepository _userRepository; - private readonly ITenantRepository _tenantRepository; public UpdateUserCommandHandler( IMediatorHandler bus, @@ -88,6 +88,7 @@ public sealed class UpdateUserCommandHandler : CommandHandlerBase, ErrorCodes.ObjectNotFound)); return; } + user.SetTenant(request.TenantId); } diff --git a/CleanArchitecture.Domain/DomainEvents/DomainEvent.cs b/CleanArchitecture.Domain/DomainEvents/DomainEvent.cs index e5c25a9..37d8eaf 100644 --- a/CleanArchitecture.Domain/DomainEvents/DomainEvent.cs +++ b/CleanArchitecture.Domain/DomainEvents/DomainEvent.cs @@ -5,6 +5,8 @@ namespace CleanArchitecture.Domain.DomainEvents; public abstract class DomainEvent : Message, INotification { + public DateTime Timestamp { get; private set; } + protected DomainEvent(Guid aggregateId) : base(aggregateId) { Timestamp = DateTime.Now; @@ -14,6 +16,4 @@ public abstract class DomainEvent : Message, INotification { Timestamp = DateTime.Now; } - - public DateTime Timestamp { get; private set; } } \ No newline at end of file diff --git a/CleanArchitecture.Domain/DomainEvents/Message.cs b/CleanArchitecture.Domain/DomainEvents/Message.cs index 58622a3..a99929e 100644 --- a/CleanArchitecture.Domain/DomainEvents/Message.cs +++ b/CleanArchitecture.Domain/DomainEvents/Message.cs @@ -5,6 +5,9 @@ namespace CleanArchitecture.Domain.DomainEvents; public abstract class Message : IRequest { + public Guid AggregateId { get; private set; } + public string MessageType { get; protected set; } + protected Message(Guid aggregateId) { AggregateId = aggregateId; @@ -16,7 +19,4 @@ public abstract class Message : IRequest AggregateId = aggregateId; MessageType = messageType ?? string.Empty; } - - public Guid AggregateId { get; private set; } - public string MessageType { get; protected set; } } \ No newline at end of file diff --git a/CleanArchitecture.Domain/DomainEvents/StoredDomainEvent.cs b/CleanArchitecture.Domain/DomainEvents/StoredDomainEvent.cs index 895bc9c..5220441 100644 --- a/CleanArchitecture.Domain/DomainEvents/StoredDomainEvent.cs +++ b/CleanArchitecture.Domain/DomainEvents/StoredDomainEvent.cs @@ -4,6 +4,11 @@ namespace CleanArchitecture.Domain.DomainEvents; public class StoredDomainEvent : DomainEvent { + public Guid Id { get; private set; } + public string Data { get; private set; } = string.Empty; + public string User { get; private set; } = string.Empty; + public string CorrelationId { get; private set; } = string.Empty; + public StoredDomainEvent( DomainEvent domainEvent, string data, @@ -21,9 +26,4 @@ public class StoredDomainEvent : DomainEvent protected StoredDomainEvent() : base(Guid.NewGuid()) { } - - public Guid Id { get; private set; } - public string Data { get; private set; } = string.Empty; - public string User { get; private set; } = string.Empty; - public string CorrelationId { get; private set; } = string.Empty; } \ No newline at end of file diff --git a/CleanArchitecture.Domain/DomainNotifications/StoredDomainNotification.cs b/CleanArchitecture.Domain/DomainNotifications/StoredDomainNotification.cs index b3fc594..a4800a4 100644 --- a/CleanArchitecture.Domain/DomainNotifications/StoredDomainNotification.cs +++ b/CleanArchitecture.Domain/DomainNotifications/StoredDomainNotification.cs @@ -5,6 +5,11 @@ namespace CleanArchitecture.Domain.DomainNotifications; public class StoredDomainNotification : DomainNotification { + public Guid Id { get; private set; } + public string SerializedData { get; private set; } = string.Empty; + public string User { get; private set; } = string.Empty; + public string CorrelationId { get; private set; } = string.Empty; + public StoredDomainNotification( DomainNotification domainNotification, string data, @@ -28,9 +33,4 @@ public class StoredDomainNotification : DomainNotification protected StoredDomainNotification() : base(string.Empty, string.Empty, string.Empty) { } - - public Guid Id { get; private set; } - public string SerializedData { get; private set; } = string.Empty; - public string User { get; private set; } = string.Empty; - public string CorrelationId { get; private set; } = string.Empty; } \ No newline at end of file diff --git a/CleanArchitecture.Domain/Entities/Entity.cs b/CleanArchitecture.Domain/Entities/Entity.cs index 99a272a..41c7b54 100644 --- a/CleanArchitecture.Domain/Entities/Entity.cs +++ b/CleanArchitecture.Domain/Entities/Entity.cs @@ -4,14 +4,14 @@ namespace CleanArchitecture.Domain.Entities; public abstract class Entity { + public Guid Id { get; private set; } + public bool Deleted { get; private set; } + protected Entity(Guid id) { Id = id; } - public Guid Id { get; private set; } - public bool Deleted { get; private set; } - public void SetId(Guid id) { if (id == Guid.Empty) diff --git a/CleanArchitecture.Domain/Entities/Tenant.cs b/CleanArchitecture.Domain/Entities/Tenant.cs index 74b97fd..9b5f2c5 100644 --- a/CleanArchitecture.Domain/Entities/Tenant.cs +++ b/CleanArchitecture.Domain/Entities/Tenant.cs @@ -5,6 +5,10 @@ namespace CleanArchitecture.Domain.Entities; public class Tenant : Entity { + public string Name { get; private set; } + + public virtual ICollection Users { get; private set; } = new HashSet(); + public Tenant( Guid id, string name) : base(id) @@ -12,10 +16,6 @@ public class Tenant : Entity Name = name; } - public string Name { get; private set; } - - public virtual ICollection Users { get; private set; } = new HashSet(); - public void SetName(string name) { Name = name; diff --git a/CleanArchitecture.Domain/Entities/User.cs b/CleanArchitecture.Domain/Entities/User.cs index c897574..f33ea9d 100644 --- a/CleanArchitecture.Domain/Entities/User.cs +++ b/CleanArchitecture.Domain/Entities/User.cs @@ -5,6 +5,17 @@ namespace CleanArchitecture.Domain.Entities; public class User : Entity { + public string Email { get; private set; } + public string FirstName { get; private set; } + public string LastName { get; private set; } + public string Password { get; private set; } + public UserRole Role { get; private set; } + + public string FullName => $"{FirstName}, {LastName}"; + + public Guid TenantId { get; private set; } + public virtual Tenant Tenant { get; private set; } = null!; + public User( Guid id, Guid tenantId, @@ -22,17 +33,6 @@ public class User : Entity Role = role; } - public string Email { get; private set; } - public string FirstName { get; private set; } - public string LastName { get; private set; } - public string Password { get; private set; } - public UserRole Role { get; private set; } - - public string FullName => $"{FirstName}, {LastName}"; - - public Guid TenantId { get; private set; } - public virtual Tenant Tenant { get; private set; } = null!; - public void SetEmail(string email) { Email = email; diff --git a/CleanArchitecture.Domain/Events/Tenant/TenantCreatedEvent.cs b/CleanArchitecture.Domain/Events/Tenant/TenantCreatedEvent.cs index fcdca66..dc558b6 100644 --- a/CleanArchitecture.Domain/Events/Tenant/TenantCreatedEvent.cs +++ b/CleanArchitecture.Domain/Events/Tenant/TenantCreatedEvent.cs @@ -5,10 +5,10 @@ namespace CleanArchitecture.Domain.Events.Tenant; public sealed class TenantCreatedEvent : DomainEvent { + public string Name { get; set; } + public TenantCreatedEvent(Guid tenantId, string name) : base(tenantId) { Name = name; } - - public string Name { get; set; } } \ No newline at end of file diff --git a/CleanArchitecture.Domain/Events/Tenant/TenantUpdatedEvent.cs b/CleanArchitecture.Domain/Events/Tenant/TenantUpdatedEvent.cs index 0c099d2..20675d1 100644 --- a/CleanArchitecture.Domain/Events/Tenant/TenantUpdatedEvent.cs +++ b/CleanArchitecture.Domain/Events/Tenant/TenantUpdatedEvent.cs @@ -5,10 +5,10 @@ namespace CleanArchitecture.Domain.Events.Tenant; public sealed class TenantUpdatedEvent : DomainEvent { + public string Name { get; set; } + public TenantUpdatedEvent(Guid tenantId, string name) : base(tenantId) { Name = name; } - - public string Name { get; set; } } \ No newline at end of file diff --git a/CleanArchitecture.Domain/Notifications/DomainNotification.cs b/CleanArchitecture.Domain/Notifications/DomainNotification.cs index f3da784..c228ab4 100644 --- a/CleanArchitecture.Domain/Notifications/DomainNotification.cs +++ b/CleanArchitecture.Domain/Notifications/DomainNotification.cs @@ -5,6 +5,12 @@ namespace CleanArchitecture.Domain.Notifications; public class DomainNotification : DomainEvent { + public string Key { get; } + public string Value { get; } + public string Code { get; } + public object? Data { get; set; } + public int Version { get; private set; } = 1; + public DomainNotification( string key, string value, @@ -19,10 +25,4 @@ public class DomainNotification : DomainEvent Data = data; } - - public string Key { get; } - public string Value { get; } - public string Code { get; } - public object? Data { get; set; } - public int Version { get; private set; } = 1; } \ No newline at end of file diff --git a/CleanArchitecture.Infrastructure/Database/ApplicationDbContext.cs b/CleanArchitecture.Infrastructure/Database/ApplicationDbContext.cs index f2dea27..6a5517f 100644 --- a/CleanArchitecture.Infrastructure/Database/ApplicationDbContext.cs +++ b/CleanArchitecture.Infrastructure/Database/ApplicationDbContext.cs @@ -6,13 +6,13 @@ namespace CleanArchitecture.Infrastructure.Database; public class ApplicationDbContext : DbContext { + public DbSet Users { get; set; } = null!; + public DbSet Tenants { get; set; } = null!; + public ApplicationDbContext(DbContextOptions options) : base(options) { } - public DbSet Users { get; set; } = null!; - public DbSet Tenants { get; set; } = null!; - protected override void OnModelCreating(ModelBuilder builder) { builder.ApplyConfiguration(new UserConfiguration()); diff --git a/CleanArchitecture.Infrastructure/Database/DomainNotificationStoreDbContext.cs b/CleanArchitecture.Infrastructure/Database/DomainNotificationStoreDbContext.cs index 7002617..9b8c603 100644 --- a/CleanArchitecture.Infrastructure/Database/DomainNotificationStoreDbContext.cs +++ b/CleanArchitecture.Infrastructure/Database/DomainNotificationStoreDbContext.cs @@ -6,12 +6,12 @@ namespace CleanArchitecture.Infrastructure.Database; public class DomainNotificationStoreDbContext : DbContext { + public virtual DbSet StoredDomainNotifications { get; set; } = null!; + public DomainNotificationStoreDbContext(DbContextOptions options) : base(options) { } - public virtual DbSet StoredDomainNotifications { get; set; } = null!; - protected override void OnModelCreating(ModelBuilder modelBuilder) { base.OnModelCreating(modelBuilder); diff --git a/CleanArchitecture.Infrastructure/Database/EventStoreDbContext.cs b/CleanArchitecture.Infrastructure/Database/EventStoreDbContext.cs index e7e7c65..fa6e2d0 100644 --- a/CleanArchitecture.Infrastructure/Database/EventStoreDbContext.cs +++ b/CleanArchitecture.Infrastructure/Database/EventStoreDbContext.cs @@ -6,12 +6,12 @@ namespace CleanArchitecture.Infrastructure.Database; public class EventStoreDbContext : DbContext { + public virtual DbSet StoredDomainEvents { get; set; } = null!; + public EventStoreDbContext(DbContextOptions options) : base(options) { } - public virtual DbSet StoredDomainEvents { get; set; } = null!; - protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.ApplyConfiguration(new StoredDomainEventConfiguration()); diff --git a/CleanArchitecture.IntegrationTests/Controller/UserControllerTests.cs b/CleanArchitecture.IntegrationTests/Controller/UserControllerTests.cs index b78553f..45dd1f4 100644 --- a/CleanArchitecture.IntegrationTests/Controller/UserControllerTests.cs +++ b/CleanArchitecture.IntegrationTests/Controller/UserControllerTests.cs @@ -25,7 +25,7 @@ public sealed class UserControllerTests : IClassFixture { _fixture = fixture; } - + [Fact] [Priority(0)] public async Task Should_Get_All_User() @@ -49,21 +49,21 @@ public sealed class UserControllerTests : IClassFixture currentUser.FirstName.Should().Be(TestAuthenticationOptions.FirstName); currentUser.LastName.Should().Be(TestAuthenticationOptions.LastName); } - + [Fact] [Priority(5)] public async Task Should_Get_User_By_Id() { var response = await _fixture.ServerClient.GetAsync("/api/v1/user/" + TestAuthenticationOptions.TestUserId); - + response.StatusCode.Should().Be(HttpStatusCode.OK); - + var message = await response.Content.ReadAsJsonAsync(); - + message?.Data.Should().NotBeNull(); - + var content = message!.Data!; - + content.Id.Should().Be(TestAuthenticationOptions.TestUserId); content.Email.Should().Be(TestAuthenticationOptions.Email); content.FirstName.Should().Be(TestAuthenticationOptions.FirstName); @@ -148,18 +148,18 @@ public sealed class UserControllerTests : IClassFixture var content = message!.Data; content.Should().BeEquivalentTo(user); - + // Check if user is really updated var userResponse = await _fixture.ServerClient.GetAsync("/api/v1/user/" + user.Id); - + userResponse.StatusCode.Should().Be(HttpStatusCode.OK); - + var userMessage = await userResponse.Content.ReadAsJsonAsync(); - + userMessage?.Data.Should().NotBeNull(); - + var userContent = userMessage!.Data!; - + userContent.Id.Should().Be(user.Id); userContent.Email.Should().Be(user.Email); userContent.FirstName.Should().Be(user.FirstName); @@ -217,7 +217,7 @@ public sealed class UserControllerTests : IClassFixture content.Should().Be(TestAuthenticationOptions.TestUserId); var userResponse = await _fixture.ServerClient.GetAsync("/api/v1/user/" + TestAuthenticationOptions.TestUserId); - + userResponse.StatusCode.Should().Be(HttpStatusCode.NotFound); } } \ No newline at end of file diff --git a/CleanArchitecture.IntegrationTests/Fixtures/TenantTestFixture.cs b/CleanArchitecture.IntegrationTests/Fixtures/TenantTestFixture.cs index 437d39f..3d66346 100644 --- a/CleanArchitecture.IntegrationTests/Fixtures/TenantTestFixture.cs +++ b/CleanArchitecture.IntegrationTests/Fixtures/TenantTestFixture.cs @@ -11,7 +11,7 @@ public sealed class TenantTestFixture : TestFixtureBase protected override void SeedTestData(ApplicationDbContext context) { base.SeedTestData(context); - + context.Tenants.Add(new Tenant( CreatedTenantId, "Test Tenant")); diff --git a/CleanArchitecture.IntegrationTests/Fixtures/TestFixtureBase.cs b/CleanArchitecture.IntegrationTests/Fixtures/TestFixtureBase.cs index 29bcd0f..6a8d633 100644 --- a/CleanArchitecture.IntegrationTests/Fixtures/TestFixtureBase.cs +++ b/CleanArchitecture.IntegrationTests/Fixtures/TestFixtureBase.cs @@ -13,6 +13,9 @@ namespace CleanArchitecture.IntegrationTests.Fixtures; public class TestFixtureBase { + public HttpClient ServerClient { get; } + protected WebApplicationFactory Factory { get; } + public TestFixtureBase(bool useTestAuthentication = true) { Factory = new CleanArchitectureWebApplicationFactory( @@ -24,9 +27,6 @@ public class TestFixtureBase ServerClient.Timeout = TimeSpan.FromMinutes(5); } - public HttpClient ServerClient { get; } - protected WebApplicationFactory Factory { get; } - protected virtual void SeedTestData(ApplicationDbContext context) { context.Users.Add(new User( diff --git a/CleanArchitecture.IntegrationTests/Fixtures/gRPC/GetTenantsByIdsTestFixture.cs b/CleanArchitecture.IntegrationTests/Fixtures/gRPC/GetTenantsByIdsTestFixture.cs index b6f2672..fdfe5a6 100644 --- a/CleanArchitecture.IntegrationTests/Fixtures/gRPC/GetTenantsByIdsTestFixture.cs +++ b/CleanArchitecture.IntegrationTests/Fixtures/gRPC/GetTenantsByIdsTestFixture.cs @@ -7,6 +7,9 @@ namespace CleanArchitecture.IntegrationTests.Fixtures.gRPC; public sealed class GetTenantsByIdsTestFixture : TestFixtureBase { + public GrpcChannel GrpcChannel { get; } + public Guid CreatedTenantId { get; } = Guid.NewGuid(); + public GetTenantsByIdsTestFixture() { GrpcChannel = GrpcChannel.ForAddress("http://localhost", new GrpcChannelOptions @@ -15,9 +18,6 @@ public sealed class GetTenantsByIdsTestFixture : TestFixtureBase }); } - public GrpcChannel GrpcChannel { get; } - public Guid CreatedTenantId { get; } = Guid.NewGuid(); - protected override void SeedTestData(ApplicationDbContext context) { base.SeedTestData(context); diff --git a/CleanArchitecture.IntegrationTests/Fixtures/gRPC/GetUsersByIdsTestFixture.cs b/CleanArchitecture.IntegrationTests/Fixtures/gRPC/GetUsersByIdsTestFixture.cs index c39c29b..22ce59b 100644 --- a/CleanArchitecture.IntegrationTests/Fixtures/gRPC/GetUsersByIdsTestFixture.cs +++ b/CleanArchitecture.IntegrationTests/Fixtures/gRPC/GetUsersByIdsTestFixture.cs @@ -9,6 +9,9 @@ namespace CleanArchitecture.IntegrationTests.Fixtures.gRPC; public sealed class GetUsersByIdsTestFixture : TestFixtureBase { + public GrpcChannel GrpcChannel { get; } + public Guid CreatedUserId { get; } = Guid.NewGuid(); + public GetUsersByIdsTestFixture() { GrpcChannel = GrpcChannel.ForAddress("http://localhost", new GrpcChannelOptions @@ -17,9 +20,6 @@ public sealed class GetUsersByIdsTestFixture : TestFixtureBase }); } - public GrpcChannel GrpcChannel { get; } - public Guid CreatedUserId { get; } = Guid.NewGuid(); - protected override void SeedTestData(ApplicationDbContext context) { base.SeedTestData(context); diff --git a/CleanArchitecture.IntegrationTests/Infrastructure/Auth/TestAuthenticationOptions.cs b/CleanArchitecture.IntegrationTests/Infrastructure/Auth/TestAuthenticationOptions.cs index d301b34..1eb58b4 100644 --- a/CleanArchitecture.IntegrationTests/Infrastructure/Auth/TestAuthenticationOptions.cs +++ b/CleanArchitecture.IntegrationTests/Infrastructure/Auth/TestAuthenticationOptions.cs @@ -7,12 +7,12 @@ namespace CleanArchitecture.IntegrationTests.Infrastructure.Auth; public sealed class TestAuthenticationOptions : AuthenticationSchemeOptions { - public static Guid TestUserId = new("561e4300-94d6-4c3f-adf5-31c1bdbc64df"); public const string Email = "integration@tests.com"; public const string FirstName = "Integration"; public const string LastName = "Tests"; + public static Guid TestUserId = new("561e4300-94d6-4c3f-adf5-31c1bdbc64df"); - public ClaimsIdentity Identity { get; } = new ClaimsIdentity( + public ClaimsIdentity Identity { get; } = new( new[] { new Claim(ClaimTypes.Email, Email), diff --git a/CleanArchitecture.IntegrationTests/Infrastructure/CleanArchitectureWebApplicationFactory.cs b/CleanArchitecture.IntegrationTests/Infrastructure/CleanArchitectureWebApplicationFactory.cs index 1c771cc..6328d21 100644 --- a/CleanArchitecture.IntegrationTests/Infrastructure/CleanArchitectureWebApplicationFactory.cs +++ b/CleanArchitecture.IntegrationTests/Infrastructure/CleanArchitectureWebApplicationFactory.cs @@ -22,10 +22,10 @@ public sealed class CleanArchitectureWebApplicationFactory : WebApplicationFacto IServiceProvider scopedServices); private readonly AddCustomSeedDataHandler? _addCustomSeedDataHandler; + private readonly bool _addTestAuthentication; private readonly SqliteConnection _connection = new("DataSource=:memory:"); private readonly RegisterCustomServicesHandler? _registerCustomServicesHandler; - private readonly bool _addTestAuthentication; public CleanArchitectureWebApplicationFactory( AddCustomSeedDataHandler? addCustomSeedDataHandler, diff --git a/CleanArchitecture.gRPC.Tests/Fixtures/TenantTestFixture.cs b/CleanArchitecture.gRPC.Tests/Fixtures/TenantTestFixture.cs index 3910cc2..565b589 100644 --- a/CleanArchitecture.gRPC.Tests/Fixtures/TenantTestFixture.cs +++ b/CleanArchitecture.gRPC.Tests/Fixtures/TenantTestFixture.cs @@ -10,6 +10,11 @@ namespace CleanArchitecture.gRPC.Tests.Fixtures; public sealed class TenantTestFixture { + public TenantsApiImplementation TenantsApiImplementation { get; } + private ITenantRepository TenantRepository { get; } + + public IEnumerable ExistingTenants { get; } + public TenantTestFixture() { TenantRepository = Substitute.For(); @@ -25,9 +30,4 @@ public sealed class TenantTestFixture TenantsApiImplementation = new TenantsApiImplementation(TenantRepository); } - - public TenantsApiImplementation TenantsApiImplementation { get; } - private ITenantRepository TenantRepository { get; } - - public IEnumerable ExistingTenants { get; } } \ No newline at end of file diff --git a/CleanArchitecture.gRPC.Tests/Fixtures/UserTestFixture.cs b/CleanArchitecture.gRPC.Tests/Fixtures/UserTestFixture.cs index 3954783..f350d7c 100644 --- a/CleanArchitecture.gRPC.Tests/Fixtures/UserTestFixture.cs +++ b/CleanArchitecture.gRPC.Tests/Fixtures/UserTestFixture.cs @@ -11,6 +11,12 @@ namespace CleanArchitecture.gRPC.Tests.Fixtures; public sealed class UserTestFixture { + private IUserRepository UserRepository { get; } = Substitute.For(); + + public UsersApiImplementation UsersApiImplementation { get; } + + public IEnumerable ExistingUsers { get; } + public UserTestFixture() { ExistingUsers = new List @@ -47,10 +53,4 @@ public sealed class UserTestFixture UsersApiImplementation = new UsersApiImplementation(UserRepository); } - - private IUserRepository UserRepository { get; } = Substitute.For(); - - public UsersApiImplementation UsersApiImplementation { get; } - - public IEnumerable ExistingUsers { get; } } \ No newline at end of file From f6ffb4b62eba989da4945f28930b4bd1af586aa6 Mon Sep 17 00:00:00 2001 From: alex289 Date: Thu, 31 Aug 2023 18:50:23 +0200 Subject: [PATCH 10/10] chore: Cleanup --- .../Controllers/UserController.cs | 1 - .../CreateUserCommandHandlerTests.cs | 38 ++++++++++++++++++- .../CreateUserCommandTestFixture.cs | 5 +-- .../DeleteUserCommandHandlerTests.cs | 20 ++++++++++ .../DeleteUserCommandTestFixture.cs | 5 +++ .../CleanArchitectureWebApplicationFactory.cs | 2 +- 6 files changed, 64 insertions(+), 7 deletions(-) diff --git a/CleanArchitecture.Api/Controllers/UserController.cs b/CleanArchitecture.Api/Controllers/UserController.cs index aa66217..3aac047 100644 --- a/CleanArchitecture.Api/Controllers/UserController.cs +++ b/CleanArchitecture.Api/Controllers/UserController.cs @@ -56,7 +56,6 @@ public sealed class UserController : ApiController } [HttpPost] - [AllowAnonymous] [SwaggerOperation("Create a new user")] [SwaggerResponse(200, "Request successful", typeof(ResponseMessage))] public async Task CreateUserAsync([FromBody] CreateUserViewModel viewModel) diff --git a/CleanArchitecture.Domain.Tests/CommandHandler/User/CreateUser/CreateUserCommandHandlerTests.cs b/CleanArchitecture.Domain.Tests/CommandHandler/User/CreateUser/CreateUserCommandHandlerTests.cs index 0151fd4..0f0e7d6 100644 --- a/CleanArchitecture.Domain.Tests/CommandHandler/User/CreateUser/CreateUserCommandHandlerTests.cs +++ b/CleanArchitecture.Domain.Tests/CommandHandler/User/CreateUser/CreateUserCommandHandlerTests.cs @@ -1,7 +1,9 @@ using System; using CleanArchitecture.Domain.Commands.Users.CreateUser; +using CleanArchitecture.Domain.Enums; using CleanArchitecture.Domain.Errors; using CleanArchitecture.Domain.Events.User; +using NSubstitute; using Xunit; namespace CleanArchitecture.Domain.Tests.CommandHandler.User.CreateUser; @@ -13,7 +15,6 @@ public sealed class CreateUserCommandHandlerTests [Fact] public void Should_Create_User() { - // Todo: Fix tests _fixture.SetupCurrentUser(); var user = _fixture.SetupUser(); @@ -60,6 +61,41 @@ public sealed class CreateUserCommandHandlerTests DomainErrorCodes.User.UserAlreadyExists, $"There is already a user with Id {command.UserId}"); } + + [Fact] + public void Should_Not_Create_Already_Existing_Email() + { + _fixture.SetupCurrentUser(); + + _fixture.UserRepository + .GetByEmailAsync(Arg.Is(y => y == "test@email.com")) + .Returns(new Entities.User( + Guid.NewGuid(), + Guid.NewGuid(), + "max@mustermann.com", + "Max", + "Mustermann", + "Password", + UserRole.User)); + + var command = new CreateUserCommand( + Guid.NewGuid(), + Guid.NewGuid(), + "test@email.com", + "Test", + "Email", + "Po=PF]PC6t.?8?ks)A6W"); + + _fixture.CommandHandler.Handle(command, default).Wait(); + + _fixture + .VerifyNoCommit() + .VerifyNoRaisedEvent() + .VerifyAnyDomainNotification() + .VerifyExistingNotification( + DomainErrorCodes.User.UserAlreadyExists, + $"There is already a user with email {command.Email}"); + } [Fact] public void Should_Not_Create_User_Tenant_Does_Not_Exist() diff --git a/CleanArchitecture.Domain.Tests/CommandHandler/User/CreateUser/CreateUserCommandTestFixture.cs b/CleanArchitecture.Domain.Tests/CommandHandler/User/CreateUser/CreateUserCommandTestFixture.cs index 812257f..3cf3a09 100644 --- a/CleanArchitecture.Domain.Tests/CommandHandler/User/CreateUser/CreateUserCommandTestFixture.cs +++ b/CleanArchitecture.Domain.Tests/CommandHandler/User/CreateUser/CreateUserCommandTestFixture.cs @@ -1,7 +1,6 @@ using System; using CleanArchitecture.Domain.Commands.Users.CreateUser; using CleanArchitecture.Domain.Enums; -using CleanArchitecture.Domain.Interfaces; using CleanArchitecture.Domain.Interfaces.Repositories; using NSubstitute; @@ -10,15 +9,13 @@ namespace CleanArchitecture.Domain.Tests.CommandHandler.User.CreateUser; public sealed class CreateUserCommandTestFixture : CommandHandlerFixtureBase { public CreateUserCommandHandler CommandHandler { get; } - private IUserRepository UserRepository { get; } + public IUserRepository UserRepository { get; } private ITenantRepository TenantRepository { get; } - private IUser User { get; } public CreateUserCommandTestFixture() { UserRepository = Substitute.For(); TenantRepository = Substitute.For(); - User = Substitute.For(); CommandHandler = new CreateUserCommandHandler( Bus, diff --git a/CleanArchitecture.Domain.Tests/CommandHandler/User/DeleteUser/DeleteUserCommandHandlerTests.cs b/CleanArchitecture.Domain.Tests/CommandHandler/User/DeleteUser/DeleteUserCommandHandlerTests.cs index 1e65782..95d2569 100644 --- a/CleanArchitecture.Domain.Tests/CommandHandler/User/DeleteUser/DeleteUserCommandHandlerTests.cs +++ b/CleanArchitecture.Domain.Tests/CommandHandler/User/DeleteUser/DeleteUserCommandHandlerTests.cs @@ -42,4 +42,24 @@ public sealed class DeleteUserCommandHandlerTests ErrorCodes.ObjectNotFound, $"There is no user with Id {command.UserId}"); } + + [Fact] + public void Should_Not_Delete_User_Insufficient_Permissions() + { + var user = _fixture.SetupUser(); + + _fixture.SetupCurrentUser(); + + var command = new DeleteUserCommand(user.Id); + + _fixture.CommandHandler.Handle(command, default).Wait(); + + _fixture + .VerifyNoCommit() + .VerifyNoRaisedEvent() + .VerifyAnyDomainNotification() + .VerifyExistingNotification( + ErrorCodes.InsufficientPermissions, + $"No permission to delete user {command.UserId}"); + } } \ 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 96f7d5d..bdd73f9 100644 --- a/CleanArchitecture.Domain.Tests/CommandHandler/User/DeleteUser/DeleteUserCommandTestFixture.cs +++ b/CleanArchitecture.Domain.Tests/CommandHandler/User/DeleteUser/DeleteUserCommandTestFixture.cs @@ -40,4 +40,9 @@ public sealed class DeleteUserCommandTestFixture : CommandHandlerFixtureBase return user; } + + public void SetupCurrentUser() + { + User.GetUserRole().Returns(UserRole.User); + } } \ No newline at end of file diff --git a/CleanArchitecture.IntegrationTests/Infrastructure/CleanArchitectureWebApplicationFactory.cs b/CleanArchitecture.IntegrationTests/Infrastructure/CleanArchitectureWebApplicationFactory.cs index 6328d21..da21af7 100644 --- a/CleanArchitecture.IntegrationTests/Infrastructure/CleanArchitectureWebApplicationFactory.cs +++ b/CleanArchitecture.IntegrationTests/Infrastructure/CleanArchitectureWebApplicationFactory.cs @@ -57,7 +57,7 @@ public sealed class CleanArchitectureWebApplicationFactory : WebApplicationFacto { options.DefaultAuthenticateScheme = "Testing"; options.DefaultChallengeScheme = "Testing"; - }).AddTestAuthentication(options => { }); + }).AddTestAuthentication(_ => { }); } var sp = services.BuildServiceProvider();