From 8b9253c095e0750015e8dba2b30c2b31e66c0dd6 Mon Sep 17 00:00:00 2001 From: alex289 Date: Sun, 8 Dec 2024 22:51:08 +0100 Subject: [PATCH 1/4] feat: Convert deleted flag to timestamp --- .../GetAll/GetAllTenantsQueryHandler.cs | 4 +- .../Users/GetAll/GetAllUsersQueryHandler.cs | 2 +- .../gRPC/TenantsApiImplementation.cs | 2 +- .../gRPC/UsersApiImplementation.cs | 2 +- CleanArchitecture.Domain/Entities/Entity.cs | 6 +- .../Database/DbContextUtility.cs | 6 +- ...1208214605_AddDeletedTimestamp.Designer.cs | 136 ++++++++++++++++++ .../20241208214605_AddDeletedTimestamp.cs | 95 ++++++++++++ .../ApplicationDbContextModelSnapshot.cs | 12 +- CleanArchitecture.Proto/Tenants/Models.proto | 2 +- CleanArchitecture.Proto/Users/Models.proto | 2 +- .../Tenants/TenantViewModel.cs | 3 +- .../Users/UserViewModel.cs | 2 +- .../Contexts/TenantsContext.cs | 3 +- .../Contexts/UsersContext.cs | 2 +- 15 files changed, 255 insertions(+), 24 deletions(-) create mode 100644 CleanArchitecture.Infrastructure/Migrations/20241208214605_AddDeletedTimestamp.Designer.cs create mode 100644 CleanArchitecture.Infrastructure/Migrations/20241208214605_AddDeletedTimestamp.cs diff --git a/CleanArchitecture.Application/Queries/Tenants/GetAll/GetAllTenantsQueryHandler.cs b/CleanArchitecture.Application/Queries/Tenants/GetAll/GetAllTenantsQueryHandler.cs index 9f46f18..940cf53 100644 --- a/CleanArchitecture.Application/Queries/Tenants/GetAll/GetAllTenantsQueryHandler.cs +++ b/CleanArchitecture.Application/Queries/Tenants/GetAll/GetAllTenantsQueryHandler.cs @@ -33,8 +33,8 @@ public async Task> Handle( var tenantsQuery = _tenantRepository .GetAllNoTracking() .IgnoreQueryFilters() - .Include(x => x.Users.Where(y => request.IncludeDeleted || !y.Deleted)) - .Where(x => request.IncludeDeleted || !x.Deleted); + .Include(x => x.Users.Where(y => request.IncludeDeleted || y.DeletedAt == null)) + .Where(x => request.IncludeDeleted || x.DeletedAt == null ); if (!string.IsNullOrWhiteSpace(request.SearchTerm)) { diff --git a/CleanArchitecture.Application/Queries/Users/GetAll/GetAllUsersQueryHandler.cs b/CleanArchitecture.Application/Queries/Users/GetAll/GetAllUsersQueryHandler.cs index 7de025a..71c85de 100644 --- a/CleanArchitecture.Application/Queries/Users/GetAll/GetAllUsersQueryHandler.cs +++ b/CleanArchitecture.Application/Queries/Users/GetAll/GetAllUsersQueryHandler.cs @@ -33,7 +33,7 @@ public async Task> Handle( var usersQuery = _userRepository .GetAllNoTracking() .IgnoreQueryFilters() - .Where(x => request.IncludeDeleted || !x.Deleted); + .Where(x => request.IncludeDeleted || x.DeletedAt == null); if (!string.IsNullOrWhiteSpace(request.SearchTerm)) { diff --git a/CleanArchitecture.Application/gRPC/TenantsApiImplementation.cs b/CleanArchitecture.Application/gRPC/TenantsApiImplementation.cs index 97679ac..c91a6b4 100644 --- a/CleanArchitecture.Application/gRPC/TenantsApiImplementation.cs +++ b/CleanArchitecture.Application/gRPC/TenantsApiImplementation.cs @@ -40,7 +40,7 @@ public override async Task GetByIds( { Id = tenant.Id.ToString(), Name = tenant.Name, - IsDeleted = tenant.Deleted + DeletedAt = tenant.DeletedAt == null ? "": tenant.DeletedAt.ToString() }) .ToListAsync(); diff --git a/CleanArchitecture.Application/gRPC/UsersApiImplementation.cs b/CleanArchitecture.Application/gRPC/UsersApiImplementation.cs index 6255920..6c24cf2 100644 --- a/CleanArchitecture.Application/gRPC/UsersApiImplementation.cs +++ b/CleanArchitecture.Application/gRPC/UsersApiImplementation.cs @@ -42,7 +42,7 @@ public override async Task GetByIds( Email = user.Email, FirstName = user.FirstName, LastName = user.LastName, - IsDeleted = user.Deleted + DeletedAt = user.DeletedAt == null ? "": user.DeletedAt.ToString() }) .ToListAsync(); diff --git a/CleanArchitecture.Domain/Entities/Entity.cs b/CleanArchitecture.Domain/Entities/Entity.cs index 41c7b54..df7d938 100644 --- a/CleanArchitecture.Domain/Entities/Entity.cs +++ b/CleanArchitecture.Domain/Entities/Entity.cs @@ -5,7 +5,7 @@ namespace CleanArchitecture.Domain.Entities; public abstract class Entity { public Guid Id { get; private set; } - public bool Deleted { get; private set; } + public DateTimeOffset? DeletedAt { get; private set; } protected Entity(Guid id) { @@ -24,11 +24,11 @@ public void SetId(Guid id) public void Delete() { - Deleted = true; + DeletedAt = DateTimeOffset.UtcNow; } public void Undelete() { - Deleted = false; + DeletedAt = null; } } \ No newline at end of file diff --git a/CleanArchitecture.Infrastructure/Database/DbContextUtility.cs b/CleanArchitecture.Infrastructure/Database/DbContextUtility.cs index 29900c4..56f8492 100644 --- a/CleanArchitecture.Infrastructure/Database/DbContextUtility.cs +++ b/CleanArchitecture.Infrastructure/Database/DbContextUtility.cs @@ -9,17 +9,17 @@ public partial class ApplicationDbContext { public static class DbContextUtility { - public const string IsDeletedProperty = "Deleted"; + public const string IsDeletedProperty = "DeletedAt"; public static readonly MethodInfo PropertyMethod = typeof(EF) .GetMethod(nameof(EF.Property), BindingFlags.Static | BindingFlags.Public) - !.MakeGenericMethod(typeof(bool)); + !.MakeGenericMethod(typeof(DateTimeOffset?)); public static LambdaExpression GetIsDeletedRestriction(Type type) { var parm = Expression.Parameter(type, "it"); var prop = Expression.Call(PropertyMethod, parm, Expression.Constant(IsDeletedProperty)); - var condition = Expression.MakeBinary(ExpressionType.Equal, prop, Expression.Constant(false)); + var condition = Expression.MakeBinary(ExpressionType.Equal, prop, Expression.Constant(null)); var lambda = Expression.Lambda(condition, parm); return lambda; } diff --git a/CleanArchitecture.Infrastructure/Migrations/20241208214605_AddDeletedTimestamp.Designer.cs b/CleanArchitecture.Infrastructure/Migrations/20241208214605_AddDeletedTimestamp.Designer.cs new file mode 100644 index 0000000..0e544c0 --- /dev/null +++ b/CleanArchitecture.Infrastructure/Migrations/20241208214605_AddDeletedTimestamp.Designer.cs @@ -0,0 +1,136 @@ +// +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("20241208214605_AddDeletedTimestamp")] + partial class AddDeletedTimestamp + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.0") + .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("DeletedAt") + .HasColumnType("datetimeoffset"); + + 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"), + Name = "Admin Tenant" + }); + }); + + modelBuilder.Entity("CleanArchitecture.Domain.Entities.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("DeletedAt") + .HasColumnType("datetimeoffset"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(320) + .HasColumnType("nvarchar(320)"); + + b.Property("FirstName") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("LastLoggedinDate") + .HasColumnType("datetimeoffset"); + + 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("Status") + .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"), + Email = "admin@email.com", + FirstName = "Admin", + LastName = "User", + Password = "$2a$12$Blal/uiFIJdYsCLTMUik/egLbfg3XhbnxBC6Sb5IKz2ZYhiU/MzL2", + Role = 0, + Status = 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.Restrict) + .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/20241208214605_AddDeletedTimestamp.cs b/CleanArchitecture.Infrastructure/Migrations/20241208214605_AddDeletedTimestamp.cs new file mode 100644 index 0000000..71ef3e6 --- /dev/null +++ b/CleanArchitecture.Infrastructure/Migrations/20241208214605_AddDeletedTimestamp.cs @@ -0,0 +1,95 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace CleanArchitecture.Infrastructure.Migrations +{ + /// + public partial class AddDeletedTimestamp : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "DeletedAt", + table: "Users", + type: "datetimeoffset", + nullable: true); + + migrationBuilder.AddColumn( + name: "DeletedAt", + table: "Tenants", + type: "datetimeoffset", + nullable: true); + + migrationBuilder.Sql("UPDATE Users SET DeletedAt = SYSDATETIMEOFFSET() WHERE Deleted = 1"); + migrationBuilder.Sql("UPDATE Tenants SET DeletedAt = SYSDATETIMEOFFSET() WHERE Deleted = 1"); + + migrationBuilder.DropColumn( + name: "Deleted", + table: "Users"); + + migrationBuilder.DropColumn( + name: "Deleted", + table: "Tenants"); + + migrationBuilder.UpdateData( + table: "Tenants", + keyColumn: "Id", + keyValue: new Guid("b542bf25-134c-47a2-a0df-84ed14d03c4a"), + column: "DeletedAt", + value: null); + + migrationBuilder.UpdateData( + table: "Users", + keyColumn: "Id", + keyValue: new Guid("7e3892c0-9374-49fa-a3fd-53db637a40ae"), + column: "DeletedAt", + value: null); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "Deleted", + table: "Users", + type: "bit", + nullable: false, + defaultValue: false); + + migrationBuilder.AddColumn( + name: "Deleted", + table: "Tenants", + type: "bit", + nullable: false, + defaultValue: false); + + migrationBuilder.Sql("UPDATE Users SET Deleted = true WHERE DeletedAt IS NOT NULL"); + migrationBuilder.Sql("UPDATE Tenants SET Deleted = true WHERE DeletedAt IS NOT NULL"); + + migrationBuilder.DropColumn( + name: "DeletedAt", + table: "Users"); + + migrationBuilder.DropColumn( + name: "DeletedAt", + table: "Tenants"); + + migrationBuilder.UpdateData( + table: "Tenants", + keyColumn: "Id", + keyValue: new Guid("b542bf25-134c-47a2-a0df-84ed14d03c4a"), + column: "Deleted", + value: false); + + migrationBuilder.UpdateData( + table: "Users", + keyColumn: "Id", + keyValue: new Guid("7e3892c0-9374-49fa-a3fd-53db637a40ae"), + column: "Deleted", + value: false); + } + } +} diff --git a/CleanArchitecture.Infrastructure/Migrations/ApplicationDbContextModelSnapshot.cs b/CleanArchitecture.Infrastructure/Migrations/ApplicationDbContextModelSnapshot.cs index cdbe611..1d17fb5 100644 --- a/CleanArchitecture.Infrastructure/Migrations/ApplicationDbContextModelSnapshot.cs +++ b/CleanArchitecture.Infrastructure/Migrations/ApplicationDbContextModelSnapshot.cs @@ -17,7 +17,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) { #pragma warning disable 612, 618 modelBuilder - .HasAnnotation("ProductVersion", "7.0.11") + .HasAnnotation("ProductVersion", "9.0.0") .HasAnnotation("Proxies:ChangeTracking", false) .HasAnnotation("Proxies:CheckEquality", false) .HasAnnotation("Proxies:LazyLoading", true) @@ -31,8 +31,8 @@ protected override void BuildModel(ModelBuilder modelBuilder) .ValueGeneratedOnAdd() .HasColumnType("uniqueidentifier"); - b.Property("Deleted") - .HasColumnType("bit"); + b.Property("DeletedAt") + .HasColumnType("datetimeoffset"); b.Property("Name") .IsRequired() @@ -47,7 +47,6 @@ protected override void BuildModel(ModelBuilder modelBuilder) new { Id = new Guid("b542bf25-134c-47a2-a0df-84ed14d03c4a"), - Deleted = false, Name = "Admin Tenant" }); }); @@ -58,8 +57,8 @@ protected override void BuildModel(ModelBuilder modelBuilder) .ValueGeneratedOnAdd() .HasColumnType("uniqueidentifier"); - b.Property("Deleted") - .HasColumnType("bit"); + b.Property("DeletedAt") + .HasColumnType("datetimeoffset"); b.Property("Email") .IsRequired() @@ -103,7 +102,6 @@ protected override void BuildModel(ModelBuilder modelBuilder) new { Id = new Guid("7e3892c0-9374-49fa-a3fd-53db637a40ae"), - Deleted = false, Email = "admin@email.com", FirstName = "Admin", LastName = "User", diff --git a/CleanArchitecture.Proto/Tenants/Models.proto b/CleanArchitecture.Proto/Tenants/Models.proto index c98c121..db17dba 100644 --- a/CleanArchitecture.Proto/Tenants/Models.proto +++ b/CleanArchitecture.Proto/Tenants/Models.proto @@ -5,7 +5,7 @@ option csharp_namespace = "CleanArchitecture.Proto.Tenants"; message Tenant { string id = 1; string name = 2; - bool isDeleted = 3; + optional string deletedAt = 3; } message GetTenantsByIdsResult { diff --git a/CleanArchitecture.Proto/Users/Models.proto b/CleanArchitecture.Proto/Users/Models.proto index c372420..f8e2e98 100644 --- a/CleanArchitecture.Proto/Users/Models.proto +++ b/CleanArchitecture.Proto/Users/Models.proto @@ -7,7 +7,7 @@ message GrpcUser { string firstName = 3; string lastName = 4; string email = 5; - bool isDeleted = 6; + optional string deletedAt = 6; } message GetUsersByIdsResult { diff --git a/CleanArchitecture.Shared/Tenants/TenantViewModel.cs b/CleanArchitecture.Shared/Tenants/TenantViewModel.cs index 97cd79f..bc015d9 100644 --- a/CleanArchitecture.Shared/Tenants/TenantViewModel.cs +++ b/CleanArchitecture.Shared/Tenants/TenantViewModel.cs @@ -4,4 +4,5 @@ namespace CleanArchitecture.Shared.Tenants; public sealed record TenantViewModel( Guid Id, - string Name); \ No newline at end of file + string Name, + DateTimeOffset? DeletedAt); \ No newline at end of file diff --git a/CleanArchitecture.Shared/Users/UserViewModel.cs b/CleanArchitecture.Shared/Users/UserViewModel.cs index d08d316..b94c55b 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); \ No newline at end of file + DateTimeOffset? DeletedAt); \ No newline at end of file diff --git a/CleanArchitecture.gRPC/Contexts/TenantsContext.cs b/CleanArchitecture.gRPC/Contexts/TenantsContext.cs index f6038e4..6b3e7d0 100644 --- a/CleanArchitecture.gRPC/Contexts/TenantsContext.cs +++ b/CleanArchitecture.gRPC/Contexts/TenantsContext.cs @@ -27,6 +27,7 @@ public async Task> GetTenantsByIds(IEnumerable new TenantViewModel( Guid.Parse(tenant.Id), - tenant.Name)); + tenant.Name, + string.IsNullOrWhiteSpace(tenant.DeletedAt) ? null : DateTimeOffset.Parse(tenant.DeletedAt))); } } \ No newline at end of file diff --git a/CleanArchitecture.gRPC/Contexts/UsersContext.cs b/CleanArchitecture.gRPC/Contexts/UsersContext.cs index a6b02d2..03b27c9 100644 --- a/CleanArchitecture.gRPC/Contexts/UsersContext.cs +++ b/CleanArchitecture.gRPC/Contexts/UsersContext.cs @@ -30,6 +30,6 @@ public async Task> GetUsersByIds(IEnumerable id user.Email, user.FirstName, user.LastName, - user.IsDeleted)); + string.IsNullOrWhiteSpace(user.DeletedAt) ? null : DateTimeOffset.Parse(user.DeletedAt))); } } \ No newline at end of file From a03f1ce68543cc1aec4b08805429f79a1714cdf8 Mon Sep 17 00:00:00 2001 From: alex289 Date: Sun, 8 Dec 2024 22:56:06 +0100 Subject: [PATCH 2/4] feat: Add deleted entities to integration tests --- .../Fixtures/TenantTestFixture.cs | 7 +++++++ .../Fixtures/UserTestFixture.cs | 15 ++++++++++++++- 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/CleanArchitecture.IntegrationTests/Fixtures/TenantTestFixture.cs b/CleanArchitecture.IntegrationTests/Fixtures/TenantTestFixture.cs index 7416837..f56bf75 100644 --- a/CleanArchitecture.IntegrationTests/Fixtures/TenantTestFixture.cs +++ b/CleanArchitecture.IntegrationTests/Fixtures/TenantTestFixture.cs @@ -21,6 +21,13 @@ public async Task SeedTestData() CreatedTenantId, "Test Tenant")); + // This tenant should not be included in any queries + var deletedTenant = new Tenant( + CreatedTenantId, + "Test Tenant2"); + deletedTenant.Delete(); + context.Tenants.Add(deletedTenant); + context.Users.Add(new User( Guid.NewGuid(), CreatedTenantId, diff --git a/CleanArchitecture.IntegrationTests/Fixtures/UserTestFixture.cs b/CleanArchitecture.IntegrationTests/Fixtures/UserTestFixture.cs index bb07bec..05fd349 100644 --- a/CleanArchitecture.IntegrationTests/Fixtures/UserTestFixture.cs +++ b/CleanArchitecture.IntegrationTests/Fixtures/UserTestFixture.cs @@ -1,4 +1,5 @@ -using System.Threading.Tasks; +using System; +using System.Threading.Tasks; using CleanArchitecture.Domain.Constants; using CleanArchitecture.Domain.Entities; using CleanArchitecture.Domain.Enums; @@ -29,6 +30,18 @@ public async Task SeedTestData() "$2a$12$Blal/uiFIJdYsCLTMUik/egLbfg3XhbnxBC6Sb5IKz2ZYhiU/MzL2", UserRole.Admin)); + // This user should not be included in any queries + var deletedUsed = new User( + Guid.NewGuid(), + Ids.Seed.TenantId, + "admin2@email.com", + "Admin2", + "User2", + "$2a$12$Blal/uiFIJdYsCLTMUik/egLbfg3XhbnxBC6Sb5IKz2ZYhiU/MzL2", + UserRole.User); + deletedUsed.Delete(); + context.Users.Add(deletedUsed); + context.Users.Add(new User( TestAuthenticationOptions.TestUserId, Ids.Seed.TenantId, From 9e74b9f66aaeb3b07ed3274cb50f477d23c7b7ab Mon Sep 17 00:00:00 2001 From: alex289 Date: Sun, 8 Dec 2024 23:10:28 +0100 Subject: [PATCH 3/4] fix: Use new Id for deleted tenant --- .../Fixtures/TenantTestFixture.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CleanArchitecture.IntegrationTests/Fixtures/TenantTestFixture.cs b/CleanArchitecture.IntegrationTests/Fixtures/TenantTestFixture.cs index f56bf75..8418bf9 100644 --- a/CleanArchitecture.IntegrationTests/Fixtures/TenantTestFixture.cs +++ b/CleanArchitecture.IntegrationTests/Fixtures/TenantTestFixture.cs @@ -23,7 +23,7 @@ public async Task SeedTestData() // This tenant should not be included in any queries var deletedTenant = new Tenant( - CreatedTenantId, + Guid.NewGuid(), "Test Tenant2"); deletedTenant.Delete(); context.Tenants.Add(deletedTenant); From 76da8251d59cd2e1b35a38d18d200c93e3b52942 Mon Sep 17 00:00:00 2001 From: alex289 Date: Mon, 9 Dec 2024 11:06:03 +0100 Subject: [PATCH 4/4] feat: Extend tests to verify deleted entity query behavior --- .../Controller/TenantControllerTests.cs | 35 ++++++++++++++-- .../Controller/UserControllerTests.cs | 41 ++++++++++++++++--- .../Fixtures/TenantTestFixture.cs | 4 +- .../Fixtures/UserTestFixture.cs | 5 ++- .../gRPC/GetTenantsByIdsTestFixture.cs | 1 + .../Fixtures/gRPC/GetUsersByIdsTestFixture.cs | 1 + .../gRPC/GetTenantsByIdsTests.cs | 1 + .../gRPC/GetUsersByIdsTests.cs | 1 + 8 files changed, 76 insertions(+), 13 deletions(-) diff --git a/CleanArchitecture.IntegrationTests/Controller/TenantControllerTests.cs b/CleanArchitecture.IntegrationTests/Controller/TenantControllerTests.cs index d406a4d..79a96eb 100644 --- a/CleanArchitecture.IntegrationTests/Controller/TenantControllerTests.cs +++ b/CleanArchitecture.IntegrationTests/Controller/TenantControllerTests.cs @@ -54,8 +54,37 @@ public async Task Should_Get_All_Tenants() .FirstOrDefault(x => x.Id == _fixture.CreatedTenantId)! .Users.Count().Should().Be(1); } - + [Test, Order(2)] + public async Task Should_Not_Get_Deleted_Tenant_By_Id() + { + var response = await _fixture.ServerClient.GetAsync($"/api/v1/Tenant/{_fixture.DeletedTenantId}"); + + response.StatusCode.Should().Be(HttpStatusCode.NotFound); + + var message = await response.Content.ReadAsJsonAsync(); + + message?.Data.Should().BeNull(); + } + + [Test, Order(3)] + public async Task Should_Get_All_Tenants_Including_Deleted() + { + var response = await _fixture.ServerClient.GetAsync( + "api/v1/Tenant?searchTerm=Test&pageSize=5&page=1&includeDeleted=true"); + + response.StatusCode.Should().Be(HttpStatusCode.OK); + + var message = await response.Content.ReadAsJsonAsync>(); + + message?.Data!.Items.Should().NotBeEmpty(); + message!.Data!.Items.Should().HaveCount(2); + message.Data!.Items + .FirstOrDefault(x => x.Id == _fixture.DeletedTenantId) + .Should().NotBeNull(); + } + + [Test, Order(4)] public async Task Should_Create_Tenant() { var request = new CreateTenantViewModel("Test Tenant 2"); @@ -80,7 +109,7 @@ public async Task Should_Create_Tenant() tenantMessage.Data.Name.Should().Be(request.Name); } - [Test, Order(3)] + [Test, Order(5)] public async Task Should_Update_Tenant() { var request = new UpdateTenantViewModel(_fixture.CreatedTenantId, "Test Tenant 3"); @@ -107,7 +136,7 @@ public async Task Should_Update_Tenant() tenantMessage.Data.Name.Should().Be(request.Name); } - [Test, Order(4)] + [Test, Order(6)] public async Task Should_Delete_Tenant() { var response = await _fixture.ServerClient.DeleteAsync($"/api/v1/Tenant/{_fixture.CreatedTenantId}"); diff --git a/CleanArchitecture.IntegrationTests/Controller/UserControllerTests.cs b/CleanArchitecture.IntegrationTests/Controller/UserControllerTests.cs index 422cae7..5daa08f 100644 --- a/CleanArchitecture.IntegrationTests/Controller/UserControllerTests.cs +++ b/CleanArchitecture.IntegrationTests/Controller/UserControllerTests.cs @@ -61,8 +61,37 @@ public async Task Should_Get_User_By_Id() content.FirstName.Should().Be(TestAuthenticationOptions.FirstName); content.LastName.Should().Be(TestAuthenticationOptions.LastName); } - + [Test, Order(2)] + public async Task Should_Get_All_User_Including_Deleted() + { + var response = await _fixture.ServerClient.GetAsync("/api/v1/user?includeDeleted=true"); + + response.StatusCode.Should().Be(HttpStatusCode.OK); + + var message = await response.Content.ReadAsJsonAsync>(); + + message?.Data.Should().NotBeNull(); + + var content = message!.Data!.Items.ToList(); + + content.Count.Should().Be(3); + + content.FirstOrDefault(x => x.Id == _fixture.DeletedUserId).Should().NotBeNull(); + } + + [Test, Order(3)] + public async Task Should_Not_Get_Deleted_User_By_Id() + { + var response = await _fixture.ServerClient.GetAsync("/api/v1/user/" + _fixture.DeletedUserId); + + response.StatusCode.Should().Be(HttpStatusCode.NotFound); + + var message = await response.Content.ReadAsJsonAsync(); + message?.Data.Should().BeNull(); + } + + [Test, Order(4)] public async Task Should_Create_User() { var user = new CreateUserViewModel( @@ -80,7 +109,7 @@ public async Task Should_Create_User() message?.Data.Should().NotBeEmpty(); } - [Test, Order(3)] + [Test, Order(5)] public async Task Should_Login_User() { var user = new LoginUserViewModel( @@ -95,7 +124,7 @@ public async Task Should_Login_User() message?.Data.Should().NotBeEmpty(); } - [Test, Order(4)] + [Test, Order(6)] public async Task Should_Get_The_Current_Active_Users() { var response = await _fixture.ServerClient.GetAsync("/api/v1/user/me"); @@ -114,7 +143,7 @@ public async Task Should_Get_The_Current_Active_Users() content.LastName.Should().Be(TestAuthenticationOptions.LastName); } - [Test, Order(5)] + [Test, Order(7)] public async Task Should_Update_User() { var user = new UpdateUserViewModel( @@ -155,7 +184,7 @@ public async Task Should_Update_User() userContent.Role.Should().Be(user.Role); } - [Test, Order(6)] + [Test, Order(8)] public async Task Should_Change_User_Password() { var user = new ChangePasswordViewModel( @@ -188,7 +217,7 @@ public async Task Should_Change_User_Password() loginMessage?.Data.Should().NotBeEmpty(); } - [Test, Order(7)] + [Test, Order(9)] public async Task Should_Delete_User() { var response = await _fixture.ServerClient.DeleteAsync("/api/v1/user/" + TestAuthenticationOptions.TestUserId); diff --git a/CleanArchitecture.IntegrationTests/Fixtures/TenantTestFixture.cs b/CleanArchitecture.IntegrationTests/Fixtures/TenantTestFixture.cs index 8418bf9..50f99ae 100644 --- a/CleanArchitecture.IntegrationTests/Fixtures/TenantTestFixture.cs +++ b/CleanArchitecture.IntegrationTests/Fixtures/TenantTestFixture.cs @@ -10,6 +10,7 @@ namespace CleanArchitecture.IntegrationTests.Fixtures; public sealed class TenantTestFixture : TestFixtureBase { public Guid CreatedTenantId { get; } = Guid.NewGuid(); + public Guid DeletedTenantId { get; } = Guid.NewGuid(); public async Task SeedTestData() { @@ -21,9 +22,8 @@ public async Task SeedTestData() CreatedTenantId, "Test Tenant")); - // This tenant should not be included in any queries var deletedTenant = new Tenant( - Guid.NewGuid(), + DeletedTenantId, "Test Tenant2"); deletedTenant.Delete(); context.Tenants.Add(deletedTenant); diff --git a/CleanArchitecture.IntegrationTests/Fixtures/UserTestFixture.cs b/CleanArchitecture.IntegrationTests/Fixtures/UserTestFixture.cs index 05fd349..1131ddb 100644 --- a/CleanArchitecture.IntegrationTests/Fixtures/UserTestFixture.cs +++ b/CleanArchitecture.IntegrationTests/Fixtures/UserTestFixture.cs @@ -11,6 +11,8 @@ namespace CleanArchitecture.IntegrationTests.Fixtures; public sealed class UserTestFixture : TestFixtureBase { + public Guid DeletedUserId { get; } = Guid.NewGuid(); + public async Task SeedTestData() { await GlobalSetupFixture.RespawnDatabaseAsync(); @@ -30,9 +32,8 @@ public async Task SeedTestData() "$2a$12$Blal/uiFIJdYsCLTMUik/egLbfg3XhbnxBC6Sb5IKz2ZYhiU/MzL2", UserRole.Admin)); - // This user should not be included in any queries var deletedUsed = new User( - Guid.NewGuid(), + DeletedUserId, Ids.Seed.TenantId, "admin2@email.com", "Admin2", diff --git a/CleanArchitecture.IntegrationTests/Fixtures/gRPC/GetTenantsByIdsTestFixture.cs b/CleanArchitecture.IntegrationTests/Fixtures/gRPC/GetTenantsByIdsTestFixture.cs index 05c381a..c3fb162 100644 --- a/CleanArchitecture.IntegrationTests/Fixtures/gRPC/GetTenantsByIdsTestFixture.cs +++ b/CleanArchitecture.IntegrationTests/Fixtures/gRPC/GetTenantsByIdsTestFixture.cs @@ -27,6 +27,7 @@ public async Task SeedTestData() using var context = Factory.Services.GetRequiredService(); var tenant = CreateTenant(); + tenant.Delete(); context.Tenants.Add(tenant); await context.SaveChangesAsync(); diff --git a/CleanArchitecture.IntegrationTests/Fixtures/gRPC/GetUsersByIdsTestFixture.cs b/CleanArchitecture.IntegrationTests/Fixtures/gRPC/GetUsersByIdsTestFixture.cs index 0e35906..c20c020 100644 --- a/CleanArchitecture.IntegrationTests/Fixtures/gRPC/GetUsersByIdsTestFixture.cs +++ b/CleanArchitecture.IntegrationTests/Fixtures/gRPC/GetUsersByIdsTestFixture.cs @@ -33,6 +33,7 @@ public async Task SeedTestData() "Admin Tenant")); var user = CreateUser(); + user.Delete(); context.Users.Add(user); await context.SaveChangesAsync(); diff --git a/CleanArchitecture.IntegrationTests/gRPC/GetTenantsByIdsTests.cs b/CleanArchitecture.IntegrationTests/gRPC/GetTenantsByIdsTests.cs index a101f4e..8e87d79 100644 --- a/CleanArchitecture.IntegrationTests/gRPC/GetTenantsByIdsTests.cs +++ b/CleanArchitecture.IntegrationTests/gRPC/GetTenantsByIdsTests.cs @@ -31,5 +31,6 @@ public async Task Should_Get_Tenants_By_Ids() new Guid(tenant.Id).Should().Be(createdTenant.Id); tenant.Name.Should().Be(createdTenant.Name); + tenant.DeletedAt.Should().NotBeNullOrWhiteSpace(); } } \ No newline at end of file diff --git a/CleanArchitecture.IntegrationTests/gRPC/GetUsersByIdsTests.cs b/CleanArchitecture.IntegrationTests/gRPC/GetUsersByIdsTests.cs index ab4a95c..5c91388 100644 --- a/CleanArchitecture.IntegrationTests/gRPC/GetUsersByIdsTests.cs +++ b/CleanArchitecture.IntegrationTests/gRPC/GetUsersByIdsTests.cs @@ -31,5 +31,6 @@ public async Task Should_Get_Users_By_Ids() user.Email.Should().Be(createdUser.Email); user.FirstName.Should().Be(createdUser.FirstName); user.LastName.Should().Be(createdUser.LastName); + user.DeletedAt.Should().NotBeNullOrWhiteSpace(); } } \ No newline at end of file