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
new file mode 100644
index 0000000..0576122
--- /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..3aac047 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))]
@@ -66,7 +64,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 +73,6 @@ public sealed class UserController : ApiController
return Response(id);
}
- [Authorize]
[HttpPut]
[SwaggerOperation("Update a user")]
[SwaggerResponse(200, "Request successful", typeof(ResponseMessage))]
@@ -86,7 +82,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 +92,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.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 da17e91..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();
@@ -91,6 +91,7 @@ app.MapHealthChecks("/healthz", new HealthCheckOptions
});
app.MapControllers();
app.MapGrpcService();
+app.MapGrpcService();
app.Run();
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
new file mode 100644
index 0000000..b13ac69
--- /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 GetAllTenantsQueryHandler(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..e5d0622
--- /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 GetTenantByIdQueryHandler(
+ 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/Fixtures/Queries/Users/GetAllUsersTestFixture.cs b/CleanArchitecture.Application.Tests/Fixtures/Queries/Users/GetAllUsersTestFixture.cs
index 0e23086..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,19 +21,16 @@ 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(
- ExistingUserId,
- "max@mustermann.com",
- "Max",
- "Mustermann",
- "Password",
- UserRole.User);
+ ExistingUserId,
+ Guid.NewGuid(),
+ "max@mustermann.com",
+ "Max",
+ "Mustermann",
+ "Password",
+ UserRole.User);
var query = new[] { user }.BuildMock();
@@ -39,12 +40,13 @@ public sealed class GetAllUsersTestFixture : QueryHandlerBaseFixture
public void SetupDeletedUserAsync()
{
var user = new User(
- ExistingUserId,
- "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 741faac..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,19 +22,16 @@ 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(
- ExistingUserId,
- "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,12 +41,13 @@ public sealed class GetUserByIdTestFixture : QueryHandlerBaseFixture
public void SetupDeletedUserAsync()
{
var user = new User(
- ExistingUserId,
- "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
new file mode 100644
index 0000000..6940687
--- /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..230864b
--- /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/CleanArchitecture.Application.csproj b/CleanArchitecture.Application/CleanArchitecture.Application.csproj
index fd17662..3b320a5 100644
--- a/CleanArchitecture.Application/CleanArchitecture.Application.csproj
+++ b/CleanArchitecture.Application/CleanArchitecture.Application.csproj
@@ -6,12 +6,13 @@
-
+
-
-
+
+
+
diff --git a/CleanArchitecture.Application/Extensions/ServiceCollectionExtension.cs b/CleanArchitecture.Application/Extensions/ServiceCollectionExtension.cs
index 784380f..9da4c3b 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,22 @@ 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..e87ddec
--- /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..844a5f4
--- /dev/null
+++ b/CleanArchitecture.Application/Queries/Tenants/GetAll/GetAllTenantsQueryHandler.cs
@@ -0,0 +1,33 @@
+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()
+ .Include(x => x.Users)
+ .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..2e22455
--- /dev/null
+++ b/CleanArchitecture.Application/Queries/Tenants/GetTenantById/GetTenantByIdQueryHandler.cs
@@ -0,0 +1,47 @@
+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;
+using Microsoft.EntityFrameworkCore;
+
+namespace CleanArchitecture.Application.Queries.Tenants.GetTenantById;
+
+public sealed class GetTenantByIdQueryHandler :
+ IRequestHandler
+{
+ private readonly IMediatorHandler _bus;
+ private readonly ITenantRepository _tenantRepository;
+
+ public GetTenantByIdQueryHandler(ITenantRepository tenantRepository, IMediatorHandler bus)
+ {
+ _tenantRepository = tenantRepository;
+ _bus = bus;
+ }
+
+ public async Task Handle(GetTenantByIdQuery request, CancellationToken cancellationToken)
+ {
+ var tenant = _tenantRepository
+ .GetAllNoTracking()
+ .Include(x => x.Users)
+ .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..c10fe20
--- /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/Services/UserService.cs b/CleanArchitecture.Application/Services/UserService.cs
index 8ab4600..f6181f7 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,
@@ -61,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/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..af6bb43
--- /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/TenantsApiImplementation.cs b/CleanArchitecture.Application/gRPC/TenantsApiImplementation.cs
new file mode 100644
index 0000000..414206c
--- /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/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.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.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/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
new file mode 100644
index 0000000..2dce689
--- /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..3fc9210
--- /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 CreateTenantCommandHandler(
+ 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..2156932
--- /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 CreateTenantCommand(
+ 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..8f0d378
--- /dev/null
+++ b/CleanArchitecture.Domain.Tests/CommandHandler/Tenant/DeleteTenant/DeleteTenantCommandHandlerTests.cs
@@ -0,0 +1,64 @@
+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}");
+ }
+
+ [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
new file mode 100644
index 0000000..0617c3b
--- /dev/null
+++ b/CleanArchitecture.Domain.Tests/CommandHandler/Tenant/DeleteTenant/DeleteTenantCommandTestFixture.cs
@@ -0,0 +1,45 @@
+using System;
+using CleanArchitecture.Domain.Commands.Tenants.DeleteTenant;
+using CleanArchitecture.Domain.Enums;
+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 DeleteTenantCommandHandler(
+ 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;
+ }
+
+ public void SetupUser()
+ {
+ User.GetUserRole().Returns(UserRole.User);
+ }
+}
\ 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..b36e294
--- /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 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
new file mode 100644
index 0000000..77a186a
--- /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..ddf762a
--- /dev/null
+++ b/CleanArchitecture.Domain.Tests/CommandHandler/Tenant/UpdateTenant/UpdateTenantCommandTestFixture.cs
@@ -0,0 +1,38 @@
+using System;
+using CleanArchitecture.Domain.Commands.Tenants.UpdateTenant;
+using CleanArchitecture.Domain.Enums;
+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 UpdateTenantCommandHandler(
+ 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..c7c4f11
--- /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 UpdateTenantCommand(
+ id ?? Guid.NewGuid(),
+ name ?? "Test Tenant");
+ }
+}
\ No newline at end of file
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/ChangePasswordCommandTestFixture.cs b/CleanArchitecture.Domain.Tests/CommandHandler/User/ChangePassword/ChangePasswordCommandTestFixture.cs
index 20e2189..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,12 +24,10 @@ public sealed class ChangePasswordCommandTestFixture : CommandHandlerFixtureBase
User);
}
- public ChangePasswordCommandHandler CommandHandler { get; }
- private IUserRepository UserRepository { get; }
-
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/ChangePassword/ChangePasswordCommandValidationTests.cs b/CleanArchitecture.Domain.Tests/CommandHandler/User/ChangePassword/ChangePasswordCommandValidationTests.cs
index 39c8186..b75d5f4 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,13 +84,13 @@ 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(
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 9319274..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,10 +15,14 @@ public sealed class CreateUserCommandHandlerTests
[Fact]
public void Should_Create_User()
{
- _fixture.SetupUser();
+ _fixture.SetupCurrentUser();
+
+ var user = _fixture.SetupUser();
+ _fixture.SetupTenant(user.TenantId);
var command = new CreateUserCommand(
Guid.NewGuid(),
+ user.TenantId,
"test@email.com",
"Test",
"Email",
@@ -27,16 +33,19 @@ public sealed class CreateUserCommandHandlerTests
_fixture
.VerifyNoDomainNotification()
.VerifyCommit()
- .VerifyRaisedEvent(x => x.UserId == command.UserId);
+ .VerifyRaisedEvent(x => x.AggregateId == command.UserId);
}
[Fact]
public void Should_Not_Create_Already_Existing_User()
{
+ _fixture.SetupCurrentUser();
+
var user = _fixture.SetupUser();
var command = new CreateUserCommand(
user.Id,
+ Guid.NewGuid(),
"test@email.com",
"Test",
"Email",
@@ -49,7 +58,92 @@ 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}");
+ }
+
+ [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()
+ {
+ _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 3662b65..3cf3a09 100644
--- a/CleanArchitecture.Domain.Tests/CommandHandler/User/CreateUser/CreateUserCommandTestFixture.cs
+++ b/CleanArchitecture.Domain.Tests/CommandHandler/User/CreateUser/CreateUserCommandTestFixture.cs
@@ -8,23 +8,28 @@ namespace CleanArchitecture.Domain.Tests.CommandHandler.User.CreateUser;
public sealed class CreateUserCommandTestFixture : CommandHandlerFixtureBase
{
+ public CreateUserCommandHandler CommandHandler { get; }
+ public IUserRepository UserRepository { get; }
+ private ITenantRepository TenantRepository { get; }
+
public CreateUserCommandTestFixture()
{
UserRepository = Substitute.For();
+ TenantRepository = Substitute.For();
CommandHandler = new CreateUserCommandHandler(
Bus,
UnitOfWork,
NotificationHandler,
- UserRepository);
+ UserRepository,
+ TenantRepository,
+ User);
}
- public CreateUserCommandHandler CommandHandler { get; }
- private IUserRepository UserRepository { get; }
-
public Entities.User SetupUser()
{
var user = new Entities.User(
+ Guid.NewGuid(),
Guid.NewGuid(),
"max@mustermann.com",
"Max",
@@ -38,4 +43,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 f4635df..05e3d7c 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;
@@ -29,7 +30,7 @@ public sealed class CreateUserCommandValidationTests :
ShouldHaveSingleError(
command,
- DomainErrorCodes.UserEmptyId,
+ DomainErrorCodes.User.UserEmptyId,
"User id may not be empty");
}
@@ -40,7 +41,7 @@ public sealed class CreateUserCommandValidationTests :
ShouldHaveSingleError(
command,
- DomainErrorCodes.UserInvalidEmail,
+ DomainErrorCodes.User.UserInvalidEmail,
"Email is not a valid email address");
}
@@ -51,19 +52,19 @@ public sealed class CreateUserCommandValidationTests :
ShouldHaveSingleError(
command,
- DomainErrorCodes.UserInvalidEmail,
+ DomainErrorCodes.User.UserInvalidEmail,
"Email is not a valid email address");
}
[Fact]
public void Should_Be_Invalid_For_Email_Exceeds_Max_Length()
{
- var command = CreateTestCommand(email: new string('a', 320) + "@test.com");
+ var command = CreateTestCommand(email: new string('a', MaxLengths.User.Email) + "@test.com");
ShouldHaveSingleError(
command,
- DomainErrorCodes.UserEmailExceedsMaxLength,
- "Email may not be longer than 320 characters");
+ DomainErrorCodes.User.UserEmailExceedsMaxLength,
+ $"Email may not be longer than {MaxLengths.User.Email} characters");
}
[Fact]
@@ -73,19 +74,19 @@ public sealed class CreateUserCommandValidationTests :
ShouldHaveSingleError(
command,
- DomainErrorCodes.UserEmptyFirstName,
+ DomainErrorCodes.User.UserEmptyFirstName,
"FirstName may not be empty");
}
[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.UserFirstNameExceedsMaxLength,
- "FirstName may not be longer than 100 characters");
+ DomainErrorCodes.User.UserFirstNameExceedsMaxLength,
+ $"FirstName may not be longer than {MaxLengths.User.FirstName} characters");
}
[Fact]
@@ -95,19 +96,19 @@ public sealed class CreateUserCommandValidationTests :
ShouldHaveSingleError(
command,
- DomainErrorCodes.UserEmptyLastName,
+ DomainErrorCodes.User.UserEmptyLastName,
"LastName may not be empty");
}
[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.UserLastNameExceedsMaxLength,
- "LastName may not be longer than 100 characters");
+ DomainErrorCodes.User.UserLastNameExceedsMaxLength,
+ $"LastName may not be longer than {MaxLengths.User.LastName} characters");
}
[Fact]
@@ -117,12 +118,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 +134,7 @@ public sealed class CreateUserCommandValidationTests :
{
var command = CreateTestCommand(password: "z8tnayvd5FNLU9AQm");
- ShouldHaveSingleError(command, DomainErrorCodes.UserSpecialCharPassword);
+ ShouldHaveSingleError(command, DomainErrorCodes.User.UserSpecialCharPassword);
}
[Fact]
@@ -141,7 +142,7 @@ public sealed class CreateUserCommandValidationTests :
{
var command = CreateTestCommand(password: "z]tnayvdFNLU:]AQm");
- ShouldHaveSingleError(command, DomainErrorCodes.UserNumberPassword);
+ ShouldHaveSingleError(command, DomainErrorCodes.User.UserNumberPassword);
}
[Fact]
@@ -149,7 +150,7 @@ public sealed class CreateUserCommandValidationTests :
{
var command = CreateTestCommand(password: "Z8]TNAYVDFNLU:]AQM");
- ShouldHaveSingleError(command, DomainErrorCodes.UserLowercaseLetterPassword);
+ ShouldHaveSingleError(command, DomainErrorCodes.User.UserLowercaseLetterPassword);
}
[Fact]
@@ -157,7 +158,7 @@ public sealed class CreateUserCommandValidationTests :
{
var command = CreateTestCommand(password: "z8]tnayvd5fnlu9:]aqm");
- ShouldHaveSingleError(command, DomainErrorCodes.UserUppercaseLetterPassword);
+ ShouldHaveSingleError(command, DomainErrorCodes.User.UserUppercaseLetterPassword);
}
[Fact]
@@ -165,7 +166,7 @@ public sealed class CreateUserCommandValidationTests :
{
var command = CreateTestCommand(password: "zA6{");
- ShouldHaveSingleError(command, DomainErrorCodes.UserShortPassword);
+ ShouldHaveSingleError(command, DomainErrorCodes.User.UserShortPassword);
}
[Fact]
@@ -173,18 +174,28 @@ public sealed class CreateUserCommandValidationTests :
{
var command = CreateTestCommand(password: string.Concat(Enumerable.Repeat("zA6{", 12), 12));
- ShouldHaveSingleError(command, DomainErrorCodes.UserLongPassword);
+ 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,
+ Guid? tenantId = null,
string? email = null,
string? firstName = null,
string? lastName = null,
string? password = null)
{
- return new(
+ return new CreateUserCommand(
userId ?? Guid.NewGuid(),
+ tenantId ?? Guid.NewGuid(),
email ?? "test@email.com",
firstName ?? "test",
lastName ?? "email",
diff --git a/CleanArchitecture.Domain.Tests/CommandHandler/User/DeleteUser/DeleteUserCommandHandlerTests.cs b/CleanArchitecture.Domain.Tests/CommandHandler/User/DeleteUser/DeleteUserCommandHandlerTests.cs
index fabfd29..95d2569 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,26 @@ public sealed class DeleteUserCommandHandlerTests
.VerifyAnyDomainNotification()
.VerifyExistingNotification(
ErrorCodes.ObjectNotFound,
- $"There is no User with Id {command.UserId}");
+ $"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 92af9a4..bdd73f9 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,12 +23,10 @@ public sealed class DeleteUserCommandTestFixture : CommandHandlerFixtureBase
User);
}
- public DeleteUserCommandHandler CommandHandler { get; }
- private IUserRepository UserRepository { get; }
-
public Entities.User SetupUser()
{
var user = new Entities.User(
+ Guid.NewGuid(),
Guid.NewGuid(),
"max@mustermann.com",
"Max",
@@ -39,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.Domain.Tests/CommandHandler/User/DeleteUser/DeleteUserCommandValidationTests.cs b/CleanArchitecture.Domain.Tests/CommandHandler/User/DeleteUser/DeleteUserCommandValidationTests.cs
index a2e6e45..8ea63cf 100644
--- a/CleanArchitecture.Domain.Tests/CommandHandler/User/DeleteUser/DeleteUserCommandValidationTests.cs
+++ b/CleanArchitecture.Domain.Tests/CommandHandler/User/DeleteUser/DeleteUserCommandValidationTests.cs
@@ -27,12 +27,12 @@ public sealed class DeleteUserCommandValidationTests :
ShouldHaveSingleError(
command,
- DomainErrorCodes.UserEmptyId,
+ DomainErrorCodes.User.UserEmptyId,
"User id may not be empty");
}
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/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/LoginUserCommandTestFixture.cs b/CleanArchitecture.Domain.Tests/CommandHandler/User/LoginUser/LoginUserCommandTestFixture.cs
index c8f91cd..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,13 +34,10 @@ 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(
+ Guid.NewGuid(),
Guid.NewGuid(),
"max@mustermann.com",
"Max",
diff --git a/CleanArchitecture.Domain.Tests/CommandHandler/User/LoginUser/LoginUserCommandValidationTests.cs b/CleanArchitecture.Domain.Tests/CommandHandler/User/LoginUser/LoginUserCommandValidationTests.cs
index fd85ce8..7b1ca35 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;
@@ -28,7 +29,7 @@ public sealed class LoginUserCommandValidationTests :
ShouldHaveSingleError(
command,
- DomainErrorCodes.UserInvalidEmail,
+ DomainErrorCodes.User.UserInvalidEmail,
"Email is not a valid email address");
}
@@ -39,19 +40,19 @@ public sealed class LoginUserCommandValidationTests :
ShouldHaveSingleError(
command,
- DomainErrorCodes.UserInvalidEmail,
+ DomainErrorCodes.User.UserInvalidEmail,
"Email is not a valid email address");
}
[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.UserEmailExceedsMaxLength,
- "Email may not be longer than 320 characters");
+ DomainErrorCodes.User.UserEmailExceedsMaxLength,
+ $"Email may not be longer than {MaxLengths.User.Email} characters");
}
[Fact]
@@ -61,12 +62,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 +78,7 @@ public sealed class LoginUserCommandValidationTests :
{
var command = CreateTestCommand(password: "z8tnayvd5FNLU9AQm");
- ShouldHaveSingleError(command, DomainErrorCodes.UserSpecialCharPassword);
+ ShouldHaveSingleError(command, DomainErrorCodes.User.UserSpecialCharPassword);
}
[Fact]
@@ -85,7 +86,7 @@ public sealed class LoginUserCommandValidationTests :
{
var command = CreateTestCommand(password: "z]tnayvdFNLU:]AQm");
- ShouldHaveSingleError(command, DomainErrorCodes.UserNumberPassword);
+ ShouldHaveSingleError(command, DomainErrorCodes.User.UserNumberPassword);
}
[Fact]
@@ -93,7 +94,7 @@ public sealed class LoginUserCommandValidationTests :
{
var command = CreateTestCommand(password: "Z8]TNAYVDFNLU:]AQM");
- ShouldHaveSingleError(command, DomainErrorCodes.UserLowercaseLetterPassword);
+ ShouldHaveSingleError(command, DomainErrorCodes.User.UserLowercaseLetterPassword);
}
[Fact]
@@ -101,7 +102,7 @@ public sealed class LoginUserCommandValidationTests :
{
var command = CreateTestCommand(password: "z8]tnayvd5fnlu9:]aqm");
- ShouldHaveSingleError(command, DomainErrorCodes.UserUppercaseLetterPassword);
+ ShouldHaveSingleError(command, DomainErrorCodes.User.UserUppercaseLetterPassword);
}
[Fact]
@@ -109,7 +110,7 @@ public sealed class LoginUserCommandValidationTests :
{
var command = CreateTestCommand(password: "zA6{");
- ShouldHaveSingleError(command, DomainErrorCodes.UserShortPassword);
+ ShouldHaveSingleError(command, DomainErrorCodes.User.UserShortPassword);
}
[Fact]
@@ -117,14 +118,14 @@ 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(
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/UpdateUserCommandHandlerTests.cs b/CleanArchitecture.Domain.Tests/CommandHandler/User/UpdateUser/UpdateUserCommandHandlerTests.cs
index 841c796..c1229b7 100644
--- a/CleanArchitecture.Domain.Tests/CommandHandler/User/UpdateUser/UpdateUserCommandHandlerTests.cs
+++ b/CleanArchitecture.Domain.Tests/CommandHandler/User/UpdateUser/UpdateUserCommandHandlerTests.cs
@@ -23,14 +23,17 @@ public sealed class UpdateUserCommandHandlerTests
"test@email.com",
"Test",
"Email",
- UserRole.User);
+ UserRole.User,
+ Guid.NewGuid());
+
+ _fixture.SetupTenant(command.TenantId);
await _fixture.CommandHandler.Handle(command, default);
_fixture
.VerifyNoDomainNotification()
.VerifyCommit()
- .VerifyRaisedEvent(x => x.UserId == command.UserId);
+ .VerifyRaisedEvent(x => x.AggregateId == command.UserId);
}
[Fact]
@@ -43,7 +46,10 @@ public sealed class UpdateUserCommandHandlerTests
"test@email.com",
"Test",
"Email",
- UserRole.User);
+ UserRole.User,
+ Guid.NewGuid());
+
+ _fixture.SetupTenant(command.TenantId);
await _fixture.CommandHandler.Handle(command, default);
@@ -53,7 +59,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]
@@ -66,11 +72,15 @@ public sealed class UpdateUserCommandHandlerTests
"test@email.com",
"Test",
"Email",
- UserRole.User);
+ UserRole.User,
+ Guid.NewGuid());
+
+ _fixture.SetupTenant(command.TenantId);
_fixture.UserRepository
.GetByEmailAsync(command.Email)
.Returns(new Entities.User(
+ Guid.NewGuid(),
Guid.NewGuid(),
command.Email,
"Some",
@@ -85,7 +95,62 @@ 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}");
+ }
+
+ [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 cd21e9f..4e572d4 100644
--- a/CleanArchitecture.Domain.Tests/CommandHandler/User/UpdateUser/UpdateUserCommandTestFixture.cs
+++ b/CleanArchitecture.Domain.Tests/CommandHandler/User/UpdateUser/UpdateUserCommandTestFixture.cs
@@ -8,24 +8,28 @@ 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();
+ TenantRepository = Substitute.For();
CommandHandler = new UpdateUserCommandHandler(
Bus,
UnitOfWork,
NotificationHandler,
UserRepository,
- User);
+ User,
+ TenantRepository);
}
- public UpdateUserCommandHandler CommandHandler { get; }
- public IUserRepository UserRepository { get; }
-
public Entities.User SetupUser()
{
var user = new Entities.User(
+ Guid.NewGuid(),
Guid.NewGuid(),
"max@mustermann.com",
"Max",
@@ -39,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.Tests/CommandHandler/User/UpdateUser/UpdateUserCommandValidationTests.cs b/CleanArchitecture.Domain.Tests/CommandHandler/User/UpdateUser/UpdateUserCommandValidationTests.cs
index 7a919f4..42de98f 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;
@@ -28,7 +29,7 @@ public sealed class UpdateUserCommandValidationTests :
ShouldHaveSingleError(
command,
- DomainErrorCodes.UserEmptyId,
+ DomainErrorCodes.User.UserEmptyId,
"User id may not be empty");
}
@@ -39,7 +40,7 @@ public sealed class UpdateUserCommandValidationTests :
ShouldHaveSingleError(
command,
- DomainErrorCodes.UserInvalidEmail,
+ DomainErrorCodes.User.UserInvalidEmail,
"Email is not a valid email address");
}
@@ -50,19 +51,19 @@ public sealed class UpdateUserCommandValidationTests :
ShouldHaveSingleError(
command,
- DomainErrorCodes.UserInvalidEmail,
+ DomainErrorCodes.User.UserInvalidEmail,
"Email is not a valid email address");
}
[Fact]
public void Should_Be_Invalid_For_Email_Exceeds_Max_Length()
{
- var command = CreateTestCommand(email: new string('a', 320) + "@test.com");
+ var command = CreateTestCommand(email: new string('a', MaxLengths.User.Email) + "@test.com");
ShouldHaveSingleError(
command,
- DomainErrorCodes.UserEmailExceedsMaxLength,
- "Email may not be longer than 320 characters");
+ DomainErrorCodes.User.UserEmailExceedsMaxLength,
+ $"Email may not be longer than {MaxLengths.User.Email} characters");
}
[Fact]
@@ -72,19 +73,19 @@ public sealed class UpdateUserCommandValidationTests :
ShouldHaveSingleError(
command,
- DomainErrorCodes.UserEmptyFirstName,
+ DomainErrorCodes.User.UserEmptyFirstName,
"FirstName may not be empty");
}
[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.UserFirstNameExceedsMaxLength,
- "FirstName may not be longer than 100 characters");
+ DomainErrorCodes.User.UserFirstNameExceedsMaxLength,
+ $"FirstName may not be longer than {MaxLengths.User.FirstName} characters");
}
[Fact]
@@ -94,33 +95,46 @@ public sealed class UpdateUserCommandValidationTests :
ShouldHaveSingleError(
command,
- DomainErrorCodes.UserEmptyLastName,
+ DomainErrorCodes.User.UserEmptyLastName,
"LastName may not be empty");
}
[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.UserLastNameExceedsMaxLength,
- "LastName may not be longer than 100 characters");
+ DomainErrorCodes.User.UserLastNameExceedsMaxLength,
+ $"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,
UserRole? role = null)
{
- return new(
+ return new UpdateUserCommand(
userId ?? Guid.NewGuid(),
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.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/ApiUser.cs b/CleanArchitecture.Domain/ApiUser.cs
index 708cec4..3e0ea81 100644
--- a/CleanArchitecture.Domain/ApiUser.cs
+++ b/CleanArchitecture.Domain/ApiUser.cs
@@ -55,21 +55,24 @@ 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;
}
+
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/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/CommandHandlerBase.cs b/CleanArchitecture.Domain/Commands/CommandHandlerBase.cs
index 54a557b..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,
@@ -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..b9bc519
--- /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..16d0821
--- /dev/null
+++ b/CleanArchitecture.Domain/Commands/Tenants/CreateTenant/CreateTenantCommandHandler.cs
@@ -0,0 +1,73 @@
+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;
+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;
+ private readonly IUser _user;
+
+ public CreateTenantCommandHandler(
+ IMediatorHandler bus,
+ IUnitOfWork unitOfWork,
+ INotificationHandler notifications,
+ ITenantRepository tenantRepository,
+ IUser user) : base(bus, unitOfWork, notifications)
+ {
+ _tenantRepository = tenantRepository;
+ _user = user;
+ }
+
+ public async Task Handle(CreateTenantCommand request, CancellationToken cancellationToken)
+ {
+ if (!await TestValidityAsync(request))
+ {
+ 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))
+ {
+ 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..9ac7602
--- /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..e83ddc8
--- /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..1303d7b
--- /dev/null
+++ b/CleanArchitecture.Domain/Commands/Tenants/DeleteTenant/DeleteTenantCommandHandler.cs
@@ -0,0 +1,78 @@
+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;
+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 IUser _user;
+ private readonly IUserRepository _userRepository;
+
+ public DeleteTenantCommandHandler(
+ IMediatorHandler bus,
+ IUnitOfWork unitOfWork,
+ INotificationHandler notifications,
+ ITenantRepository tenantRepository,
+ IUserRepository userRepository,
+ IUser user) : base(bus, unitOfWork, notifications)
+ {
+ _tenantRepository = tenantRepository;
+ _userRepository = userRepository;
+ _user = user;
+ }
+
+ public async Task Handle(DeleteTenantCommand request, CancellationToken cancellationToken)
+ {
+ if (!await TestValidityAsync(request))
+ {
+ return;
+ }
+
+ 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);
+
+ 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..7b80034
--- /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..fd2356d
--- /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..dc1e076
--- /dev/null
+++ b/CleanArchitecture.Domain/Commands/Tenants/UpdateTenant/UpdateTenantCommandHandler.cs
@@ -0,0 +1,70 @@
+using System.Threading;
+using System.Threading.Tasks;
+using CleanArchitecture.Domain.Enums;
+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;
+ private readonly IUser _user;
+
+ public UpdateTenantCommandHandler(
+ IMediatorHandler bus,
+ IUnitOfWork unitOfWork,
+ INotificationHandler notifications,
+ ITenantRepository tenantRepository,
+ IUser user) : base(bus, unitOfWork, notifications)
+ {
+ _tenantRepository = tenantRepository;
+ _user = user;
+ }
+
+ public async Task Handle(UpdateTenantCommand request, CancellationToken cancellationToken)
+ {
+ if (!await TestValidityAsync(request))
+ {
+ 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);
+
+ 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..c9abd29
--- /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..18ba9a2 100644
--- a/CleanArchitecture.Domain/Commands/Users/ChangePassword/ChangePasswordCommand.cs
+++ b/CleanArchitecture.Domain/Commands/Users/ChangePassword/ChangePasswordCommand.cs
@@ -4,7 +4,10 @@ namespace CleanArchitecture.Domain.Commands.Users.ChangePassword;
public sealed class ChangePasswordCommand : CommandBase
{
- private readonly ChangePasswordCommandValidation _validation = new();
+ private static readonly ChangePasswordCommandValidation s_validation = new();
+
+ public string Password { get; }
+ public string NewPassword { get; }
public ChangePasswordCommand(string password, string newPassword) : base(Guid.NewGuid())
{
@@ -12,12 +15,9 @@ public sealed class ChangePasswordCommand : CommandBase
NewPassword = newPassword;
}
- public string Password { get; }
- public string NewPassword { get; }
-
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 c9e9e79..3929d0a 100644
--- a/CleanArchitecture.Domain/Commands/Users/CreateUser/CreateUserCommand.cs
+++ b/CleanArchitecture.Domain/Commands/Users/CreateUser/CreateUserCommand.cs
@@ -4,31 +4,34 @@ namespace CleanArchitecture.Domain.Commands.Users.CreateUser;
public sealed class CreateUserCommand : CommandBase
{
- private readonly CreateUserCommandValidation _validation = new();
+ 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,
string email,
string firstName,
string lastName,
string password) : base(userId)
{
UserId = userId;
+ TenantId = tenantId;
Email = email;
FirstName = firstName;
LastName = lastName;
Password = password;
}
- public Guid UserId { get; }
- public string Email { get; }
- public string FirstName { get; }
- public string LastName { get; }
- public string Password { get; }
-
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 98e1b2b..8eb5388 100644
--- a/CleanArchitecture.Domain/Commands/Users/CreateUser/CreateUserCommandHandler.cs
+++ b/CleanArchitecture.Domain/Commands/Users/CreateUser/CreateUserCommandHandler.cs
@@ -15,15 +15,21 @@ namespace CleanArchitecture.Domain.Commands.Users.CreateUser;
public sealed class CreateUserCommandHandler : CommandHandlerBase,
IRequestHandler
{
+ private readonly ITenantRepository _tenantRepository;
+ private readonly IUser _user;
private readonly IUserRepository _userRepository;
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)
@@ -33,27 +39,49 @@ public sealed class CreateUserCommandHandler : CommandHandlerBase,
return;
}
- var existingUser = await _userRepository.GetByIdAsync(request.UserId);
+ var currentUser = await _userRepository.GetByIdAsync(_user.GetUserId());
- if (existingUser != null)
+ if (currentUser is null || currentUser.Role != UserRole.Admin)
{
- await Bus.RaiseEventAsync(
+ await NotifyAsync(
new DomainNotification(
request.MessageType,
- $"There is already a User with Id {request.UserId}",
- DomainErrorCodes.UserAlreadyExists));
+ "You are not allowed to create users",
+ ErrorCodes.InsufficientPermissions));
+ return;
+ }
+
+ var existingUser = await _userRepository.GetByIdAsync(request.UserId);
+
+ if (existingUser is not null)
+ {
+ await NotifyAsync(
+ new DomainNotification(
+ request.MessageType,
+ $"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(
+ await NotifyAsync(
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;
+ }
+
+ if (!await _tenantRepository.ExistsAsync(request.TenantId))
+ {
+ await NotifyAsync(
+ new DomainNotification(
+ request.MessageType,
+ $"There is no tenant with Id {request.TenantId}",
+ ErrorCodes.ObjectNotFound));
return;
}
@@ -61,6 +89,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..371bf60 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;
@@ -9,6 +10,7 @@ public sealed class CreateUserCommandValidation : AbstractValidator cmd.UserId)
.NotEmpty()
- .WithErrorCode(DomainErrorCodes.UserEmptyId)
+ .WithErrorCode(DomainErrorCodes.User.UserEmptyId)
.WithMessage("User id may not be empty");
}
+ private void AddRuleForTenantId()
+ {
+ RuleFor(cmd => cmd.TenantId)
+ .NotEmpty()
+ .WithErrorCode(DomainErrorCodes.Tenant.TenantEmptyId)
+ .WithMessage("Tenant id may not be empty");
+ }
+
private void AddRuleForEmail()
{
RuleFor(cmd => cmd.Email)
.EmailAddress()
- .WithErrorCode(DomainErrorCodes.UserInvalidEmail)
+ .WithErrorCode(DomainErrorCodes.User.UserInvalidEmail)
.WithMessage("Email is not a valid email address")
- .MaximumLength(320)
- .WithErrorCode(DomainErrorCodes.UserEmailExceedsMaxLength)
- .WithMessage("Email may not be longer than 320 characters");
+ .MaximumLength(MaxLengths.User.Email)
+ .WithErrorCode(DomainErrorCodes.User.UserEmailExceedsMaxLength)
+ .WithMessage($"Email may not be longer than {MaxLengths.User.Email} characters");
}
private void AddRuleForFirstName()
{
RuleFor(cmd => cmd.FirstName)
.NotEmpty()
- .WithErrorCode(DomainErrorCodes.UserEmptyFirstName)
+ .WithErrorCode(DomainErrorCodes.User.UserEmptyFirstName)
.WithMessage("FirstName may not be empty")
- .MaximumLength(100)
- .WithErrorCode(DomainErrorCodes.UserFirstNameExceedsMaxLength)
- .WithMessage("FirstName may not be longer than 100 characters");
+ .MaximumLength(MaxLengths.User.FirstName)
+ .WithErrorCode(DomainErrorCodes.User.UserFirstNameExceedsMaxLength)
+ .WithMessage($"FirstName may not be longer than {MaxLengths.User.FirstName} characters");
}
private void AddRuleForLastName()
{
RuleFor(cmd => cmd.LastName)
.NotEmpty()
- .WithErrorCode(DomainErrorCodes.UserEmptyLastName)
+ .WithErrorCode(DomainErrorCodes.User.UserEmptyLastName)
.WithMessage("LastName may not be empty")
- .MaximumLength(100)
- .WithErrorCode(DomainErrorCodes.UserLastNameExceedsMaxLength)
- .WithMessage("LastName may not be longer than 100 characters");
+ .MaximumLength(MaxLengths.User.LastName)
+ .WithErrorCode(DomainErrorCodes.User.UserLastNameExceedsMaxLength)
+ .WithMessage($"LastName may not be longer than {MaxLengths.User.LastName} characters");
}
private void AddRuleForPassword()
diff --git a/CleanArchitecture.Domain/Commands/Users/DeleteUser/DeleteUserCommand.cs b/CleanArchitecture.Domain/Commands/Users/DeleteUser/DeleteUserCommand.cs
index 48c42f5..eda35ae 100644
--- a/CleanArchitecture.Domain/Commands/Users/DeleteUser/DeleteUserCommand.cs
+++ b/CleanArchitecture.Domain/Commands/Users/DeleteUser/DeleteUserCommand.cs
@@ -4,18 +4,18 @@ namespace CleanArchitecture.Domain.Commands.Users.DeleteUser;
public sealed class DeleteUserCommand : CommandBase
{
- private readonly DeleteUserCommandValidation _validation = new();
+ 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 = _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..838d3a9 100644
--- a/CleanArchitecture.Domain/Commands/Users/LoginUser/LoginUserCommand.cs
+++ b/CleanArchitecture.Domain/Commands/Users/LoginUser/LoginUserCommand.cs
@@ -6,7 +6,10 @@ 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 string Email { get; set; }
+ public string Password { get; set; }
public LoginUserCommand(
@@ -17,12 +20,9 @@ public sealed class LoginUserCommand : CommandBase,
Password = password;
}
- public string Email { get; set; }
- public string Password { get; set; }
-
public override bool IsValid()
{
- ValidationResult = _validation.Validate(this);
+ 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 9f668cd..488fca0 100644
--- a/CleanArchitecture.Domain/Commands/Users/LoginUser/LoginUserCommandValidation.cs
+++ b/CleanArchitecture.Domain/Commands/Users/LoginUser/LoginUserCommandValidation.cs
@@ -1,4 +1,5 @@
-using CleanArchitecture.Domain.Errors;
+using CleanArchitecture.Domain.Constants;
+using CleanArchitecture.Domain.Errors;
using CleanArchitecture.Domain.Extensions.Validation;
using FluentValidation;
@@ -16,11 +17,11 @@ public sealed class LoginUserCommandValidation : AbstractValidator cmd.Email)
.EmailAddress()
- .WithErrorCode(DomainErrorCodes.UserInvalidEmail)
+ .WithErrorCode(DomainErrorCodes.User.UserInvalidEmail)
.WithMessage("Email is not a valid email address")
- .MaximumLength(320)
- .WithErrorCode(DomainErrorCodes.UserEmailExceedsMaxLength)
- .WithMessage("Email may not be longer than 320 characters");
+ .MaximumLength(MaxLengths.User.Email)
+ .WithErrorCode(DomainErrorCodes.User.UserEmailExceedsMaxLength)
+ .WithMessage($"Email may not be longer than {MaxLengths.User.Email} characters");
}
private void AddRuleForPassword()
diff --git a/CleanArchitecture.Domain/Commands/Users/UpdateUser/UpdateUserCommand.cs b/CleanArchitecture.Domain/Commands/Users/UpdateUser/UpdateUserCommand.cs
index 13bc3d9..9f7f58c 100644
--- a/CleanArchitecture.Domain/Commands/Users/UpdateUser/UpdateUserCommand.cs
+++ b/CleanArchitecture.Domain/Commands/Users/UpdateUser/UpdateUserCommand.cs
@@ -5,31 +5,33 @@ namespace CleanArchitecture.Domain.Commands.Users.UpdateUser;
public sealed class UpdateUserCommand : CommandBase
{
- private readonly UpdateUserCommandValidation _validation = new();
+ 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,
string firstName,
string lastName,
- UserRole role) : base(userId)
+ UserRole role, Guid tenantId) : base(userId)
{
UserId = userId;
Email = email;
FirstName = firstName;
LastName = lastName;
Role = role;
+ TenantId = tenantId;
}
- public Guid UserId { get; }
- public string Email { get; }
- public string FirstName { get; }
- public string LastName { get; }
- public UserRole Role { get; }
-
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..09ef27e 100644
--- a/CleanArchitecture.Domain/Commands/Users/UpdateUser/UpdateUserCommandHandler.cs
+++ b/CleanArchitecture.Domain/Commands/Users/UpdateUser/UpdateUserCommandHandler.cs
@@ -13,6 +13,7 @@ namespace CleanArchitecture.Domain.Commands.Users.UpdateUser;
public sealed class UpdateUserCommandHandler : CommandHandlerBase,
IRequestHandler
{
+ private readonly ITenantRepository _tenantRepository;
private readonly IUser _user;
private readonly IUserRepository _userRepository;
@@ -21,10 +22,12 @@ public sealed class UpdateUserCommandHandler : CommandHandlerBase,
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)
@@ -36,12 +39,12 @@ public sealed class UpdateUserCommandHandler : CommandHandlerBase,
var user = await _userRepository.GetByIdAsync(request.UserId);
- if (user == null)
+ if (user is null)
{
- await Bus.RaiseEventAsync(
+ 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;
}
@@ -61,13 +64,13 @@ public sealed class UpdateUserCommandHandler : CommandHandlerBase,
{
var existingUser = await _userRepository.GetByEmailAsync(request.Email);
- if (existingUser != null)
+ if (existingUser is not null)
{
- await Bus.RaiseEventAsync(
+ await NotifyAsync(
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;
}
}
@@ -75,6 +78,18 @@ public sealed class UpdateUserCommandHandler : CommandHandlerBase,
if (_user.GetUserRole() == UserRole.Admin)
{
user.SetRole(request.Role);
+
+ 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);
}
user.SetEmail(request.Email);
diff --git a/CleanArchitecture.Domain/Commands/Users/UpdateUser/UpdateUserCommandValidation.cs b/CleanArchitecture.Domain/Commands/Users/UpdateUser/UpdateUserCommandValidation.cs
index 2b29a94..9380fb2 100644
--- a/CleanArchitecture.Domain/Commands/Users/UpdateUser/UpdateUserCommandValidation.cs
+++ b/CleanArchitecture.Domain/Commands/Users/UpdateUser/UpdateUserCommandValidation.cs
@@ -1,3 +1,4 @@
+using CleanArchitecture.Domain.Constants;
using CleanArchitecture.Domain.Errors;
using FluentValidation;
@@ -8,6 +9,7 @@ public sealed class UpdateUserCommandValidation : AbstractValidator cmd.UserId)
.NotEmpty()
- .WithErrorCode(DomainErrorCodes.UserEmptyId)
+ .WithErrorCode(DomainErrorCodes.User.UserEmptyId)
.WithMessage("User id may not be empty");
}
+ private void AddRuleForTenantId()
+ {
+ RuleFor(cmd => cmd.TenantId)
+ .NotEmpty()
+ .WithErrorCode(DomainErrorCodes.Tenant.TenantEmptyId)
+ .WithMessage("Tenant id may not be empty");
+ }
+
private void AddRuleForEmail()
{
RuleFor(cmd => cmd.Email)
.EmailAddress()
- .WithErrorCode(DomainErrorCodes.UserInvalidEmail)
+ .WithErrorCode(DomainErrorCodes.User.UserInvalidEmail)
.WithMessage("Email is not a valid email address")
- .MaximumLength(320)
- .WithErrorCode(DomainErrorCodes.UserEmailExceedsMaxLength)
- .WithMessage("Email may not be longer than 320 characters");
+ .MaximumLength(MaxLengths.User.Email)
+ .WithErrorCode(DomainErrorCodes.User.UserEmailExceedsMaxLength)
+ .WithMessage($"Email may not be longer than {MaxLengths.User.Email} characters");
}
private void AddRuleForFirstName()
{
RuleFor(cmd => cmd.FirstName)
.NotEmpty()
- .WithErrorCode(DomainErrorCodes.UserEmptyFirstName)
+ .WithErrorCode(DomainErrorCodes.User.UserEmptyFirstName)
.WithMessage("FirstName may not be empty")
- .MaximumLength(100)
- .WithErrorCode(DomainErrorCodes.UserFirstNameExceedsMaxLength)
- .WithMessage("FirstName may not be longer than 100 characters");
+ .MaximumLength(MaxLengths.User.FirstName)
+ .WithErrorCode(DomainErrorCodes.User.UserFirstNameExceedsMaxLength)
+ .WithMessage($"FirstName may not be longer than {MaxLengths.User.FirstName} characters");
}
private void AddRuleForLastName()
{
RuleFor(cmd => cmd.LastName)
.NotEmpty()
- .WithErrorCode(DomainErrorCodes.UserEmptyLastName)
+ .WithErrorCode(DomainErrorCodes.User.UserEmptyLastName)
.WithMessage("LastName may not be empty")
- .MaximumLength(100)
- .WithErrorCode(DomainErrorCodes.UserLastNameExceedsMaxLength)
- .WithMessage("LastName may not be longer than 100 characters");
+ .MaximumLength(MaxLengths.User.LastName)
+ .WithErrorCode(DomainErrorCodes.User.UserLastNameExceedsMaxLength)
+ .WithMessage($"LastName may not be longer than {MaxLengths.User.LastName} characters");
}
private void AddRuleForRole()
{
RuleFor(cmd => 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/Constants/Ids.cs b/CleanArchitecture.Domain/Constants/Ids.cs
new file mode 100644
index 0000000..3e7e340
--- /dev/null
+++ b/CleanArchitecture.Domain/Constants/Ids.cs
@@ -0,0 +1,12 @@
+using System;
+
+namespace CleanArchitecture.Domain.Constants;
+
+public static class Ids
+{
+ public static class Seed
+ {
+ public static readonly Guid UserId = new("7e3892c0-9374-49fa-a3fd-53db637a40ae");
+ public static readonly Guid TenantId = new("b542bf25-134c-47a2-a0df-84ed14d03c4a");
+ }
+}
\ No newline at end of file
diff --git a/CleanArchitecture.Domain/Constants/MaxLengths.cs b/CleanArchitecture.Domain/Constants/MaxLengths.cs
new file mode 100644
index 0000000..ab8c6de
--- /dev/null
+++ b/CleanArchitecture.Domain/Constants/MaxLengths.cs
@@ -0,0 +1,17 @@
+namespace CleanArchitecture.Domain.Constants;
+
+public static class MaxLengths
+{
+ public static class User
+ {
+ public const int Email = 320;
+ public const int FirstName = 100;
+ public const int LastName = 100;
+ public const int Password = 128;
+ }
+
+ public static class Tenant
+ {
+ public const int Name = 255;
+ }
+}
\ No newline at end of file
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 606c259..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 1074410..5220441 100644
--- a/CleanArchitecture.Domain/DomainEvents/StoredDomainEvent.cs
+++ b/CleanArchitecture.Domain/DomainEvents/StoredDomainEvent.cs
@@ -24,5 +24,6 @@ public class StoredDomainEvent : DomainEvent
// EF Constructor
protected StoredDomainEvent() : base(Guid.NewGuid())
- { }
-}
+ {
+ }
+}
\ No newline at end of file
diff --git a/CleanArchitecture.Domain/DomainNotifications/StoredDomainNotification.cs b/CleanArchitecture.Domain/DomainNotifications/StoredDomainNotification.cs
index 3a6199c..a4800a4 100644
--- a/CleanArchitecture.Domain/DomainNotifications/StoredDomainNotification.cs
+++ b/CleanArchitecture.Domain/DomainNotifications/StoredDomainNotification.cs
@@ -15,11 +15,11 @@ public class StoredDomainNotification : 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 +31,6 @@ public class StoredDomainNotification : DomainNotification
// EF Constructor
protected StoredDomainNotification() : base(string.Empty, string.Empty, 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
new file mode 100644
index 0000000..9b5f2c5
--- /dev/null
+++ b/CleanArchitecture.Domain/Entities/Tenant.cs
@@ -0,0 +1,23 @@
+using System;
+using System.Collections.Generic;
+
+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 void SetName(string name)
+ {
+ Name = name;
+ }
+}
\ No newline at end of file
diff --git a/CleanArchitecture.Domain/Entities/User.cs b/CleanArchitecture.Domain/Entities/User.cs
index 186913e..f33ea9d 100644
--- a/CleanArchitecture.Domain/Entities/User.cs
+++ b/CleanArchitecture.Domain/Entities/User.cs
@@ -1,26 +1,10 @@
using System;
-using System.Diagnostics.CodeAnalysis;
using CleanArchitecture.Domain.Enums;
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 +13,43 @@ public class User : Entity
public string FullName => $"{FirstName}, {LastName}";
- [MemberNotNull(nameof(Email))]
+ public Guid TenantId { get; private set; }
+ public virtual 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;
}
@@ -101,4 +57,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 a6091de..76977e4 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..dc558b6
--- /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..20675d1
--- /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..eddd6e7 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;
}
@@ -34,6 +42,11 @@ public static class ServiceCollectionExtension
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.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.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/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..08642b0 100644
--- a/CleanArchitecture.Infrastructure/Configurations/UserConfiguration.cs
+++ b/CleanArchitecture.Infrastructure/Configurations/UserConfiguration.cs
@@ -1,4 +1,4 @@
-using System;
+using CleanArchitecture.Domain.Constants;
using CleanArchitecture.Domain.Entities;
using CleanArchitecture.Domain.Enums;
using Microsoft.EntityFrameworkCore;
@@ -13,25 +13,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..6a5517f 100644
--- a/CleanArchitecture.Infrastructure/Database/ApplicationDbContext.cs
+++ b/CleanArchitecture.Infrastructure/Database/ApplicationDbContext.cs
@@ -6,14 +6,16 @@ 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!;
-
protected override void OnModelCreating(ModelBuilder builder)
{
builder.ApplyConfiguration(new UserConfiguration());
+ builder.ApplyConfiguration(new TenantConfiguration());
}
}
\ No newline at end of file
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 fb6a4b3..fa6e2d0 100644
--- a/CleanArchitecture.Infrastructure/Database/EventStoreDbContext.cs
+++ b/CleanArchitecture.Infrastructure/Database/EventStoreDbContext.cs
@@ -6,16 +6,16 @@ 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());
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 0edaffa..f4610b4 100644
--- a/CleanArchitecture.Infrastructure/EventSourcing/EventStoreContext.cs
+++ b/CleanArchitecture.Infrastructure/EventSourcing/EventStoreContext.cs
@@ -13,8 +13,9 @@ public sealed class EventStoreContext : IEventStoreContext
{
_user = user;
- if (httpContextAccessor?.HttpContext == null ||
- !httpContextAccessor.HttpContext.Request.Headers.TryGetValue("X-CLEAN-ARCHITECTURE-CORRELATION-ID", out var id))
+ if (httpContextAccessor?.HttpContext is null ||
+ !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 02d063f..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 =>
@@ -46,6 +46,7 @@ public static class ServiceCollectionExtensions
// Repositories
services.AddScoped();
+ services.AddScoped();
return services;
}
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/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
}
}
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 db13d69..88b7349 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 virtual 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)
@@ -73,6 +73,20 @@ public class BaseRepository : IRepository where TEntity : Enti
}
}
+ public void RemoveRange(IEnumerable entities, bool hardDelete = false)
+ {
+ if (hardDelete)
+ {
+ DbSet.RemoveRange(entities);
+ return;
+ }
+
+ foreach (var entity in entities)
+ {
+ entity.Delete();
+ }
+ }
+
public int SaveChanges()
{
return _dbContext.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/CleanArchitecture.IntegrationTests.csproj b/CleanArchitecture.IntegrationTests/CleanArchitecture.IntegrationTests.csproj
index d008535..56723e6 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,9 @@
-
-
+
+
+
diff --git a/CleanArchitecture.IntegrationTests/Controller/TenantControllerTests.cs b/CleanArchitecture.IntegrationTests/Controller/TenantControllerTests.cs
new file mode 100644
index 0000000..9e7f49f
--- /dev/null
+++ b/CleanArchitecture.IntegrationTests/Controller/TenantControllerTests.cs
@@ -0,0 +1,126 @@
+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()
+ {
+ 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()
+ {
+ 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()
+ {
+ 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()
+ {
+ 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()
+ {
+ 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 ad2cb37..45dd1f4 100644
--- a/CleanArchitecture.IntegrationTests/Controller/UserControllerTests.cs
+++ b/CleanArchitecture.IntegrationTests/Controller/UserControllerTests.cs
@@ -4,9 +4,11 @@ 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;
+using CleanArchitecture.IntegrationTests.Infrastructure.Auth;
using FluentAssertions;
using Xunit;
using Xunit.Priority;
@@ -26,50 +28,33 @@ public sealed class UserControllerTests : IClassFixture
[Fact]
[Priority(0)]
- public async Task Should_Create_User()
+ public async Task Should_Get_All_User()
{
- var user = new CreateUserViewModel(
- _fixture.CreatedUserEmail,
- "Test",
- "Email",
- _fixture.CreatedUserPassword);
-
- var response = await _fixture.ServerClient.PostAsJsonAsync("/api/v1/user", user);
+ var response = await _fixture.ServerClient.GetAsync("/api/v1/user");
response.StatusCode.Should().Be(HttpStatusCode.OK);
- var message = await response.Content.ReadAsJsonAsync();
+ var message = await response.Content.ReadAsJsonAsync