mirror of
https://github.com/alex289/CleanArchitecture.git
synced 2025-08-24 04:08:34 +00:00
commit
0601af8e42
75
CleanArchitecture.Api/Controllers/TenantController.cs
Normal file
75
CleanArchitecture.Api/Controllers/TenantController.cs
Normal file
@ -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<DomainNotification> notifications,
|
||||||
|
ITenantService tenantService) : base(notifications)
|
||||||
|
{
|
||||||
|
_tenantService = tenantService;
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet]
|
||||||
|
[SwaggerOperation("Get a list of all tenants")]
|
||||||
|
[SwaggerResponse(200, "Request successful", typeof(ResponseMessage<IEnumerable<TenantViewModel>>))]
|
||||||
|
public async Task<IActionResult> GetAllTenantsAsync()
|
||||||
|
{
|
||||||
|
var tenants = await _tenantService.GetAllTenantsAsync();
|
||||||
|
return Response(tenants);
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("{id:guid}")]
|
||||||
|
[SwaggerOperation("Get a tenant by id")]
|
||||||
|
[SwaggerResponse(200, "Request successful", typeof(ResponseMessage<TenantViewModel>))]
|
||||||
|
public async Task<IActionResult> 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<Guid>))]
|
||||||
|
public async Task<IActionResult> 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<UpdateTenantViewModel>))]
|
||||||
|
public async Task<IActionResult> 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<Guid>))]
|
||||||
|
public async Task<IActionResult> DeleteTenantAsync([FromRoute] Guid id)
|
||||||
|
{
|
||||||
|
await _tenantService.DeleteTenantAsync(id);
|
||||||
|
return Response(id);
|
||||||
|
}
|
||||||
|
}
|
@ -13,6 +13,7 @@ using Swashbuckle.AspNetCore.Annotations;
|
|||||||
namespace CleanArchitecture.Api.Controllers;
|
namespace CleanArchitecture.Api.Controllers;
|
||||||
|
|
||||||
[ApiController]
|
[ApiController]
|
||||||
|
[Authorize]
|
||||||
[Route("/api/v1/[controller]")]
|
[Route("/api/v1/[controller]")]
|
||||||
public sealed class UserController : ApiController
|
public sealed class UserController : ApiController
|
||||||
{
|
{
|
||||||
@ -25,7 +26,6 @@ public sealed class UserController : ApiController
|
|||||||
_userService = userService;
|
_userService = userService;
|
||||||
}
|
}
|
||||||
|
|
||||||
[Authorize]
|
|
||||||
[HttpGet]
|
[HttpGet]
|
||||||
[SwaggerOperation("Get a list of all users")]
|
[SwaggerOperation("Get a list of all users")]
|
||||||
[SwaggerResponse(200, "Request successful", typeof(ResponseMessage<IEnumerable<UserViewModel>>))]
|
[SwaggerResponse(200, "Request successful", typeof(ResponseMessage<IEnumerable<UserViewModel>>))]
|
||||||
@ -35,7 +35,6 @@ public sealed class UserController : ApiController
|
|||||||
return Response(users);
|
return Response(users);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Authorize]
|
|
||||||
[HttpGet("{id:guid}")]
|
[HttpGet("{id:guid}")]
|
||||||
[SwaggerOperation("Get a user by id")]
|
[SwaggerOperation("Get a user by id")]
|
||||||
[SwaggerResponse(200, "Request successful", typeof(ResponseMessage<UserViewModel>))]
|
[SwaggerResponse(200, "Request successful", typeof(ResponseMessage<UserViewModel>))]
|
||||||
@ -47,7 +46,6 @@ public sealed class UserController : ApiController
|
|||||||
return Response(user);
|
return Response(user);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Authorize]
|
|
||||||
[HttpGet("me")]
|
[HttpGet("me")]
|
||||||
[SwaggerOperation("Get the current active user")]
|
[SwaggerOperation("Get the current active user")]
|
||||||
[SwaggerResponse(200, "Request successful", typeof(ResponseMessage<UserViewModel>))]
|
[SwaggerResponse(200, "Request successful", typeof(ResponseMessage<UserViewModel>))]
|
||||||
@ -66,7 +64,6 @@ public sealed class UserController : ApiController
|
|||||||
return Response(userId);
|
return Response(userId);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Authorize]
|
|
||||||
[HttpDelete("{id:guid}")]
|
[HttpDelete("{id:guid}")]
|
||||||
[SwaggerOperation("Delete a user")]
|
[SwaggerOperation("Delete a user")]
|
||||||
[SwaggerResponse(200, "Request successful", typeof(ResponseMessage<Guid>))]
|
[SwaggerResponse(200, "Request successful", typeof(ResponseMessage<Guid>))]
|
||||||
@ -76,7 +73,6 @@ public sealed class UserController : ApiController
|
|||||||
return Response(id);
|
return Response(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Authorize]
|
|
||||||
[HttpPut]
|
[HttpPut]
|
||||||
[SwaggerOperation("Update a user")]
|
[SwaggerOperation("Update a user")]
|
||||||
[SwaggerResponse(200, "Request successful", typeof(ResponseMessage<UpdateUserViewModel>))]
|
[SwaggerResponse(200, "Request successful", typeof(ResponseMessage<UpdateUserViewModel>))]
|
||||||
@ -86,7 +82,6 @@ public sealed class UserController : ApiController
|
|||||||
return Response(viewModel);
|
return Response(viewModel);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Authorize]
|
|
||||||
[HttpPost("changePassword")]
|
[HttpPost("changePassword")]
|
||||||
[SwaggerOperation("Change a password for the current active user")]
|
[SwaggerOperation("Change a password for the current active user")]
|
||||||
[SwaggerResponse(200, "Request successful", typeof(ResponseMessage<ChangePasswordViewModel>))]
|
[SwaggerResponse(200, "Request successful", typeof(ResponseMessage<ChangePasswordViewModel>))]
|
||||||
@ -97,6 +92,7 @@ public sealed class UserController : ApiController
|
|||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost("login")]
|
[HttpPost("login")]
|
||||||
|
[AllowAnonymous]
|
||||||
[SwaggerOperation("Get a signed token for a user")]
|
[SwaggerOperation("Get a signed token for a user")]
|
||||||
[SwaggerResponse(200, "Request successful", typeof(ResponseMessage<string>))]
|
[SwaggerResponse(200, "Request successful", typeof(ResponseMessage<string>))]
|
||||||
public async Task<IActionResult> LoginUserAsync([FromBody] LoginUserViewModel viewModel)
|
public async Task<IActionResult> LoginUserAsync([FromBody] LoginUserViewModel viewModel)
|
||||||
|
@ -62,7 +62,10 @@ public static class ServiceCollectionExtension
|
|||||||
services.AddAuthentication(
|
services.AddAuthentication(
|
||||||
options => { options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme; })
|
options => { options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme; })
|
||||||
.AddJwtBearer(
|
.AddJwtBearer(
|
||||||
jwtOptions => { jwtOptions.TokenValidationParameters = CreateTokenValidationParameters(configuration); });
|
jwtOptions =>
|
||||||
|
{
|
||||||
|
jwtOptions.TokenValidationParameters = CreateTokenValidationParameters(configuration);
|
||||||
|
});
|
||||||
|
|
||||||
services
|
services
|
||||||
.AddOptions<TokenSettings>()
|
.AddOptions<TokenSettings>()
|
||||||
|
@ -4,9 +4,7 @@ namespace CleanArchitecture.Api.Models;
|
|||||||
|
|
||||||
public sealed class DetailedError
|
public sealed class DetailedError
|
||||||
{
|
{
|
||||||
[JsonPropertyName("code")]
|
[JsonPropertyName("code")] public string Code { get; init; } = string.Empty;
|
||||||
public string Code { get; init; } = string.Empty;
|
|
||||||
|
|
||||||
[JsonPropertyName("data")]
|
[JsonPropertyName("data")] public object? Data { get; init; }
|
||||||
public object? Data { get; init; }
|
|
||||||
}
|
}
|
@ -6,15 +6,12 @@ namespace CleanArchitecture.Api.Models;
|
|||||||
|
|
||||||
public sealed class ResponseMessage<T>
|
public sealed class ResponseMessage<T>
|
||||||
{
|
{
|
||||||
[JsonPropertyName("success")]
|
[JsonPropertyName("success")] public bool Success { get; init; }
|
||||||
public bool Success { get; init; }
|
|
||||||
|
|
||||||
[JsonPropertyName("errors")]
|
[JsonPropertyName("errors")] public IEnumerable<string>? Errors { get; init; }
|
||||||
public IEnumerable<string>? Errors { get; init; }
|
|
||||||
|
|
||||||
[JsonPropertyName("detailedErrors")]
|
[JsonPropertyName("detailedErrors")]
|
||||||
public IEnumerable<DetailedError> DetailedErrors { get; init; } = Enumerable.Empty<DetailedError>();
|
public IEnumerable<DetailedError> DetailedErrors { get; init; } = Enumerable.Empty<DetailedError>();
|
||||||
|
|
||||||
[JsonPropertyName("data")]
|
[JsonPropertyName("data")] public T? Data { get; init; }
|
||||||
public T? Data { get; init; }
|
|
||||||
}
|
}
|
@ -61,9 +61,9 @@ var app = builder.Build();
|
|||||||
using (var scope = app.Services.CreateScope())
|
using (var scope = app.Services.CreateScope())
|
||||||
{
|
{
|
||||||
var services = scope.ServiceProvider;
|
var services = scope.ServiceProvider;
|
||||||
ApplicationDbContext appDbContext = services.GetRequiredService<ApplicationDbContext>();
|
var appDbContext = services.GetRequiredService<ApplicationDbContext>();
|
||||||
EventStoreDbContext storeDbContext = services.GetRequiredService<EventStoreDbContext>();
|
var storeDbContext = services.GetRequiredService<EventStoreDbContext>();
|
||||||
DomainNotificationStoreDbContext domainStoreDbContext = services.GetRequiredService<DomainNotificationStoreDbContext>();
|
var domainStoreDbContext = services.GetRequiredService<DomainNotificationStoreDbContext>();
|
||||||
|
|
||||||
appDbContext.EnsureMigrationsApplied();
|
appDbContext.EnsureMigrationsApplied();
|
||||||
|
|
||||||
@ -91,6 +91,7 @@ app.MapHealthChecks("/healthz", new HealthCheckOptions
|
|||||||
});
|
});
|
||||||
app.MapControllers();
|
app.MapControllers();
|
||||||
app.MapGrpcService<UsersApiImplementation>();
|
app.MapGrpcService<UsersApiImplementation>();
|
||||||
|
app.MapGrpcService<TenantsApiImplementation>();
|
||||||
|
|
||||||
app.Run();
|
app.Run();
|
||||||
|
|
||||||
|
@ -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<ITenantRepository>();
|
||||||
|
|
||||||
|
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> { tenant }.BuildMock();
|
||||||
|
TenantRepository.GetAllNoTracking().Returns(tenantList);
|
||||||
|
|
||||||
|
return tenant;
|
||||||
|
}
|
||||||
|
}
|
@ -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<ITenantRepository>();
|
||||||
|
|
||||||
|
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> { tenant }.BuildMock();
|
||||||
|
TenantRepository.GetAllNoTracking().Returns(tenantList);
|
||||||
|
|
||||||
|
return tenant;
|
||||||
|
}
|
||||||
|
}
|
@ -10,6 +10,10 @@ namespace CleanArchitecture.Application.Tests.Fixtures.Queries.Users;
|
|||||||
|
|
||||||
public sealed class GetAllUsersTestFixture : QueryHandlerBaseFixture
|
public sealed class GetAllUsersTestFixture : QueryHandlerBaseFixture
|
||||||
{
|
{
|
||||||
|
private IUserRepository UserRepository { get; }
|
||||||
|
public GetAllUsersQueryHandler Handler { get; }
|
||||||
|
public Guid ExistingUserId { get; } = Guid.NewGuid();
|
||||||
|
|
||||||
public GetAllUsersTestFixture()
|
public GetAllUsersTestFixture()
|
||||||
{
|
{
|
||||||
UserRepository = Substitute.For<IUserRepository>();
|
UserRepository = Substitute.For<IUserRepository>();
|
||||||
@ -17,14 +21,11 @@ public sealed class GetAllUsersTestFixture : QueryHandlerBaseFixture
|
|||||||
Handler = new GetAllUsersQueryHandler(UserRepository);
|
Handler = new GetAllUsersQueryHandler(UserRepository);
|
||||||
}
|
}
|
||||||
|
|
||||||
private IUserRepository UserRepository { get; }
|
|
||||||
public GetAllUsersQueryHandler Handler { get; }
|
|
||||||
public Guid ExistingUserId { get; } = Guid.NewGuid();
|
|
||||||
|
|
||||||
public void SetupUserAsync()
|
public void SetupUserAsync()
|
||||||
{
|
{
|
||||||
var user = new User(
|
var user = new User(
|
||||||
ExistingUserId,
|
ExistingUserId,
|
||||||
|
Guid.NewGuid(),
|
||||||
"max@mustermann.com",
|
"max@mustermann.com",
|
||||||
"Max",
|
"Max",
|
||||||
"Mustermann",
|
"Mustermann",
|
||||||
@ -40,6 +41,7 @@ public sealed class GetAllUsersTestFixture : QueryHandlerBaseFixture
|
|||||||
{
|
{
|
||||||
var user = new User(
|
var user = new User(
|
||||||
ExistingUserId,
|
ExistingUserId,
|
||||||
|
Guid.NewGuid(),
|
||||||
"max@mustermann.com",
|
"max@mustermann.com",
|
||||||
"Max",
|
"Max",
|
||||||
"Mustermann",
|
"Mustermann",
|
||||||
|
@ -11,6 +11,10 @@ namespace CleanArchitecture.Application.Tests.Fixtures.Queries.Users;
|
|||||||
|
|
||||||
public sealed class GetUserByIdTestFixture : QueryHandlerBaseFixture
|
public sealed class GetUserByIdTestFixture : QueryHandlerBaseFixture
|
||||||
{
|
{
|
||||||
|
private IUserRepository UserRepository { get; }
|
||||||
|
public GetUserByIdQueryHandler Handler { get; }
|
||||||
|
public Guid ExistingUserId { get; } = Guid.NewGuid();
|
||||||
|
|
||||||
public GetUserByIdTestFixture()
|
public GetUserByIdTestFixture()
|
||||||
{
|
{
|
||||||
UserRepository = Substitute.For<IUserRepository>();
|
UserRepository = Substitute.For<IUserRepository>();
|
||||||
@ -18,14 +22,11 @@ public sealed class GetUserByIdTestFixture : QueryHandlerBaseFixture
|
|||||||
Handler = new GetUserByIdQueryHandler(UserRepository, Bus);
|
Handler = new GetUserByIdQueryHandler(UserRepository, Bus);
|
||||||
}
|
}
|
||||||
|
|
||||||
private IUserRepository UserRepository { get; }
|
|
||||||
public GetUserByIdQueryHandler Handler { get; }
|
|
||||||
public Guid ExistingUserId { get; } = Guid.NewGuid();
|
|
||||||
|
|
||||||
public void SetupUserAsync()
|
public void SetupUserAsync()
|
||||||
{
|
{
|
||||||
var user = new User(
|
var user = new User(
|
||||||
ExistingUserId,
|
ExistingUserId,
|
||||||
|
Guid.NewGuid(),
|
||||||
"max@mustermann.com",
|
"max@mustermann.com",
|
||||||
"Max",
|
"Max",
|
||||||
"Mustermann",
|
"Mustermann",
|
||||||
@ -41,6 +42,7 @@ public sealed class GetUserByIdTestFixture : QueryHandlerBaseFixture
|
|||||||
{
|
{
|
||||||
var user = new User(
|
var user = new User(
|
||||||
ExistingUserId,
|
ExistingUserId,
|
||||||
|
Guid.NewGuid(),
|
||||||
"max@mustermann.com",
|
"max@mustermann.com",
|
||||||
"Max",
|
"Max",
|
||||||
"Mustermann",
|
"Mustermann",
|
||||||
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
@ -14,4 +14,5 @@
|
|||||||
<ProjectReference Include="..\CleanArchitecture.Proto\CleanArchitecture.Proto.csproj"/>
|
<ProjectReference Include="..\CleanArchitecture.Proto\CleanArchitecture.Proto.csproj"/>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
@ -1,8 +1,11 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using CleanArchitecture.Application.Interfaces;
|
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.GetAll;
|
||||||
using CleanArchitecture.Application.Queries.Users.GetUserById;
|
using CleanArchitecture.Application.Queries.Users.GetUserById;
|
||||||
using CleanArchitecture.Application.Services;
|
using CleanArchitecture.Application.Services;
|
||||||
|
using CleanArchitecture.Application.ViewModels.Tenants;
|
||||||
using CleanArchitecture.Application.ViewModels.Users;
|
using CleanArchitecture.Application.ViewModels.Users;
|
||||||
using MediatR;
|
using MediatR;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
@ -14,15 +17,22 @@ public static class ServiceCollectionExtension
|
|||||||
public static IServiceCollection AddServices(this IServiceCollection services)
|
public static IServiceCollection AddServices(this IServiceCollection services)
|
||||||
{
|
{
|
||||||
services.AddScoped<IUserService, UserService>();
|
services.AddScoped<IUserService, UserService>();
|
||||||
|
services.AddScoped<ITenantService, TenantService>();
|
||||||
|
|
||||||
return services;
|
return services;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static IServiceCollection AddQueryHandlers(this IServiceCollection services)
|
public static IServiceCollection AddQueryHandlers(this IServiceCollection services)
|
||||||
{
|
{
|
||||||
|
// User
|
||||||
services.AddScoped<IRequestHandler<GetUserByIdQuery, UserViewModel?>, GetUserByIdQueryHandler>();
|
services.AddScoped<IRequestHandler<GetUserByIdQuery, UserViewModel?>, GetUserByIdQueryHandler>();
|
||||||
services.AddScoped<IRequestHandler<GetAllUsersQuery, IEnumerable<UserViewModel>>, GetAllUsersQueryHandler>();
|
services.AddScoped<IRequestHandler<GetAllUsersQuery, IEnumerable<UserViewModel>>, GetAllUsersQueryHandler>();
|
||||||
|
|
||||||
|
// Tenant
|
||||||
|
services.AddScoped<IRequestHandler<GetTenantByIdQuery, TenantViewModel?>, GetTenantByIdQueryHandler>();
|
||||||
|
services
|
||||||
|
.AddScoped<IRequestHandler<GetAllTenantsQuery, IEnumerable<TenantViewModel>>, GetAllTenantsQueryHandler>();
|
||||||
|
|
||||||
return services;
|
return services;
|
||||||
}
|
}
|
||||||
}
|
}
|
15
CleanArchitecture.Application/Interfaces/ITenantService.cs
Normal file
15
CleanArchitecture.Application/Interfaces/ITenantService.cs
Normal file
@ -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<Guid> CreateTenantAsync(CreateTenantViewModel tenant);
|
||||||
|
public Task UpdateTenantAsync(UpdateTenantViewModel tenant);
|
||||||
|
public Task DeleteTenantAsync(Guid tenantId);
|
||||||
|
public Task<TenantViewModel?> GetTenantByIdAsync(Guid tenantId, bool deleted);
|
||||||
|
public Task<IEnumerable<TenantViewModel>> GetAllTenantsAsync();
|
||||||
|
}
|
@ -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<IEnumerable<TenantViewModel>>;
|
@ -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<GetAllTenantsQuery, IEnumerable<TenantViewModel>>
|
||||||
|
{
|
||||||
|
private readonly ITenantRepository _tenantRepository;
|
||||||
|
|
||||||
|
public GetAllTenantsQueryHandler(ITenantRepository tenantRepository)
|
||||||
|
{
|
||||||
|
_tenantRepository = tenantRepository;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<IEnumerable<TenantViewModel>> Handle(
|
||||||
|
GetAllTenantsQuery request,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
return await _tenantRepository
|
||||||
|
.GetAllNoTracking()
|
||||||
|
.Include(x => x.Users)
|
||||||
|
.Where(x => !x.Deleted)
|
||||||
|
.Select(x => TenantViewModel.FromTenant(x))
|
||||||
|
.ToListAsync(cancellationToken);
|
||||||
|
}
|
||||||
|
}
|
@ -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<TenantViewModel?>;
|
@ -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<GetTenantByIdQuery, TenantViewModel?>
|
||||||
|
{
|
||||||
|
private readonly IMediatorHandler _bus;
|
||||||
|
private readonly ITenantRepository _tenantRepository;
|
||||||
|
|
||||||
|
public GetTenantByIdQueryHandler(ITenantRepository tenantRepository, IMediatorHandler bus)
|
||||||
|
{
|
||||||
|
_tenantRepository = tenantRepository;
|
||||||
|
_bus = bus;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<TenantViewModel?> 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);
|
||||||
|
}
|
||||||
|
}
|
@ -30,7 +30,7 @@ public sealed class GetUserByIdQueryHandler :
|
|||||||
x.Id == request.UserId &&
|
x.Id == request.UserId &&
|
||||||
x.Deleted == request.IsDeleted);
|
x.Deleted == request.IsDeleted);
|
||||||
|
|
||||||
if (user == null)
|
if (user is null)
|
||||||
{
|
{
|
||||||
await _bus.RaiseEventAsync(
|
await _bus.RaiseEventAsync(
|
||||||
new DomainNotification(
|
new DomainNotification(
|
||||||
|
56
CleanArchitecture.Application/Services/TenantService.cs
Normal file
56
CleanArchitecture.Application/Services/TenantService.cs
Normal file
@ -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<Guid> 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<TenantViewModel?> GetTenantByIdAsync(Guid tenantId, bool deleted)
|
||||||
|
{
|
||||||
|
return await _bus.QueryAsync(new GetTenantByIdQuery(tenantId, deleted));
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<IEnumerable<TenantViewModel>> GetAllTenantsAsync()
|
||||||
|
{
|
||||||
|
return await _bus.QueryAsync(new GetAllTenantsQuery());
|
||||||
|
}
|
||||||
|
}
|
@ -46,6 +46,7 @@ public sealed class UserService : IUserService
|
|||||||
|
|
||||||
await _bus.SendCommandAsync(new CreateUserCommand(
|
await _bus.SendCommandAsync(new CreateUserCommand(
|
||||||
userId,
|
userId,
|
||||||
|
user.TenantId,
|
||||||
user.Email,
|
user.Email,
|
||||||
user.FirstName,
|
user.FirstName,
|
||||||
user.LastName,
|
user.LastName,
|
||||||
@ -61,7 +62,8 @@ public sealed class UserService : IUserService
|
|||||||
user.Email,
|
user.Email,
|
||||||
user.FirstName,
|
user.FirstName,
|
||||||
user.LastName,
|
user.LastName,
|
||||||
user.Role));
|
user.Role,
|
||||||
|
user.TenantId));
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task DeleteUserAsync(Guid userId)
|
public async Task DeleteUserAsync(Guid userId)
|
||||||
|
@ -0,0 +1,3 @@
|
|||||||
|
namespace CleanArchitecture.Application.ViewModels.Tenants;
|
||||||
|
|
||||||
|
public sealed record CreateTenantViewModel(string Name);
|
@ -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<UserViewModel> Users { get; set; } = new List<UserViewModel>();
|
||||||
|
|
||||||
|
public static TenantViewModel FromTenant(Tenant tenant)
|
||||||
|
{
|
||||||
|
return new TenantViewModel
|
||||||
|
{
|
||||||
|
Id = tenant.Id,
|
||||||
|
Name = tenant.Name,
|
||||||
|
Users = tenant.Users.Select(UserViewModel.FromUser)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,7 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace CleanArchitecture.Application.ViewModels.Tenants;
|
||||||
|
|
||||||
|
public sealed record UpdateTenantViewModel(
|
||||||
|
Guid Id,
|
||||||
|
string Name);
|
@ -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<GetTenantsByIdsResult> GetByIds(
|
||||||
|
GetTenantsByIdsRequest request,
|
||||||
|
ServerCallContext context)
|
||||||
|
{
|
||||||
|
var idsAsGuids = new List<Guid>(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;
|
||||||
|
}
|
||||||
|
}
|
@ -18,8 +18,8 @@ public sealed class UsersApiImplementation : UsersApi.UsersApiBase
|
|||||||
_userRepository = userRepository;
|
_userRepository = userRepository;
|
||||||
}
|
}
|
||||||
|
|
||||||
public override async Task<GetByIdsResult> GetByIds(
|
public override async Task<GetUsersByIdsResult> GetByIds(
|
||||||
GetByIdsRequest request,
|
GetUsersByIdsRequest request,
|
||||||
ServerCallContext context)
|
ServerCallContext context)
|
||||||
{
|
{
|
||||||
var idsAsGuids = new List<Guid>(request.Ids.Count);
|
var idsAsGuids = new List<Guid>(request.Ids.Count);
|
||||||
@ -45,7 +45,7 @@ public sealed class UsersApiImplementation : UsersApi.UsersApiBase
|
|||||||
})
|
})
|
||||||
.ToListAsync();
|
.ToListAsync();
|
||||||
|
|
||||||
var result = new GetByIdsResult();
|
var result = new GetUsersByIdsResult();
|
||||||
|
|
||||||
result.Users.AddRange(users);
|
result.Users.AddRange(users);
|
||||||
|
|
||||||
|
@ -1,7 +1,10 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
namespace CleanArchitecture.Application.ViewModels.Users;
|
namespace CleanArchitecture.Application.ViewModels.Users;
|
||||||
|
|
||||||
public sealed record CreateUserViewModel(
|
public sealed record CreateUserViewModel(
|
||||||
string Email,
|
string Email,
|
||||||
string FirstName,
|
string FirstName,
|
||||||
string LastName,
|
string LastName,
|
||||||
string Password);
|
string Password,
|
||||||
|
Guid TenantId);
|
@ -8,4 +8,5 @@ public sealed record UpdateUserViewModel(
|
|||||||
string Email,
|
string Email,
|
||||||
string FirstName,
|
string FirstName,
|
||||||
string LastName,
|
string LastName,
|
||||||
UserRole Role);
|
UserRole Role,
|
||||||
|
Guid TenantId);
|
@ -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<TenantCreatedEvent>(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<TenantCreatedEvent>()
|
||||||
|
.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<TenantCreatedEvent>()
|
||||||
|
.VerifyAnyDomainNotification()
|
||||||
|
.VerifyExistingNotification(
|
||||||
|
DomainErrorCodes.Tenant.TenantAlreadyExists,
|
||||||
|
$"There is already a tenant with Id {command.AggregateId}");
|
||||||
|
}
|
||||||
|
}
|
@ -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<ITenantRepository>();
|
||||||
|
|
||||||
|
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<Guid>(x => x == id))
|
||||||
|
.Returns(true);
|
||||||
|
}
|
||||||
|
}
|
@ -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<CreateTenantCommand, CreateTenantCommandValidation>
|
||||||
|
{
|
||||||
|
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");
|
||||||
|
}
|
||||||
|
}
|
@ -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<TenantDeletedEvent>(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<TenantDeletedEvent>()
|
||||||
|
.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<TenantDeletedEvent>()
|
||||||
|
.VerifyAnyDomainNotification()
|
||||||
|
.VerifyExistingNotification(
|
||||||
|
ErrorCodes.InsufficientPermissions,
|
||||||
|
$"No permission to delete tenant {command.AggregateId}");
|
||||||
|
}
|
||||||
|
}
|
@ -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<ITenantRepository>();
|
||||||
|
UserRepository = Substitute.For<IUserRepository>();
|
||||||
|
|
||||||
|
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<Guid>(y => y == tenant.Id))
|
||||||
|
.Returns(tenant);
|
||||||
|
|
||||||
|
return tenant;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetupUser()
|
||||||
|
{
|
||||||
|
User.GetUserRole().Returns(UserRole.User);
|
||||||
|
}
|
||||||
|
}
|
@ -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<DeleteTenantCommand, DeleteTenantCommandValidation>
|
||||||
|
{
|
||||||
|
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());
|
||||||
|
}
|
||||||
|
}
|
@ -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<TenantUpdatedEvent>(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<TenantUpdatedEvent>()
|
||||||
|
.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<TenantUpdatedEvent>()
|
||||||
|
.VerifyAnyDomainNotification()
|
||||||
|
.VerifyExistingNotification(
|
||||||
|
ErrorCodes.ObjectNotFound,
|
||||||
|
$"There is no tenant with Id {command.AggregateId}");
|
||||||
|
}
|
||||||
|
}
|
@ -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<ITenantRepository>();
|
||||||
|
|
||||||
|
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<Guid>(x => x == id))
|
||||||
|
.Returns(new Entities.Tenant(id, "Test Tenant"));
|
||||||
|
}
|
||||||
|
}
|
@ -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<UpdateTenantCommand, UpdateTenantCommandValidation>
|
||||||
|
{
|
||||||
|
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");
|
||||||
|
}
|
||||||
|
}
|
@ -22,7 +22,7 @@ public sealed class ChangePasswordCommandHandlerTests
|
|||||||
_fixture
|
_fixture
|
||||||
.VerifyNoDomainNotification()
|
.VerifyNoDomainNotification()
|
||||||
.VerifyCommit()
|
.VerifyCommit()
|
||||||
.VerifyRaisedEvent<PasswordChangedEvent>(x => x.UserId == user.Id);
|
.VerifyRaisedEvent<PasswordChangedEvent>(x => x.AggregateId == user.Id);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
@ -40,7 +40,7 @@ public sealed class ChangePasswordCommandHandlerTests
|
|||||||
.VerifyAnyDomainNotification()
|
.VerifyAnyDomainNotification()
|
||||||
.VerifyExistingNotification(
|
.VerifyExistingNotification(
|
||||||
ErrorCodes.ObjectNotFound,
|
ErrorCodes.ObjectNotFound,
|
||||||
$"There is no User with Id {userId}");
|
$"There is no user with Id {userId}");
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
@ -57,7 +57,7 @@ public sealed class ChangePasswordCommandHandlerTests
|
|||||||
.VerifyNoRaisedEvent<UserUpdatedEvent>()
|
.VerifyNoRaisedEvent<UserUpdatedEvent>()
|
||||||
.VerifyAnyDomainNotification()
|
.VerifyAnyDomainNotification()
|
||||||
.VerifyExistingNotification(
|
.VerifyExistingNotification(
|
||||||
DomainErrorCodes.UserPasswordIncorrect,
|
DomainErrorCodes.User.UserPasswordIncorrect,
|
||||||
"The password is incorrect");
|
"The password is incorrect");
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -9,6 +9,9 @@ namespace CleanArchitecture.Domain.Tests.CommandHandler.User.ChangePassword;
|
|||||||
|
|
||||||
public sealed class ChangePasswordCommandTestFixture : CommandHandlerFixtureBase
|
public sealed class ChangePasswordCommandTestFixture : CommandHandlerFixtureBase
|
||||||
{
|
{
|
||||||
|
public ChangePasswordCommandHandler CommandHandler { get; }
|
||||||
|
private IUserRepository UserRepository { get; }
|
||||||
|
|
||||||
public ChangePasswordCommandTestFixture()
|
public ChangePasswordCommandTestFixture()
|
||||||
{
|
{
|
||||||
UserRepository = Substitute.For<IUserRepository>();
|
UserRepository = Substitute.For<IUserRepository>();
|
||||||
@ -21,12 +24,10 @@ public sealed class ChangePasswordCommandTestFixture : CommandHandlerFixtureBase
|
|||||||
User);
|
User);
|
||||||
}
|
}
|
||||||
|
|
||||||
public ChangePasswordCommandHandler CommandHandler { get; }
|
|
||||||
private IUserRepository UserRepository { get; }
|
|
||||||
|
|
||||||
public Entities.User SetupUser()
|
public Entities.User SetupUser()
|
||||||
{
|
{
|
||||||
var user = new Entities.User(
|
var user = new Entities.User(
|
||||||
|
Guid.NewGuid(),
|
||||||
Guid.NewGuid(),
|
Guid.NewGuid(),
|
||||||
"max@mustermann.com",
|
"max@mustermann.com",
|
||||||
"Max",
|
"Max",
|
||||||
|
@ -28,12 +28,12 @@ public sealed class ChangePasswordCommandValidationTests :
|
|||||||
|
|
||||||
var errors = new List<string>
|
var errors = new List<string>
|
||||||
{
|
{
|
||||||
DomainErrorCodes.UserEmptyPassword,
|
DomainErrorCodes.User.UserEmptyPassword,
|
||||||
DomainErrorCodes.UserSpecialCharPassword,
|
DomainErrorCodes.User.UserSpecialCharPassword,
|
||||||
DomainErrorCodes.UserNumberPassword,
|
DomainErrorCodes.User.UserNumberPassword,
|
||||||
DomainErrorCodes.UserLowercaseLetterPassword,
|
DomainErrorCodes.User.UserLowercaseLetterPassword,
|
||||||
DomainErrorCodes.UserUppercaseLetterPassword,
|
DomainErrorCodes.User.UserUppercaseLetterPassword,
|
||||||
DomainErrorCodes.UserShortPassword
|
DomainErrorCodes.User.UserShortPassword
|
||||||
};
|
};
|
||||||
|
|
||||||
ShouldHaveExpectedErrors(command, errors.ToArray());
|
ShouldHaveExpectedErrors(command, errors.ToArray());
|
||||||
@ -44,7 +44,7 @@ public sealed class ChangePasswordCommandValidationTests :
|
|||||||
{
|
{
|
||||||
var command = CreateTestCommand("z8tnayvd5FNLU9AQm");
|
var command = CreateTestCommand("z8tnayvd5FNLU9AQm");
|
||||||
|
|
||||||
ShouldHaveSingleError(command, DomainErrorCodes.UserSpecialCharPassword);
|
ShouldHaveSingleError(command, DomainErrorCodes.User.UserSpecialCharPassword);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
@ -52,7 +52,7 @@ public sealed class ChangePasswordCommandValidationTests :
|
|||||||
{
|
{
|
||||||
var command = CreateTestCommand("z]tnayvdFNLU:]AQm");
|
var command = CreateTestCommand("z]tnayvdFNLU:]AQm");
|
||||||
|
|
||||||
ShouldHaveSingleError(command, DomainErrorCodes.UserNumberPassword);
|
ShouldHaveSingleError(command, DomainErrorCodes.User.UserNumberPassword);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
@ -60,7 +60,7 @@ public sealed class ChangePasswordCommandValidationTests :
|
|||||||
{
|
{
|
||||||
var command = CreateTestCommand("Z8]TNAYVDFNLU:]AQM");
|
var command = CreateTestCommand("Z8]TNAYVDFNLU:]AQM");
|
||||||
|
|
||||||
ShouldHaveSingleError(command, DomainErrorCodes.UserLowercaseLetterPassword);
|
ShouldHaveSingleError(command, DomainErrorCodes.User.UserLowercaseLetterPassword);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
@ -68,7 +68,7 @@ public sealed class ChangePasswordCommandValidationTests :
|
|||||||
{
|
{
|
||||||
var command = CreateTestCommand("z8]tnayvd5fnlu9:]aqm");
|
var command = CreateTestCommand("z8]tnayvd5fnlu9:]aqm");
|
||||||
|
|
||||||
ShouldHaveSingleError(command, DomainErrorCodes.UserUppercaseLetterPassword);
|
ShouldHaveSingleError(command, DomainErrorCodes.User.UserUppercaseLetterPassword);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
@ -76,7 +76,7 @@ public sealed class ChangePasswordCommandValidationTests :
|
|||||||
{
|
{
|
||||||
var command = CreateTestCommand("zA6{");
|
var command = CreateTestCommand("zA6{");
|
||||||
|
|
||||||
ShouldHaveSingleError(command, DomainErrorCodes.UserShortPassword);
|
ShouldHaveSingleError(command, DomainErrorCodes.User.UserShortPassword);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
@ -84,13 +84,13 @@ public sealed class ChangePasswordCommandValidationTests :
|
|||||||
{
|
{
|
||||||
var command = CreateTestCommand(string.Concat(Enumerable.Repeat("zA6{", 12), 12));
|
var command = CreateTestCommand(string.Concat(Enumerable.Repeat("zA6{", 12), 12));
|
||||||
|
|
||||||
ShouldHaveSingleError(command, DomainErrorCodes.UserLongPassword);
|
ShouldHaveSingleError(command, DomainErrorCodes.User.UserLongPassword);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static ChangePasswordCommand CreateTestCommand(
|
private static ChangePasswordCommand CreateTestCommand(
|
||||||
string? password = null, string? newPassword = null)
|
string? password = null, string? newPassword = null)
|
||||||
{
|
{
|
||||||
return new(
|
return new ChangePasswordCommand(
|
||||||
password ?? "z8]tnayvd5FNLU9:]AQm",
|
password ?? "z8]tnayvd5FNLU9:]AQm",
|
||||||
newPassword ?? "z8]tnayvd5FNLU9:]AQw");
|
newPassword ?? "z8]tnayvd5FNLU9:]AQw");
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,9 @@
|
|||||||
using System;
|
using System;
|
||||||
using CleanArchitecture.Domain.Commands.Users.CreateUser;
|
using CleanArchitecture.Domain.Commands.Users.CreateUser;
|
||||||
|
using CleanArchitecture.Domain.Enums;
|
||||||
using CleanArchitecture.Domain.Errors;
|
using CleanArchitecture.Domain.Errors;
|
||||||
using CleanArchitecture.Domain.Events.User;
|
using CleanArchitecture.Domain.Events.User;
|
||||||
|
using NSubstitute;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
||||||
namespace CleanArchitecture.Domain.Tests.CommandHandler.User.CreateUser;
|
namespace CleanArchitecture.Domain.Tests.CommandHandler.User.CreateUser;
|
||||||
@ -13,10 +15,14 @@ public sealed class CreateUserCommandHandlerTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public void Should_Create_User()
|
public void Should_Create_User()
|
||||||
{
|
{
|
||||||
_fixture.SetupUser();
|
_fixture.SetupCurrentUser();
|
||||||
|
|
||||||
|
var user = _fixture.SetupUser();
|
||||||
|
_fixture.SetupTenant(user.TenantId);
|
||||||
|
|
||||||
var command = new CreateUserCommand(
|
var command = new CreateUserCommand(
|
||||||
Guid.NewGuid(),
|
Guid.NewGuid(),
|
||||||
|
user.TenantId,
|
||||||
"test@email.com",
|
"test@email.com",
|
||||||
"Test",
|
"Test",
|
||||||
"Email",
|
"Email",
|
||||||
@ -27,16 +33,19 @@ public sealed class CreateUserCommandHandlerTests
|
|||||||
_fixture
|
_fixture
|
||||||
.VerifyNoDomainNotification()
|
.VerifyNoDomainNotification()
|
||||||
.VerifyCommit()
|
.VerifyCommit()
|
||||||
.VerifyRaisedEvent<UserCreatedEvent>(x => x.UserId == command.UserId);
|
.VerifyRaisedEvent<UserCreatedEvent>(x => x.AggregateId == command.UserId);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void Should_Not_Create_Already_Existing_User()
|
public void Should_Not_Create_Already_Existing_User()
|
||||||
{
|
{
|
||||||
|
_fixture.SetupCurrentUser();
|
||||||
|
|
||||||
var user = _fixture.SetupUser();
|
var user = _fixture.SetupUser();
|
||||||
|
|
||||||
var command = new CreateUserCommand(
|
var command = new CreateUserCommand(
|
||||||
user.Id,
|
user.Id,
|
||||||
|
Guid.NewGuid(),
|
||||||
"test@email.com",
|
"test@email.com",
|
||||||
"Test",
|
"Test",
|
||||||
"Email",
|
"Email",
|
||||||
@ -49,7 +58,92 @@ public sealed class CreateUserCommandHandlerTests
|
|||||||
.VerifyNoRaisedEvent<UserCreatedEvent>()
|
.VerifyNoRaisedEvent<UserCreatedEvent>()
|
||||||
.VerifyAnyDomainNotification()
|
.VerifyAnyDomainNotification()
|
||||||
.VerifyExistingNotification(
|
.VerifyExistingNotification(
|
||||||
DomainErrorCodes.UserAlreadyExists,
|
DomainErrorCodes.User.UserAlreadyExists,
|
||||||
$"There is already a User with Id {command.UserId}");
|
$"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<string>(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<UserCreatedEvent>()
|
||||||
|
.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<UserCreatedEvent>()
|
||||||
|
.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<UserCreatedEvent>()
|
||||||
|
.VerifyAnyDomainNotification()
|
||||||
|
.VerifyExistingNotification(
|
||||||
|
ErrorCodes.InsufficientPermissions,
|
||||||
|
"You are not allowed to create users");
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -8,23 +8,28 @@ namespace CleanArchitecture.Domain.Tests.CommandHandler.User.CreateUser;
|
|||||||
|
|
||||||
public sealed class CreateUserCommandTestFixture : CommandHandlerFixtureBase
|
public sealed class CreateUserCommandTestFixture : CommandHandlerFixtureBase
|
||||||
{
|
{
|
||||||
|
public CreateUserCommandHandler CommandHandler { get; }
|
||||||
|
public IUserRepository UserRepository { get; }
|
||||||
|
private ITenantRepository TenantRepository { get; }
|
||||||
|
|
||||||
public CreateUserCommandTestFixture()
|
public CreateUserCommandTestFixture()
|
||||||
{
|
{
|
||||||
UserRepository = Substitute.For<IUserRepository>();
|
UserRepository = Substitute.For<IUserRepository>();
|
||||||
|
TenantRepository = Substitute.For<ITenantRepository>();
|
||||||
|
|
||||||
CommandHandler = new CreateUserCommandHandler(
|
CommandHandler = new CreateUserCommandHandler(
|
||||||
Bus,
|
Bus,
|
||||||
UnitOfWork,
|
UnitOfWork,
|
||||||
NotificationHandler,
|
NotificationHandler,
|
||||||
UserRepository);
|
UserRepository,
|
||||||
|
TenantRepository,
|
||||||
|
User);
|
||||||
}
|
}
|
||||||
|
|
||||||
public CreateUserCommandHandler CommandHandler { get; }
|
|
||||||
private IUserRepository UserRepository { get; }
|
|
||||||
|
|
||||||
public Entities.User SetupUser()
|
public Entities.User SetupUser()
|
||||||
{
|
{
|
||||||
var user = new Entities.User(
|
var user = new Entities.User(
|
||||||
|
Guid.NewGuid(),
|
||||||
Guid.NewGuid(),
|
Guid.NewGuid(),
|
||||||
"max@mustermann.com",
|
"max@mustermann.com",
|
||||||
"Max",
|
"Max",
|
||||||
@ -38,4 +43,29 @@ public sealed class CreateUserCommandTestFixture : CommandHandlerFixtureBase
|
|||||||
|
|
||||||
return user;
|
return user;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void SetupCurrentUser()
|
||||||
|
{
|
||||||
|
var userId = Guid.NewGuid();
|
||||||
|
|
||||||
|
User.GetUserId().Returns(userId);
|
||||||
|
|
||||||
|
UserRepository
|
||||||
|
.GetByIdAsync(Arg.Is<Guid>(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<Guid>(y => y == tenantId))
|
||||||
|
.Returns(true);
|
||||||
|
}
|
||||||
}
|
}
|
@ -2,6 +2,7 @@ using System;
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using CleanArchitecture.Domain.Commands.Users.CreateUser;
|
using CleanArchitecture.Domain.Commands.Users.CreateUser;
|
||||||
|
using CleanArchitecture.Domain.Constants;
|
||||||
using CleanArchitecture.Domain.Errors;
|
using CleanArchitecture.Domain.Errors;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
||||||
@ -29,7 +30,7 @@ public sealed class CreateUserCommandValidationTests :
|
|||||||
|
|
||||||
ShouldHaveSingleError(
|
ShouldHaveSingleError(
|
||||||
command,
|
command,
|
||||||
DomainErrorCodes.UserEmptyId,
|
DomainErrorCodes.User.UserEmptyId,
|
||||||
"User id may not be empty");
|
"User id may not be empty");
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -40,7 +41,7 @@ public sealed class CreateUserCommandValidationTests :
|
|||||||
|
|
||||||
ShouldHaveSingleError(
|
ShouldHaveSingleError(
|
||||||
command,
|
command,
|
||||||
DomainErrorCodes.UserInvalidEmail,
|
DomainErrorCodes.User.UserInvalidEmail,
|
||||||
"Email is not a valid email address");
|
"Email is not a valid email address");
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -51,19 +52,19 @@ public sealed class CreateUserCommandValidationTests :
|
|||||||
|
|
||||||
ShouldHaveSingleError(
|
ShouldHaveSingleError(
|
||||||
command,
|
command,
|
||||||
DomainErrorCodes.UserInvalidEmail,
|
DomainErrorCodes.User.UserInvalidEmail,
|
||||||
"Email is not a valid email address");
|
"Email is not a valid email address");
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void Should_Be_Invalid_For_Email_Exceeds_Max_Length()
|
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(
|
ShouldHaveSingleError(
|
||||||
command,
|
command,
|
||||||
DomainErrorCodes.UserEmailExceedsMaxLength,
|
DomainErrorCodes.User.UserEmailExceedsMaxLength,
|
||||||
"Email may not be longer than 320 characters");
|
$"Email may not be longer than {MaxLengths.User.Email} characters");
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
@ -73,19 +74,19 @@ public sealed class CreateUserCommandValidationTests :
|
|||||||
|
|
||||||
ShouldHaveSingleError(
|
ShouldHaveSingleError(
|
||||||
command,
|
command,
|
||||||
DomainErrorCodes.UserEmptyFirstName,
|
DomainErrorCodes.User.UserEmptyFirstName,
|
||||||
"FirstName may not be empty");
|
"FirstName may not be empty");
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void Should_Be_Invalid_For_First_Name_Exceeds_Max_Length()
|
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(
|
ShouldHaveSingleError(
|
||||||
command,
|
command,
|
||||||
DomainErrorCodes.UserFirstNameExceedsMaxLength,
|
DomainErrorCodes.User.UserFirstNameExceedsMaxLength,
|
||||||
"FirstName may not be longer than 100 characters");
|
$"FirstName may not be longer than {MaxLengths.User.FirstName} characters");
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
@ -95,19 +96,19 @@ public sealed class CreateUserCommandValidationTests :
|
|||||||
|
|
||||||
ShouldHaveSingleError(
|
ShouldHaveSingleError(
|
||||||
command,
|
command,
|
||||||
DomainErrorCodes.UserEmptyLastName,
|
DomainErrorCodes.User.UserEmptyLastName,
|
||||||
"LastName may not be empty");
|
"LastName may not be empty");
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void Should_Be_Invalid_For_Last_Name_Exceeds_Max_Length()
|
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(
|
ShouldHaveSingleError(
|
||||||
command,
|
command,
|
||||||
DomainErrorCodes.UserLastNameExceedsMaxLength,
|
DomainErrorCodes.User.UserLastNameExceedsMaxLength,
|
||||||
"LastName may not be longer than 100 characters");
|
$"LastName may not be longer than {MaxLengths.User.LastName} characters");
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
@ -117,12 +118,12 @@ public sealed class CreateUserCommandValidationTests :
|
|||||||
|
|
||||||
var errors = new List<string>
|
var errors = new List<string>
|
||||||
{
|
{
|
||||||
DomainErrorCodes.UserEmptyPassword,
|
DomainErrorCodes.User.UserEmptyPassword,
|
||||||
DomainErrorCodes.UserSpecialCharPassword,
|
DomainErrorCodes.User.UserSpecialCharPassword,
|
||||||
DomainErrorCodes.UserNumberPassword,
|
DomainErrorCodes.User.UserNumberPassword,
|
||||||
DomainErrorCodes.UserLowercaseLetterPassword,
|
DomainErrorCodes.User.UserLowercaseLetterPassword,
|
||||||
DomainErrorCodes.UserUppercaseLetterPassword,
|
DomainErrorCodes.User.UserUppercaseLetterPassword,
|
||||||
DomainErrorCodes.UserShortPassword
|
DomainErrorCodes.User.UserShortPassword
|
||||||
};
|
};
|
||||||
|
|
||||||
ShouldHaveExpectedErrors(command, errors.ToArray());
|
ShouldHaveExpectedErrors(command, errors.ToArray());
|
||||||
@ -133,7 +134,7 @@ public sealed class CreateUserCommandValidationTests :
|
|||||||
{
|
{
|
||||||
var command = CreateTestCommand(password: "z8tnayvd5FNLU9AQm");
|
var command = CreateTestCommand(password: "z8tnayvd5FNLU9AQm");
|
||||||
|
|
||||||
ShouldHaveSingleError(command, DomainErrorCodes.UserSpecialCharPassword);
|
ShouldHaveSingleError(command, DomainErrorCodes.User.UserSpecialCharPassword);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
@ -141,7 +142,7 @@ public sealed class CreateUserCommandValidationTests :
|
|||||||
{
|
{
|
||||||
var command = CreateTestCommand(password: "z]tnayvdFNLU:]AQm");
|
var command = CreateTestCommand(password: "z]tnayvdFNLU:]AQm");
|
||||||
|
|
||||||
ShouldHaveSingleError(command, DomainErrorCodes.UserNumberPassword);
|
ShouldHaveSingleError(command, DomainErrorCodes.User.UserNumberPassword);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
@ -149,7 +150,7 @@ public sealed class CreateUserCommandValidationTests :
|
|||||||
{
|
{
|
||||||
var command = CreateTestCommand(password: "Z8]TNAYVDFNLU:]AQM");
|
var command = CreateTestCommand(password: "Z8]TNAYVDFNLU:]AQM");
|
||||||
|
|
||||||
ShouldHaveSingleError(command, DomainErrorCodes.UserLowercaseLetterPassword);
|
ShouldHaveSingleError(command, DomainErrorCodes.User.UserLowercaseLetterPassword);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
@ -157,7 +158,7 @@ public sealed class CreateUserCommandValidationTests :
|
|||||||
{
|
{
|
||||||
var command = CreateTestCommand(password: "z8]tnayvd5fnlu9:]aqm");
|
var command = CreateTestCommand(password: "z8]tnayvd5fnlu9:]aqm");
|
||||||
|
|
||||||
ShouldHaveSingleError(command, DomainErrorCodes.UserUppercaseLetterPassword);
|
ShouldHaveSingleError(command, DomainErrorCodes.User.UserUppercaseLetterPassword);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
@ -165,7 +166,7 @@ public sealed class CreateUserCommandValidationTests :
|
|||||||
{
|
{
|
||||||
var command = CreateTestCommand(password: "zA6{");
|
var command = CreateTestCommand(password: "zA6{");
|
||||||
|
|
||||||
ShouldHaveSingleError(command, DomainErrorCodes.UserShortPassword);
|
ShouldHaveSingleError(command, DomainErrorCodes.User.UserShortPassword);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
@ -173,18 +174,28 @@ public sealed class CreateUserCommandValidationTests :
|
|||||||
{
|
{
|
||||||
var command = CreateTestCommand(password: string.Concat(Enumerable.Repeat("zA6{", 12), 12));
|
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(
|
private static CreateUserCommand CreateTestCommand(
|
||||||
Guid? userId = null,
|
Guid? userId = null,
|
||||||
|
Guid? tenantId = null,
|
||||||
string? email = null,
|
string? email = null,
|
||||||
string? firstName = null,
|
string? firstName = null,
|
||||||
string? lastName = null,
|
string? lastName = null,
|
||||||
string? password = null)
|
string? password = null)
|
||||||
{
|
{
|
||||||
return new(
|
return new CreateUserCommand(
|
||||||
userId ?? Guid.NewGuid(),
|
userId ?? Guid.NewGuid(),
|
||||||
|
tenantId ?? Guid.NewGuid(),
|
||||||
email ?? "test@email.com",
|
email ?? "test@email.com",
|
||||||
firstName ?? "test",
|
firstName ?? "test",
|
||||||
lastName ?? "email",
|
lastName ?? "email",
|
||||||
|
@ -22,7 +22,7 @@ public sealed class DeleteUserCommandHandlerTests
|
|||||||
_fixture
|
_fixture
|
||||||
.VerifyNoDomainNotification()
|
.VerifyNoDomainNotification()
|
||||||
.VerifyCommit()
|
.VerifyCommit()
|
||||||
.VerifyRaisedEvent<UserDeletedEvent>(x => x.UserId == user.Id);
|
.VerifyRaisedEvent<UserDeletedEvent>(x => x.AggregateId == user.Id);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
@ -40,6 +40,26 @@ public sealed class DeleteUserCommandHandlerTests
|
|||||||
.VerifyAnyDomainNotification()
|
.VerifyAnyDomainNotification()
|
||||||
.VerifyExistingNotification(
|
.VerifyExistingNotification(
|
||||||
ErrorCodes.ObjectNotFound,
|
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<UserDeletedEvent>()
|
||||||
|
.VerifyAnyDomainNotification()
|
||||||
|
.VerifyExistingNotification(
|
||||||
|
ErrorCodes.InsufficientPermissions,
|
||||||
|
$"No permission to delete user {command.UserId}");
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -8,6 +8,9 @@ namespace CleanArchitecture.Domain.Tests.CommandHandler.User.DeleteUser;
|
|||||||
|
|
||||||
public sealed class DeleteUserCommandTestFixture : CommandHandlerFixtureBase
|
public sealed class DeleteUserCommandTestFixture : CommandHandlerFixtureBase
|
||||||
{
|
{
|
||||||
|
public DeleteUserCommandHandler CommandHandler { get; }
|
||||||
|
private IUserRepository UserRepository { get; }
|
||||||
|
|
||||||
public DeleteUserCommandTestFixture()
|
public DeleteUserCommandTestFixture()
|
||||||
{
|
{
|
||||||
UserRepository = Substitute.For<IUserRepository>();
|
UserRepository = Substitute.For<IUserRepository>();
|
||||||
@ -20,12 +23,10 @@ public sealed class DeleteUserCommandTestFixture : CommandHandlerFixtureBase
|
|||||||
User);
|
User);
|
||||||
}
|
}
|
||||||
|
|
||||||
public DeleteUserCommandHandler CommandHandler { get; }
|
|
||||||
private IUserRepository UserRepository { get; }
|
|
||||||
|
|
||||||
public Entities.User SetupUser()
|
public Entities.User SetupUser()
|
||||||
{
|
{
|
||||||
var user = new Entities.User(
|
var user = new Entities.User(
|
||||||
|
Guid.NewGuid(),
|
||||||
Guid.NewGuid(),
|
Guid.NewGuid(),
|
||||||
"max@mustermann.com",
|
"max@mustermann.com",
|
||||||
"Max",
|
"Max",
|
||||||
@ -39,4 +40,9 @@ public sealed class DeleteUserCommandTestFixture : CommandHandlerFixtureBase
|
|||||||
|
|
||||||
return user;
|
return user;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void SetupCurrentUser()
|
||||||
|
{
|
||||||
|
User.GetUserRole().Returns(UserRole.User);
|
||||||
|
}
|
||||||
}
|
}
|
@ -27,12 +27,12 @@ public sealed class DeleteUserCommandValidationTests :
|
|||||||
|
|
||||||
ShouldHaveSingleError(
|
ShouldHaveSingleError(
|
||||||
command,
|
command,
|
||||||
DomainErrorCodes.UserEmptyId,
|
DomainErrorCodes.User.UserEmptyId,
|
||||||
"User id may not be empty");
|
"User id may not be empty");
|
||||||
}
|
}
|
||||||
|
|
||||||
private static DeleteUserCommand CreateTestCommand(Guid? userId = null)
|
private static DeleteUserCommand CreateTestCommand(Guid? userId = null)
|
||||||
{
|
{
|
||||||
return new(userId ?? Guid.NewGuid());
|
return new DeleteUserCommand(userId ?? Guid.NewGuid());
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -57,7 +57,7 @@ public sealed class LoginUserCommandHandlerTests
|
|||||||
.VerifyAnyDomainNotification()
|
.VerifyAnyDomainNotification()
|
||||||
.VerifyExistingNotification(
|
.VerifyExistingNotification(
|
||||||
ErrorCodes.ObjectNotFound,
|
ErrorCodes.ObjectNotFound,
|
||||||
$"There is no User with Email {command.Email}");
|
$"There is no user with email {command.Email}");
|
||||||
|
|
||||||
token.Should().BeEmpty();
|
token.Should().BeEmpty();
|
||||||
}
|
}
|
||||||
@ -74,7 +74,7 @@ public sealed class LoginUserCommandHandlerTests
|
|||||||
_fixture
|
_fixture
|
||||||
.VerifyAnyDomainNotification()
|
.VerifyAnyDomainNotification()
|
||||||
.VerifyExistingNotification(
|
.VerifyExistingNotification(
|
||||||
DomainErrorCodes.UserPasswordIncorrect,
|
DomainErrorCodes.User.UserPasswordIncorrect,
|
||||||
"The password is incorrect");
|
"The password is incorrect");
|
||||||
|
|
||||||
token.Should().BeEmpty();
|
token.Should().BeEmpty();
|
||||||
|
@ -11,6 +11,10 @@ namespace CleanArchitecture.Domain.Tests.CommandHandler.User.LoginUser;
|
|||||||
|
|
||||||
public sealed class LoginUserCommandTestFixture : CommandHandlerFixtureBase
|
public sealed class LoginUserCommandTestFixture : CommandHandlerFixtureBase
|
||||||
{
|
{
|
||||||
|
public LoginUserCommandHandler CommandHandler { get; set; }
|
||||||
|
public IUserRepository UserRepository { get; set; }
|
||||||
|
public IOptions<TokenSettings> TokenSettings { get; set; }
|
||||||
|
|
||||||
public LoginUserCommandTestFixture()
|
public LoginUserCommandTestFixture()
|
||||||
{
|
{
|
||||||
UserRepository = Substitute.For<IUserRepository>();
|
UserRepository = Substitute.For<IUserRepository>();
|
||||||
@ -30,13 +34,10 @@ public sealed class LoginUserCommandTestFixture : CommandHandlerFixtureBase
|
|||||||
TokenSettings);
|
TokenSettings);
|
||||||
}
|
}
|
||||||
|
|
||||||
public LoginUserCommandHandler CommandHandler { get; set; }
|
|
||||||
public IUserRepository UserRepository { get; set; }
|
|
||||||
public IOptions<TokenSettings> TokenSettings { get; set; }
|
|
||||||
|
|
||||||
public Entities.User SetupUser()
|
public Entities.User SetupUser()
|
||||||
{
|
{
|
||||||
var user = new Entities.User(
|
var user = new Entities.User(
|
||||||
|
Guid.NewGuid(),
|
||||||
Guid.NewGuid(),
|
Guid.NewGuid(),
|
||||||
"max@mustermann.com",
|
"max@mustermann.com",
|
||||||
"Max",
|
"Max",
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using CleanArchitecture.Domain.Commands.Users.LoginUser;
|
using CleanArchitecture.Domain.Commands.Users.LoginUser;
|
||||||
|
using CleanArchitecture.Domain.Constants;
|
||||||
using CleanArchitecture.Domain.Errors;
|
using CleanArchitecture.Domain.Errors;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
||||||
@ -28,7 +29,7 @@ public sealed class LoginUserCommandValidationTests :
|
|||||||
|
|
||||||
ShouldHaveSingleError(
|
ShouldHaveSingleError(
|
||||||
command,
|
command,
|
||||||
DomainErrorCodes.UserInvalidEmail,
|
DomainErrorCodes.User.UserInvalidEmail,
|
||||||
"Email is not a valid email address");
|
"Email is not a valid email address");
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -39,19 +40,19 @@ public sealed class LoginUserCommandValidationTests :
|
|||||||
|
|
||||||
ShouldHaveSingleError(
|
ShouldHaveSingleError(
|
||||||
command,
|
command,
|
||||||
DomainErrorCodes.UserInvalidEmail,
|
DomainErrorCodes.User.UserInvalidEmail,
|
||||||
"Email is not a valid email address");
|
"Email is not a valid email address");
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void Should_Be_Invalid_For_Email_Exceeds_Max_Length()
|
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(
|
ShouldHaveSingleError(
|
||||||
command,
|
command,
|
||||||
DomainErrorCodes.UserEmailExceedsMaxLength,
|
DomainErrorCodes.User.UserEmailExceedsMaxLength,
|
||||||
"Email may not be longer than 320 characters");
|
$"Email may not be longer than {MaxLengths.User.Email} characters");
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
@ -61,12 +62,12 @@ public sealed class LoginUserCommandValidationTests :
|
|||||||
|
|
||||||
var errors = new List<string>
|
var errors = new List<string>
|
||||||
{
|
{
|
||||||
DomainErrorCodes.UserEmptyPassword,
|
DomainErrorCodes.User.UserEmptyPassword,
|
||||||
DomainErrorCodes.UserSpecialCharPassword,
|
DomainErrorCodes.User.UserSpecialCharPassword,
|
||||||
DomainErrorCodes.UserNumberPassword,
|
DomainErrorCodes.User.UserNumberPassword,
|
||||||
DomainErrorCodes.UserLowercaseLetterPassword,
|
DomainErrorCodes.User.UserLowercaseLetterPassword,
|
||||||
DomainErrorCodes.UserUppercaseLetterPassword,
|
DomainErrorCodes.User.UserUppercaseLetterPassword,
|
||||||
DomainErrorCodes.UserShortPassword
|
DomainErrorCodes.User.UserShortPassword
|
||||||
};
|
};
|
||||||
|
|
||||||
ShouldHaveExpectedErrors(command, errors.ToArray());
|
ShouldHaveExpectedErrors(command, errors.ToArray());
|
||||||
@ -77,7 +78,7 @@ public sealed class LoginUserCommandValidationTests :
|
|||||||
{
|
{
|
||||||
var command = CreateTestCommand(password: "z8tnayvd5FNLU9AQm");
|
var command = CreateTestCommand(password: "z8tnayvd5FNLU9AQm");
|
||||||
|
|
||||||
ShouldHaveSingleError(command, DomainErrorCodes.UserSpecialCharPassword);
|
ShouldHaveSingleError(command, DomainErrorCodes.User.UserSpecialCharPassword);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
@ -85,7 +86,7 @@ public sealed class LoginUserCommandValidationTests :
|
|||||||
{
|
{
|
||||||
var command = CreateTestCommand(password: "z]tnayvdFNLU:]AQm");
|
var command = CreateTestCommand(password: "z]tnayvdFNLU:]AQm");
|
||||||
|
|
||||||
ShouldHaveSingleError(command, DomainErrorCodes.UserNumberPassword);
|
ShouldHaveSingleError(command, DomainErrorCodes.User.UserNumberPassword);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
@ -93,7 +94,7 @@ public sealed class LoginUserCommandValidationTests :
|
|||||||
{
|
{
|
||||||
var command = CreateTestCommand(password: "Z8]TNAYVDFNLU:]AQM");
|
var command = CreateTestCommand(password: "Z8]TNAYVDFNLU:]AQM");
|
||||||
|
|
||||||
ShouldHaveSingleError(command, DomainErrorCodes.UserLowercaseLetterPassword);
|
ShouldHaveSingleError(command, DomainErrorCodes.User.UserLowercaseLetterPassword);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
@ -101,7 +102,7 @@ public sealed class LoginUserCommandValidationTests :
|
|||||||
{
|
{
|
||||||
var command = CreateTestCommand(password: "z8]tnayvd5fnlu9:]aqm");
|
var command = CreateTestCommand(password: "z8]tnayvd5fnlu9:]aqm");
|
||||||
|
|
||||||
ShouldHaveSingleError(command, DomainErrorCodes.UserUppercaseLetterPassword);
|
ShouldHaveSingleError(command, DomainErrorCodes.User.UserUppercaseLetterPassword);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
@ -109,7 +110,7 @@ public sealed class LoginUserCommandValidationTests :
|
|||||||
{
|
{
|
||||||
var command = CreateTestCommand(password: "zA6{");
|
var command = CreateTestCommand(password: "zA6{");
|
||||||
|
|
||||||
ShouldHaveSingleError(command, DomainErrorCodes.UserShortPassword);
|
ShouldHaveSingleError(command, DomainErrorCodes.User.UserShortPassword);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
@ -117,14 +118,14 @@ public sealed class LoginUserCommandValidationTests :
|
|||||||
{
|
{
|
||||||
var command = CreateTestCommand(password: string.Concat(Enumerable.Repeat("zA6{", 12), 12));
|
var command = CreateTestCommand(password: string.Concat(Enumerable.Repeat("zA6{", 12), 12));
|
||||||
|
|
||||||
ShouldHaveSingleError(command, DomainErrorCodes.UserLongPassword);
|
ShouldHaveSingleError(command, DomainErrorCodes.User.UserLongPassword);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static LoginUserCommand CreateTestCommand(
|
private static LoginUserCommand CreateTestCommand(
|
||||||
string? email = null,
|
string? email = null,
|
||||||
string? password = null)
|
string? password = null)
|
||||||
{
|
{
|
||||||
return new(
|
return new LoginUserCommand(
|
||||||
email ?? "test@email.com",
|
email ?? "test@email.com",
|
||||||
password ?? "Po=PF]PC6t.?8?ks)A6W");
|
password ?? "Po=PF]PC6t.?8?ks)A6W");
|
||||||
}
|
}
|
||||||
|
@ -23,14 +23,17 @@ public sealed class UpdateUserCommandHandlerTests
|
|||||||
"test@email.com",
|
"test@email.com",
|
||||||
"Test",
|
"Test",
|
||||||
"Email",
|
"Email",
|
||||||
UserRole.User);
|
UserRole.User,
|
||||||
|
Guid.NewGuid());
|
||||||
|
|
||||||
|
_fixture.SetupTenant(command.TenantId);
|
||||||
|
|
||||||
await _fixture.CommandHandler.Handle(command, default);
|
await _fixture.CommandHandler.Handle(command, default);
|
||||||
|
|
||||||
_fixture
|
_fixture
|
||||||
.VerifyNoDomainNotification()
|
.VerifyNoDomainNotification()
|
||||||
.VerifyCommit()
|
.VerifyCommit()
|
||||||
.VerifyRaisedEvent<UserUpdatedEvent>(x => x.UserId == command.UserId);
|
.VerifyRaisedEvent<UserUpdatedEvent>(x => x.AggregateId == command.UserId);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
@ -43,7 +46,10 @@ public sealed class UpdateUserCommandHandlerTests
|
|||||||
"test@email.com",
|
"test@email.com",
|
||||||
"Test",
|
"Test",
|
||||||
"Email",
|
"Email",
|
||||||
UserRole.User);
|
UserRole.User,
|
||||||
|
Guid.NewGuid());
|
||||||
|
|
||||||
|
_fixture.SetupTenant(command.TenantId);
|
||||||
|
|
||||||
await _fixture.CommandHandler.Handle(command, default);
|
await _fixture.CommandHandler.Handle(command, default);
|
||||||
|
|
||||||
@ -53,7 +59,7 @@ public sealed class UpdateUserCommandHandlerTests
|
|||||||
.VerifyAnyDomainNotification()
|
.VerifyAnyDomainNotification()
|
||||||
.VerifyExistingNotification(
|
.VerifyExistingNotification(
|
||||||
ErrorCodes.ObjectNotFound,
|
ErrorCodes.ObjectNotFound,
|
||||||
$"There is no User with Id {command.UserId}");
|
$"There is no user with Id {command.UserId}");
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
@ -66,11 +72,15 @@ public sealed class UpdateUserCommandHandlerTests
|
|||||||
"test@email.com",
|
"test@email.com",
|
||||||
"Test",
|
"Test",
|
||||||
"Email",
|
"Email",
|
||||||
UserRole.User);
|
UserRole.User,
|
||||||
|
Guid.NewGuid());
|
||||||
|
|
||||||
|
_fixture.SetupTenant(command.TenantId);
|
||||||
|
|
||||||
_fixture.UserRepository
|
_fixture.UserRepository
|
||||||
.GetByEmailAsync(command.Email)
|
.GetByEmailAsync(command.Email)
|
||||||
.Returns(new Entities.User(
|
.Returns(new Entities.User(
|
||||||
|
Guid.NewGuid(),
|
||||||
Guid.NewGuid(),
|
Guid.NewGuid(),
|
||||||
command.Email,
|
command.Email,
|
||||||
"Some",
|
"Some",
|
||||||
@ -85,7 +95,62 @@ public sealed class UpdateUserCommandHandlerTests
|
|||||||
.VerifyNoRaisedEvent<UserUpdatedEvent>()
|
.VerifyNoRaisedEvent<UserUpdatedEvent>()
|
||||||
.VerifyAnyDomainNotification()
|
.VerifyAnyDomainNotification()
|
||||||
.VerifyExistingNotification(
|
.VerifyExistingNotification(
|
||||||
DomainErrorCodes.UserAlreadyExists,
|
DomainErrorCodes.User.UserAlreadyExists,
|
||||||
$"There is already a User with Email {command.Email}");
|
$"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<UserUpdatedEvent>()
|
||||||
|
.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<Entities.User>(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<UserUpdatedEvent>(x => x.AggregateId == command.UserId);
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -8,24 +8,28 @@ namespace CleanArchitecture.Domain.Tests.CommandHandler.User.UpdateUser;
|
|||||||
|
|
||||||
public sealed class UpdateUserCommandTestFixture : CommandHandlerFixtureBase
|
public sealed class UpdateUserCommandTestFixture : CommandHandlerFixtureBase
|
||||||
{
|
{
|
||||||
|
public UpdateUserCommandHandler CommandHandler { get; }
|
||||||
|
public IUserRepository UserRepository { get; }
|
||||||
|
private ITenantRepository TenantRepository { get; }
|
||||||
|
|
||||||
public UpdateUserCommandTestFixture()
|
public UpdateUserCommandTestFixture()
|
||||||
{
|
{
|
||||||
UserRepository = Substitute.For<IUserRepository>();
|
UserRepository = Substitute.For<IUserRepository>();
|
||||||
|
TenantRepository = Substitute.For<ITenantRepository>();
|
||||||
|
|
||||||
CommandHandler = new UpdateUserCommandHandler(
|
CommandHandler = new UpdateUserCommandHandler(
|
||||||
Bus,
|
Bus,
|
||||||
UnitOfWork,
|
UnitOfWork,
|
||||||
NotificationHandler,
|
NotificationHandler,
|
||||||
UserRepository,
|
UserRepository,
|
||||||
User);
|
User,
|
||||||
|
TenantRepository);
|
||||||
}
|
}
|
||||||
|
|
||||||
public UpdateUserCommandHandler CommandHandler { get; }
|
|
||||||
public IUserRepository UserRepository { get; }
|
|
||||||
|
|
||||||
public Entities.User SetupUser()
|
public Entities.User SetupUser()
|
||||||
{
|
{
|
||||||
var user = new Entities.User(
|
var user = new Entities.User(
|
||||||
|
Guid.NewGuid(),
|
||||||
Guid.NewGuid(),
|
Guid.NewGuid(),
|
||||||
"max@mustermann.com",
|
"max@mustermann.com",
|
||||||
"Max",
|
"Max",
|
||||||
@ -39,4 +43,21 @@ public sealed class UpdateUserCommandTestFixture : CommandHandlerFixtureBase
|
|||||||
|
|
||||||
return user;
|
return user;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Entities.Tenant SetupTenant(Guid tenantId)
|
||||||
|
{
|
||||||
|
var tenant = new Entities.Tenant(tenantId, "Name");
|
||||||
|
|
||||||
|
TenantRepository
|
||||||
|
.ExistsAsync(Arg.Is<Guid>(y => y == tenant.Id))
|
||||||
|
.Returns(true);
|
||||||
|
|
||||||
|
return tenant;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetupCurrentUser(Guid userId)
|
||||||
|
{
|
||||||
|
User.GetUserId().Returns(userId);
|
||||||
|
User.GetUserRole().Returns(UserRole.User);
|
||||||
|
}
|
||||||
}
|
}
|
@ -1,5 +1,6 @@
|
|||||||
using System;
|
using System;
|
||||||
using CleanArchitecture.Domain.Commands.Users.UpdateUser;
|
using CleanArchitecture.Domain.Commands.Users.UpdateUser;
|
||||||
|
using CleanArchitecture.Domain.Constants;
|
||||||
using CleanArchitecture.Domain.Enums;
|
using CleanArchitecture.Domain.Enums;
|
||||||
using CleanArchitecture.Domain.Errors;
|
using CleanArchitecture.Domain.Errors;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
@ -28,7 +29,7 @@ public sealed class UpdateUserCommandValidationTests :
|
|||||||
|
|
||||||
ShouldHaveSingleError(
|
ShouldHaveSingleError(
|
||||||
command,
|
command,
|
||||||
DomainErrorCodes.UserEmptyId,
|
DomainErrorCodes.User.UserEmptyId,
|
||||||
"User id may not be empty");
|
"User id may not be empty");
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -39,7 +40,7 @@ public sealed class UpdateUserCommandValidationTests :
|
|||||||
|
|
||||||
ShouldHaveSingleError(
|
ShouldHaveSingleError(
|
||||||
command,
|
command,
|
||||||
DomainErrorCodes.UserInvalidEmail,
|
DomainErrorCodes.User.UserInvalidEmail,
|
||||||
"Email is not a valid email address");
|
"Email is not a valid email address");
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -50,19 +51,19 @@ public sealed class UpdateUserCommandValidationTests :
|
|||||||
|
|
||||||
ShouldHaveSingleError(
|
ShouldHaveSingleError(
|
||||||
command,
|
command,
|
||||||
DomainErrorCodes.UserInvalidEmail,
|
DomainErrorCodes.User.UserInvalidEmail,
|
||||||
"Email is not a valid email address");
|
"Email is not a valid email address");
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void Should_Be_Invalid_For_Email_Exceeds_Max_Length()
|
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(
|
ShouldHaveSingleError(
|
||||||
command,
|
command,
|
||||||
DomainErrorCodes.UserEmailExceedsMaxLength,
|
DomainErrorCodes.User.UserEmailExceedsMaxLength,
|
||||||
"Email may not be longer than 320 characters");
|
$"Email may not be longer than {MaxLengths.User.Email} characters");
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
@ -72,19 +73,19 @@ public sealed class UpdateUserCommandValidationTests :
|
|||||||
|
|
||||||
ShouldHaveSingleError(
|
ShouldHaveSingleError(
|
||||||
command,
|
command,
|
||||||
DomainErrorCodes.UserEmptyFirstName,
|
DomainErrorCodes.User.UserEmptyFirstName,
|
||||||
"FirstName may not be empty");
|
"FirstName may not be empty");
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void Should_Be_Invalid_For_First_Name_Exceeds_Max_Length()
|
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(
|
ShouldHaveSingleError(
|
||||||
command,
|
command,
|
||||||
DomainErrorCodes.UserFirstNameExceedsMaxLength,
|
DomainErrorCodes.User.UserFirstNameExceedsMaxLength,
|
||||||
"FirstName may not be longer than 100 characters");
|
$"FirstName may not be longer than {MaxLengths.User.FirstName} characters");
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
@ -94,33 +95,46 @@ public sealed class UpdateUserCommandValidationTests :
|
|||||||
|
|
||||||
ShouldHaveSingleError(
|
ShouldHaveSingleError(
|
||||||
command,
|
command,
|
||||||
DomainErrorCodes.UserEmptyLastName,
|
DomainErrorCodes.User.UserEmptyLastName,
|
||||||
"LastName may not be empty");
|
"LastName may not be empty");
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void Should_Be_Invalid_For_Last_Name_Exceeds_Max_Length()
|
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(
|
ShouldHaveSingleError(
|
||||||
command,
|
command,
|
||||||
DomainErrorCodes.UserLastNameExceedsMaxLength,
|
DomainErrorCodes.User.UserLastNameExceedsMaxLength,
|
||||||
"LastName may not be longer than 100 characters");
|
$"LastName may not be longer than {MaxLengths.User.LastName} characters");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Should_Be_Invalid_For_Empty_Tenant_Id()
|
||||||
|
{
|
||||||
|
var command = CreateTestCommand(tenantId: Guid.Empty);
|
||||||
|
|
||||||
|
ShouldHaveSingleError(
|
||||||
|
command,
|
||||||
|
DomainErrorCodes.Tenant.TenantEmptyId,
|
||||||
|
"Tenant id may not be empty");
|
||||||
}
|
}
|
||||||
|
|
||||||
private static UpdateUserCommand CreateTestCommand(
|
private static UpdateUserCommand CreateTestCommand(
|
||||||
Guid? userId = null,
|
Guid? userId = null,
|
||||||
|
Guid? tenantId = null,
|
||||||
string? email = null,
|
string? email = null,
|
||||||
string? firstName = null,
|
string? firstName = null,
|
||||||
string? lastName = null,
|
string? lastName = null,
|
||||||
UserRole? role = null)
|
UserRole? role = null)
|
||||||
{
|
{
|
||||||
return new(
|
return new UpdateUserCommand(
|
||||||
userId ?? Guid.NewGuid(),
|
userId ?? Guid.NewGuid(),
|
||||||
email ?? "test@email.com",
|
email ?? "test@email.com",
|
||||||
firstName ?? "test",
|
firstName ?? "test",
|
||||||
lastName ?? "email",
|
lastName ?? "email",
|
||||||
role ?? UserRole.User);
|
role ?? UserRole.User,
|
||||||
|
tenantId ?? Guid.NewGuid());
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -10,6 +10,11 @@ namespace CleanArchitecture.Domain.Tests;
|
|||||||
|
|
||||||
public class CommandHandlerFixtureBase
|
public class CommandHandlerFixtureBase
|
||||||
{
|
{
|
||||||
|
protected IMediatorHandler Bus { get; }
|
||||||
|
protected IUnitOfWork UnitOfWork { get; }
|
||||||
|
protected DomainNotificationHandler NotificationHandler { get; }
|
||||||
|
protected IUser User { get; }
|
||||||
|
|
||||||
protected CommandHandlerFixtureBase()
|
protected CommandHandlerFixtureBase()
|
||||||
{
|
{
|
||||||
Bus = Substitute.For<IMediatorHandler>();
|
Bus = Substitute.For<IMediatorHandler>();
|
||||||
@ -23,11 +28,6 @@ public class CommandHandlerFixtureBase
|
|||||||
UnitOfWork.CommitAsync().Returns(true);
|
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)
|
public CommandHandlerFixtureBase VerifyExistingNotification(string errorCode, string message)
|
||||||
{
|
{
|
||||||
Bus.Received(1).RaiseEventAsync(
|
Bus.Received(1).RaiseEventAsync(
|
||||||
|
@ -55,21 +55,24 @@ public sealed class ApiUser : IUser
|
|||||||
{
|
{
|
||||||
get
|
get
|
||||||
{
|
{
|
||||||
if (_name != null)
|
if (_name is not null)
|
||||||
{
|
{
|
||||||
return _name;
|
return _name;
|
||||||
}
|
}
|
||||||
|
|
||||||
var identity = _httpContextAccessor.HttpContext?.User.Identity;
|
var identity = _httpContextAccessor.HttpContext?.User.Identity;
|
||||||
if (identity == null)
|
if (identity is null)
|
||||||
{
|
{
|
||||||
_name = string.Empty;
|
_name = string.Empty;
|
||||||
return string.Empty;
|
return string.Empty;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(identity.Name))
|
if (!string.IsNullOrWhiteSpace(identity.Name))
|
||||||
{
|
{
|
||||||
_name = identity.Name;
|
_name = identity.Name;
|
||||||
return identity.Name;
|
return identity.Name;
|
||||||
}
|
}
|
||||||
|
|
||||||
var claim = _httpContextAccessor.HttpContext!.User.Claims
|
var claim = _httpContextAccessor.HttpContext!.User.Claims
|
||||||
.FirstOrDefault(c => string.Equals(c.Type, ClaimTypes.Name, StringComparison.OrdinalIgnoreCase))?
|
.FirstOrDefault(c => string.Equals(c.Type, ClaimTypes.Name, StringComparison.OrdinalIgnoreCase))?
|
||||||
.Value;
|
.Value;
|
||||||
|
@ -6,6 +6,11 @@ namespace CleanArchitecture.Domain.Commands;
|
|||||||
|
|
||||||
public abstract class CommandBase : IRequest
|
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)
|
protected CommandBase(Guid aggregateId)
|
||||||
{
|
{
|
||||||
MessageType = GetType().Name;
|
MessageType = GetType().Name;
|
||||||
@ -13,10 +18,5 @@ public abstract class CommandBase : IRequest
|
|||||||
AggregateId = aggregateId;
|
AggregateId = aggregateId;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Guid AggregateId { get; }
|
|
||||||
public string MessageType { get; }
|
|
||||||
public DateTime Timestamp { get; }
|
|
||||||
public ValidationResult? ValidationResult { get; protected set; }
|
|
||||||
|
|
||||||
public abstract bool IsValid();
|
public abstract bool IsValid();
|
||||||
}
|
}
|
@ -9,9 +9,9 @@ namespace CleanArchitecture.Domain.Commands;
|
|||||||
|
|
||||||
public abstract class CommandHandlerBase
|
public abstract class CommandHandlerBase
|
||||||
{
|
{
|
||||||
protected readonly IMediatorHandler Bus;
|
|
||||||
private readonly DomainNotificationHandler _notifications;
|
private readonly DomainNotificationHandler _notifications;
|
||||||
private readonly IUnitOfWork _unitOfWork;
|
private readonly IUnitOfWork _unitOfWork;
|
||||||
|
protected readonly IMediatorHandler Bus;
|
||||||
|
|
||||||
protected CommandHandlerBase(
|
protected CommandHandlerBase(
|
||||||
IMediatorHandler bus,
|
IMediatorHandler bus,
|
||||||
@ -62,7 +62,7 @@ public abstract class CommandHandlerBase
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (command.ValidationResult == null)
|
if (command.ValidationResult is null)
|
||||||
{
|
{
|
||||||
throw new InvalidOperationException("Command is invalid and should therefore have a validation result");
|
throw new InvalidOperationException("Command is invalid and should therefore have a validation result");
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
@ -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<CreateTenantCommand>
|
||||||
|
{
|
||||||
|
private readonly ITenantRepository _tenantRepository;
|
||||||
|
private readonly IUser _user;
|
||||||
|
|
||||||
|
public CreateTenantCommandHandler(
|
||||||
|
IMediatorHandler bus,
|
||||||
|
IUnitOfWork unitOfWork,
|
||||||
|
INotificationHandler<DomainNotification> 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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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<CreateTenantCommand>
|
||||||
|
{
|
||||||
|
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");
|
||||||
|
}
|
||||||
|
}
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
@ -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<DeleteTenantCommand>
|
||||||
|
{
|
||||||
|
private readonly ITenantRepository _tenantRepository;
|
||||||
|
private readonly IUser _user;
|
||||||
|
private readonly IUserRepository _userRepository;
|
||||||
|
|
||||||
|
public DeleteTenantCommandHandler(
|
||||||
|
IMediatorHandler bus,
|
||||||
|
IUnitOfWork unitOfWork,
|
||||||
|
INotificationHandler<DomainNotification> 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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,20 @@
|
|||||||
|
using CleanArchitecture.Domain.Errors;
|
||||||
|
using FluentValidation;
|
||||||
|
|
||||||
|
namespace CleanArchitecture.Domain.Commands.Tenants.DeleteTenant;
|
||||||
|
|
||||||
|
public sealed class DeleteTenantCommandValidation : AbstractValidator<DeleteTenantCommand>
|
||||||
|
{
|
||||||
|
public DeleteTenantCommandValidation()
|
||||||
|
{
|
||||||
|
AddRuleForId();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void AddRuleForId()
|
||||||
|
{
|
||||||
|
RuleFor(cmd => cmd.AggregateId)
|
||||||
|
.NotEmpty()
|
||||||
|
.WithErrorCode(DomainErrorCodes.Tenant.TenantEmptyId)
|
||||||
|
.WithMessage("Tenant id may not be empty");
|
||||||
|
}
|
||||||
|
}
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
@ -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<UpdateTenantCommand>
|
||||||
|
{
|
||||||
|
private readonly ITenantRepository _tenantRepository;
|
||||||
|
private readonly IUser _user;
|
||||||
|
|
||||||
|
public UpdateTenantCommandHandler(
|
||||||
|
IMediatorHandler bus,
|
||||||
|
IUnitOfWork unitOfWork,
|
||||||
|
INotificationHandler<DomainNotification> 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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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<UpdateTenantCommand>
|
||||||
|
{
|
||||||
|
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");
|
||||||
|
}
|
||||||
|
}
|
@ -4,7 +4,10 @@ namespace CleanArchitecture.Domain.Commands.Users.ChangePassword;
|
|||||||
|
|
||||||
public sealed class ChangePasswordCommand : CommandBase
|
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())
|
public ChangePasswordCommand(string password, string newPassword) : base(Guid.NewGuid())
|
||||||
{
|
{
|
||||||
@ -12,12 +15,9 @@ public sealed class ChangePasswordCommand : CommandBase
|
|||||||
NewPassword = newPassword;
|
NewPassword = newPassword;
|
||||||
}
|
}
|
||||||
|
|
||||||
public string Password { get; }
|
|
||||||
public string NewPassword { get; }
|
|
||||||
|
|
||||||
public override bool IsValid()
|
public override bool IsValid()
|
||||||
{
|
{
|
||||||
ValidationResult = _validation.Validate(this);
|
ValidationResult = s_validation.Validate(this);
|
||||||
return ValidationResult.IsValid;
|
return ValidationResult.IsValid;
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -36,12 +36,12 @@ public sealed class ChangePasswordCommandHandler : CommandHandlerBase,
|
|||||||
|
|
||||||
var user = await _userRepository.GetByIdAsync(_user.GetUserId());
|
var user = await _userRepository.GetByIdAsync(_user.GetUserId());
|
||||||
|
|
||||||
if (user == null)
|
if (user is null)
|
||||||
{
|
{
|
||||||
await NotifyAsync(
|
await NotifyAsync(
|
||||||
new DomainNotification(
|
new DomainNotification(
|
||||||
request.MessageType,
|
request.MessageType,
|
||||||
$"There is no User with Id {_user.GetUserId()}",
|
$"There is no user with Id {_user.GetUserId()}",
|
||||||
ErrorCodes.ObjectNotFound));
|
ErrorCodes.ObjectNotFound));
|
||||||
|
|
||||||
return;
|
return;
|
||||||
@ -53,7 +53,7 @@ public sealed class ChangePasswordCommandHandler : CommandHandlerBase,
|
|||||||
new DomainNotification(
|
new DomainNotification(
|
||||||
request.MessageType,
|
request.MessageType,
|
||||||
"The password is incorrect",
|
"The password is incorrect",
|
||||||
DomainErrorCodes.UserPasswordIncorrect));
|
DomainErrorCodes.User.UserPasswordIncorrect));
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -4,31 +4,34 @@ namespace CleanArchitecture.Domain.Commands.Users.CreateUser;
|
|||||||
|
|
||||||
public sealed class CreateUserCommand : CommandBase
|
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(
|
public CreateUserCommand(
|
||||||
Guid userId,
|
Guid userId,
|
||||||
|
Guid tenantId,
|
||||||
string email,
|
string email,
|
||||||
string firstName,
|
string firstName,
|
||||||
string lastName,
|
string lastName,
|
||||||
string password) : base(userId)
|
string password) : base(userId)
|
||||||
{
|
{
|
||||||
UserId = userId;
|
UserId = userId;
|
||||||
|
TenantId = tenantId;
|
||||||
Email = email;
|
Email = email;
|
||||||
FirstName = firstName;
|
FirstName = firstName;
|
||||||
LastName = lastName;
|
LastName = lastName;
|
||||||
Password = password;
|
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()
|
public override bool IsValid()
|
||||||
{
|
{
|
||||||
ValidationResult = _validation.Validate(this);
|
ValidationResult = s_validation.Validate(this);
|
||||||
return ValidationResult.IsValid;
|
return ValidationResult.IsValid;
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -15,15 +15,21 @@ namespace CleanArchitecture.Domain.Commands.Users.CreateUser;
|
|||||||
public sealed class CreateUserCommandHandler : CommandHandlerBase,
|
public sealed class CreateUserCommandHandler : CommandHandlerBase,
|
||||||
IRequestHandler<CreateUserCommand>
|
IRequestHandler<CreateUserCommand>
|
||||||
{
|
{
|
||||||
|
private readonly ITenantRepository _tenantRepository;
|
||||||
|
private readonly IUser _user;
|
||||||
private readonly IUserRepository _userRepository;
|
private readonly IUserRepository _userRepository;
|
||||||
|
|
||||||
public CreateUserCommandHandler(
|
public CreateUserCommandHandler(
|
||||||
IMediatorHandler bus,
|
IMediatorHandler bus,
|
||||||
IUnitOfWork unitOfWork,
|
IUnitOfWork unitOfWork,
|
||||||
INotificationHandler<DomainNotification> notifications,
|
INotificationHandler<DomainNotification> notifications,
|
||||||
IUserRepository userRepository) : base(bus, unitOfWork, notifications)
|
IUserRepository userRepository,
|
||||||
|
ITenantRepository tenantRepository,
|
||||||
|
IUser user) : base(bus, unitOfWork, notifications)
|
||||||
{
|
{
|
||||||
_userRepository = userRepository;
|
_userRepository = userRepository;
|
||||||
|
_tenantRepository = tenantRepository;
|
||||||
|
_user = user;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task Handle(CreateUserCommand request, CancellationToken cancellationToken)
|
public async Task Handle(CreateUserCommand request, CancellationToken cancellationToken)
|
||||||
@ -33,27 +39,49 @@ public sealed class CreateUserCommandHandler : CommandHandlerBase,
|
|||||||
return;
|
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(
|
new DomainNotification(
|
||||||
request.MessageType,
|
request.MessageType,
|
||||||
$"There is already a User with Id {request.UserId}",
|
"You are not allowed to create users",
|
||||||
DomainErrorCodes.UserAlreadyExists));
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
existingUser = await _userRepository.GetByEmailAsync(request.Email);
|
existingUser = await _userRepository.GetByEmailAsync(request.Email);
|
||||||
|
|
||||||
if (existingUser != null)
|
if (existingUser is not null)
|
||||||
{
|
{
|
||||||
await Bus.RaiseEventAsync(
|
await NotifyAsync(
|
||||||
new DomainNotification(
|
new DomainNotification(
|
||||||
request.MessageType,
|
request.MessageType,
|
||||||
$"There is already a User with Email {request.Email}",
|
$"There is already a user with email {request.Email}",
|
||||||
DomainErrorCodes.UserAlreadyExists));
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -61,6 +89,7 @@ public sealed class CreateUserCommandHandler : CommandHandlerBase,
|
|||||||
|
|
||||||
var user = new User(
|
var user = new User(
|
||||||
request.UserId,
|
request.UserId,
|
||||||
|
request.TenantId,
|
||||||
request.Email,
|
request.Email,
|
||||||
request.FirstName,
|
request.FirstName,
|
||||||
request.LastName,
|
request.LastName,
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
using CleanArchitecture.Domain.Constants;
|
||||||
using CleanArchitecture.Domain.Errors;
|
using CleanArchitecture.Domain.Errors;
|
||||||
using CleanArchitecture.Domain.Extensions.Validation;
|
using CleanArchitecture.Domain.Extensions.Validation;
|
||||||
using FluentValidation;
|
using FluentValidation;
|
||||||
@ -9,6 +10,7 @@ public sealed class CreateUserCommandValidation : AbstractValidator<CreateUserCo
|
|||||||
public CreateUserCommandValidation()
|
public CreateUserCommandValidation()
|
||||||
{
|
{
|
||||||
AddRuleForId();
|
AddRuleForId();
|
||||||
|
AddRuleForTenantId();
|
||||||
AddRuleForEmail();
|
AddRuleForEmail();
|
||||||
AddRuleForFirstName();
|
AddRuleForFirstName();
|
||||||
AddRuleForLastName();
|
AddRuleForLastName();
|
||||||
@ -19,41 +21,49 @@ public sealed class CreateUserCommandValidation : AbstractValidator<CreateUserCo
|
|||||||
{
|
{
|
||||||
RuleFor(cmd => cmd.UserId)
|
RuleFor(cmd => cmd.UserId)
|
||||||
.NotEmpty()
|
.NotEmpty()
|
||||||
.WithErrorCode(DomainErrorCodes.UserEmptyId)
|
.WithErrorCode(DomainErrorCodes.User.UserEmptyId)
|
||||||
.WithMessage("User id may not be empty");
|
.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()
|
private void AddRuleForEmail()
|
||||||
{
|
{
|
||||||
RuleFor(cmd => cmd.Email)
|
RuleFor(cmd => cmd.Email)
|
||||||
.EmailAddress()
|
.EmailAddress()
|
||||||
.WithErrorCode(DomainErrorCodes.UserInvalidEmail)
|
.WithErrorCode(DomainErrorCodes.User.UserInvalidEmail)
|
||||||
.WithMessage("Email is not a valid email address")
|
.WithMessage("Email is not a valid email address")
|
||||||
.MaximumLength(320)
|
.MaximumLength(MaxLengths.User.Email)
|
||||||
.WithErrorCode(DomainErrorCodes.UserEmailExceedsMaxLength)
|
.WithErrorCode(DomainErrorCodes.User.UserEmailExceedsMaxLength)
|
||||||
.WithMessage("Email may not be longer than 320 characters");
|
.WithMessage($"Email may not be longer than {MaxLengths.User.Email} characters");
|
||||||
}
|
}
|
||||||
|
|
||||||
private void AddRuleForFirstName()
|
private void AddRuleForFirstName()
|
||||||
{
|
{
|
||||||
RuleFor(cmd => cmd.FirstName)
|
RuleFor(cmd => cmd.FirstName)
|
||||||
.NotEmpty()
|
.NotEmpty()
|
||||||
.WithErrorCode(DomainErrorCodes.UserEmptyFirstName)
|
.WithErrorCode(DomainErrorCodes.User.UserEmptyFirstName)
|
||||||
.WithMessage("FirstName may not be empty")
|
.WithMessage("FirstName may not be empty")
|
||||||
.MaximumLength(100)
|
.MaximumLength(MaxLengths.User.FirstName)
|
||||||
.WithErrorCode(DomainErrorCodes.UserFirstNameExceedsMaxLength)
|
.WithErrorCode(DomainErrorCodes.User.UserFirstNameExceedsMaxLength)
|
||||||
.WithMessage("FirstName may not be longer than 100 characters");
|
.WithMessage($"FirstName may not be longer than {MaxLengths.User.FirstName} characters");
|
||||||
}
|
}
|
||||||
|
|
||||||
private void AddRuleForLastName()
|
private void AddRuleForLastName()
|
||||||
{
|
{
|
||||||
RuleFor(cmd => cmd.LastName)
|
RuleFor(cmd => cmd.LastName)
|
||||||
.NotEmpty()
|
.NotEmpty()
|
||||||
.WithErrorCode(DomainErrorCodes.UserEmptyLastName)
|
.WithErrorCode(DomainErrorCodes.User.UserEmptyLastName)
|
||||||
.WithMessage("LastName may not be empty")
|
.WithMessage("LastName may not be empty")
|
||||||
.MaximumLength(100)
|
.MaximumLength(MaxLengths.User.LastName)
|
||||||
.WithErrorCode(DomainErrorCodes.UserLastNameExceedsMaxLength)
|
.WithErrorCode(DomainErrorCodes.User.UserLastNameExceedsMaxLength)
|
||||||
.WithMessage("LastName may not be longer than 100 characters");
|
.WithMessage($"LastName may not be longer than {MaxLengths.User.LastName} characters");
|
||||||
}
|
}
|
||||||
|
|
||||||
private void AddRuleForPassword()
|
private void AddRuleForPassword()
|
||||||
|
@ -4,18 +4,18 @@ namespace CleanArchitecture.Domain.Commands.Users.DeleteUser;
|
|||||||
|
|
||||||
public sealed class DeleteUserCommand : CommandBase
|
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)
|
public DeleteUserCommand(Guid userId) : base(userId)
|
||||||
{
|
{
|
||||||
UserId = userId;
|
UserId = userId;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Guid UserId { get; }
|
|
||||||
|
|
||||||
public override bool IsValid()
|
public override bool IsValid()
|
||||||
{
|
{
|
||||||
ValidationResult = _validation.Validate(this);
|
ValidationResult = s_validation.Validate(this);
|
||||||
return ValidationResult.IsValid;
|
return ValidationResult.IsValid;
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -36,12 +36,12 @@ public sealed class DeleteUserCommandHandler : CommandHandlerBase,
|
|||||||
|
|
||||||
var user = await _userRepository.GetByIdAsync(request.UserId);
|
var user = await _userRepository.GetByIdAsync(request.UserId);
|
||||||
|
|
||||||
if (user == null)
|
if (user is null)
|
||||||
{
|
{
|
||||||
await NotifyAsync(
|
await NotifyAsync(
|
||||||
new DomainNotification(
|
new DomainNotification(
|
||||||
request.MessageType,
|
request.MessageType,
|
||||||
$"There is no User with Id {request.UserId}",
|
$"There is no user with Id {request.UserId}",
|
||||||
ErrorCodes.ObjectNotFound));
|
ErrorCodes.ObjectNotFound));
|
||||||
|
|
||||||
return;
|
return;
|
||||||
|
@ -14,7 +14,7 @@ public sealed class DeleteUserCommandValidation : AbstractValidator<DeleteUserCo
|
|||||||
{
|
{
|
||||||
RuleFor(cmd => cmd.UserId)
|
RuleFor(cmd => cmd.UserId)
|
||||||
.NotEmpty()
|
.NotEmpty()
|
||||||
.WithErrorCode(DomainErrorCodes.UserEmptyId)
|
.WithErrorCode(DomainErrorCodes.User.UserEmptyId)
|
||||||
.WithMessage("User id may not be empty");
|
.WithMessage("User id may not be empty");
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -6,7 +6,10 @@ namespace CleanArchitecture.Domain.Commands.Users.LoginUser;
|
|||||||
public sealed class LoginUserCommand : CommandBase,
|
public sealed class LoginUserCommand : CommandBase,
|
||||||
IRequest<string>
|
IRequest<string>
|
||||||
{
|
{
|
||||||
private readonly LoginUserCommandValidation _validation = new();
|
private static readonly LoginUserCommandValidation s_validation = new();
|
||||||
|
|
||||||
|
public string Email { get; set; }
|
||||||
|
public string Password { get; set; }
|
||||||
|
|
||||||
|
|
||||||
public LoginUserCommand(
|
public LoginUserCommand(
|
||||||
@ -17,12 +20,9 @@ public sealed class LoginUserCommand : CommandBase,
|
|||||||
Password = password;
|
Password = password;
|
||||||
}
|
}
|
||||||
|
|
||||||
public string Email { get; set; }
|
|
||||||
public string Password { get; set; }
|
|
||||||
|
|
||||||
public override bool IsValid()
|
public override bool IsValid()
|
||||||
{
|
{
|
||||||
ValidationResult = _validation.Validate(this);
|
ValidationResult = s_validation.Validate(this);
|
||||||
return ValidationResult.IsValid;
|
return ValidationResult.IsValid;
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -45,12 +45,12 @@ public sealed class LoginUserCommandHandler : CommandHandlerBase,
|
|||||||
|
|
||||||
var user = await _userRepository.GetByEmailAsync(request.Email);
|
var user = await _userRepository.GetByEmailAsync(request.Email);
|
||||||
|
|
||||||
if (user == null)
|
if (user is null)
|
||||||
{
|
{
|
||||||
await NotifyAsync(
|
await NotifyAsync(
|
||||||
new DomainNotification(
|
new DomainNotification(
|
||||||
request.MessageType,
|
request.MessageType,
|
||||||
$"There is no User with Email {request.Email}",
|
$"There is no user with email {request.Email}",
|
||||||
ErrorCodes.ObjectNotFound));
|
ErrorCodes.ObjectNotFound));
|
||||||
|
|
||||||
return "";
|
return "";
|
||||||
@ -64,7 +64,7 @@ public sealed class LoginUserCommandHandler : CommandHandlerBase,
|
|||||||
new DomainNotification(
|
new DomainNotification(
|
||||||
request.MessageType,
|
request.MessageType,
|
||||||
"The password is incorrect",
|
"The password is incorrect",
|
||||||
DomainErrorCodes.UserPasswordIncorrect));
|
DomainErrorCodes.User.UserPasswordIncorrect));
|
||||||
|
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
using CleanArchitecture.Domain.Errors;
|
using CleanArchitecture.Domain.Constants;
|
||||||
|
using CleanArchitecture.Domain.Errors;
|
||||||
using CleanArchitecture.Domain.Extensions.Validation;
|
using CleanArchitecture.Domain.Extensions.Validation;
|
||||||
using FluentValidation;
|
using FluentValidation;
|
||||||
|
|
||||||
@ -16,11 +17,11 @@ public sealed class LoginUserCommandValidation : AbstractValidator<LoginUserComm
|
|||||||
{
|
{
|
||||||
RuleFor(cmd => cmd.Email)
|
RuleFor(cmd => cmd.Email)
|
||||||
.EmailAddress()
|
.EmailAddress()
|
||||||
.WithErrorCode(DomainErrorCodes.UserInvalidEmail)
|
.WithErrorCode(DomainErrorCodes.User.UserInvalidEmail)
|
||||||
.WithMessage("Email is not a valid email address")
|
.WithMessage("Email is not a valid email address")
|
||||||
.MaximumLength(320)
|
.MaximumLength(MaxLengths.User.Email)
|
||||||
.WithErrorCode(DomainErrorCodes.UserEmailExceedsMaxLength)
|
.WithErrorCode(DomainErrorCodes.User.UserEmailExceedsMaxLength)
|
||||||
.WithMessage("Email may not be longer than 320 characters");
|
.WithMessage($"Email may not be longer than {MaxLengths.User.Email} characters");
|
||||||
}
|
}
|
||||||
|
|
||||||
private void AddRuleForPassword()
|
private void AddRuleForPassword()
|
||||||
|
@ -5,31 +5,33 @@ namespace CleanArchitecture.Domain.Commands.Users.UpdateUser;
|
|||||||
|
|
||||||
public sealed class UpdateUserCommand : CommandBase
|
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(
|
public UpdateUserCommand(
|
||||||
Guid userId,
|
Guid userId,
|
||||||
string email,
|
string email,
|
||||||
string firstName,
|
string firstName,
|
||||||
string lastName,
|
string lastName,
|
||||||
UserRole role) : base(userId)
|
UserRole role, Guid tenantId) : base(userId)
|
||||||
{
|
{
|
||||||
UserId = userId;
|
UserId = userId;
|
||||||
Email = email;
|
Email = email;
|
||||||
FirstName = firstName;
|
FirstName = firstName;
|
||||||
LastName = lastName;
|
LastName = lastName;
|
||||||
Role = role;
|
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()
|
public override bool IsValid()
|
||||||
{
|
{
|
||||||
ValidationResult = _validation.Validate(this);
|
ValidationResult = s_validation.Validate(this);
|
||||||
return ValidationResult.IsValid;
|
return ValidationResult.IsValid;
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -13,6 +13,7 @@ namespace CleanArchitecture.Domain.Commands.Users.UpdateUser;
|
|||||||
public sealed class UpdateUserCommandHandler : CommandHandlerBase,
|
public sealed class UpdateUserCommandHandler : CommandHandlerBase,
|
||||||
IRequestHandler<UpdateUserCommand>
|
IRequestHandler<UpdateUserCommand>
|
||||||
{
|
{
|
||||||
|
private readonly ITenantRepository _tenantRepository;
|
||||||
private readonly IUser _user;
|
private readonly IUser _user;
|
||||||
private readonly IUserRepository _userRepository;
|
private readonly IUserRepository _userRepository;
|
||||||
|
|
||||||
@ -21,10 +22,12 @@ public sealed class UpdateUserCommandHandler : CommandHandlerBase,
|
|||||||
IUnitOfWork unitOfWork,
|
IUnitOfWork unitOfWork,
|
||||||
INotificationHandler<DomainNotification> notifications,
|
INotificationHandler<DomainNotification> notifications,
|
||||||
IUserRepository userRepository,
|
IUserRepository userRepository,
|
||||||
IUser user) : base(bus, unitOfWork, notifications)
|
IUser user,
|
||||||
|
ITenantRepository tenantRepository) : base(bus, unitOfWork, notifications)
|
||||||
{
|
{
|
||||||
_userRepository = userRepository;
|
_userRepository = userRepository;
|
||||||
_user = user;
|
_user = user;
|
||||||
|
_tenantRepository = tenantRepository;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task Handle(UpdateUserCommand request, CancellationToken cancellationToken)
|
public async Task Handle(UpdateUserCommand request, CancellationToken cancellationToken)
|
||||||
@ -36,12 +39,12 @@ public sealed class UpdateUserCommandHandler : CommandHandlerBase,
|
|||||||
|
|
||||||
var user = await _userRepository.GetByIdAsync(request.UserId);
|
var user = await _userRepository.GetByIdAsync(request.UserId);
|
||||||
|
|
||||||
if (user == null)
|
if (user is null)
|
||||||
{
|
{
|
||||||
await Bus.RaiseEventAsync(
|
await NotifyAsync(
|
||||||
new DomainNotification(
|
new DomainNotification(
|
||||||
request.MessageType,
|
request.MessageType,
|
||||||
$"There is no User with Id {request.UserId}",
|
$"There is no user with Id {request.UserId}",
|
||||||
ErrorCodes.ObjectNotFound));
|
ErrorCodes.ObjectNotFound));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -61,13 +64,13 @@ public sealed class UpdateUserCommandHandler : CommandHandlerBase,
|
|||||||
{
|
{
|
||||||
var existingUser = await _userRepository.GetByEmailAsync(request.Email);
|
var existingUser = await _userRepository.GetByEmailAsync(request.Email);
|
||||||
|
|
||||||
if (existingUser != null)
|
if (existingUser is not null)
|
||||||
{
|
{
|
||||||
await Bus.RaiseEventAsync(
|
await NotifyAsync(
|
||||||
new DomainNotification(
|
new DomainNotification(
|
||||||
request.MessageType,
|
request.MessageType,
|
||||||
$"There is already a User with Email {request.Email}",
|
$"There is already a user with email {request.Email}",
|
||||||
DomainErrorCodes.UserAlreadyExists));
|
DomainErrorCodes.User.UserAlreadyExists));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -75,6 +78,18 @@ public sealed class UpdateUserCommandHandler : CommandHandlerBase,
|
|||||||
if (_user.GetUserRole() == UserRole.Admin)
|
if (_user.GetUserRole() == UserRole.Admin)
|
||||||
{
|
{
|
||||||
user.SetRole(request.Role);
|
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);
|
user.SetEmail(request.Email);
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
using CleanArchitecture.Domain.Constants;
|
||||||
using CleanArchitecture.Domain.Errors;
|
using CleanArchitecture.Domain.Errors;
|
||||||
using FluentValidation;
|
using FluentValidation;
|
||||||
|
|
||||||
@ -8,6 +9,7 @@ public sealed class UpdateUserCommandValidation : AbstractValidator<UpdateUserCo
|
|||||||
public UpdateUserCommandValidation()
|
public UpdateUserCommandValidation()
|
||||||
{
|
{
|
||||||
AddRuleForId();
|
AddRuleForId();
|
||||||
|
AddRuleForTenantId();
|
||||||
AddRuleForEmail();
|
AddRuleForEmail();
|
||||||
AddRuleForFirstName();
|
AddRuleForFirstName();
|
||||||
AddRuleForLastName();
|
AddRuleForLastName();
|
||||||
@ -18,48 +20,56 @@ public sealed class UpdateUserCommandValidation : AbstractValidator<UpdateUserCo
|
|||||||
{
|
{
|
||||||
RuleFor(cmd => cmd.UserId)
|
RuleFor(cmd => cmd.UserId)
|
||||||
.NotEmpty()
|
.NotEmpty()
|
||||||
.WithErrorCode(DomainErrorCodes.UserEmptyId)
|
.WithErrorCode(DomainErrorCodes.User.UserEmptyId)
|
||||||
.WithMessage("User id may not be empty");
|
.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()
|
private void AddRuleForEmail()
|
||||||
{
|
{
|
||||||
RuleFor(cmd => cmd.Email)
|
RuleFor(cmd => cmd.Email)
|
||||||
.EmailAddress()
|
.EmailAddress()
|
||||||
.WithErrorCode(DomainErrorCodes.UserInvalidEmail)
|
.WithErrorCode(DomainErrorCodes.User.UserInvalidEmail)
|
||||||
.WithMessage("Email is not a valid email address")
|
.WithMessage("Email is not a valid email address")
|
||||||
.MaximumLength(320)
|
.MaximumLength(MaxLengths.User.Email)
|
||||||
.WithErrorCode(DomainErrorCodes.UserEmailExceedsMaxLength)
|
.WithErrorCode(DomainErrorCodes.User.UserEmailExceedsMaxLength)
|
||||||
.WithMessage("Email may not be longer than 320 characters");
|
.WithMessage($"Email may not be longer than {MaxLengths.User.Email} characters");
|
||||||
}
|
}
|
||||||
|
|
||||||
private void AddRuleForFirstName()
|
private void AddRuleForFirstName()
|
||||||
{
|
{
|
||||||
RuleFor(cmd => cmd.FirstName)
|
RuleFor(cmd => cmd.FirstName)
|
||||||
.NotEmpty()
|
.NotEmpty()
|
||||||
.WithErrorCode(DomainErrorCodes.UserEmptyFirstName)
|
.WithErrorCode(DomainErrorCodes.User.UserEmptyFirstName)
|
||||||
.WithMessage("FirstName may not be empty")
|
.WithMessage("FirstName may not be empty")
|
||||||
.MaximumLength(100)
|
.MaximumLength(MaxLengths.User.FirstName)
|
||||||
.WithErrorCode(DomainErrorCodes.UserFirstNameExceedsMaxLength)
|
.WithErrorCode(DomainErrorCodes.User.UserFirstNameExceedsMaxLength)
|
||||||
.WithMessage("FirstName may not be longer than 100 characters");
|
.WithMessage($"FirstName may not be longer than {MaxLengths.User.FirstName} characters");
|
||||||
}
|
}
|
||||||
|
|
||||||
private void AddRuleForLastName()
|
private void AddRuleForLastName()
|
||||||
{
|
{
|
||||||
RuleFor(cmd => cmd.LastName)
|
RuleFor(cmd => cmd.LastName)
|
||||||
.NotEmpty()
|
.NotEmpty()
|
||||||
.WithErrorCode(DomainErrorCodes.UserEmptyLastName)
|
.WithErrorCode(DomainErrorCodes.User.UserEmptyLastName)
|
||||||
.WithMessage("LastName may not be empty")
|
.WithMessage("LastName may not be empty")
|
||||||
.MaximumLength(100)
|
.MaximumLength(MaxLengths.User.LastName)
|
||||||
.WithErrorCode(DomainErrorCodes.UserLastNameExceedsMaxLength)
|
.WithErrorCode(DomainErrorCodes.User.UserLastNameExceedsMaxLength)
|
||||||
.WithMessage("LastName may not be longer than 100 characters");
|
.WithMessage($"LastName may not be longer than {MaxLengths.User.LastName} characters");
|
||||||
}
|
}
|
||||||
|
|
||||||
private void AddRuleForRole()
|
private void AddRuleForRole()
|
||||||
{
|
{
|
||||||
RuleFor(cmd => cmd.Role)
|
RuleFor(cmd => cmd.Role)
|
||||||
.IsInEnum()
|
.IsInEnum()
|
||||||
.WithErrorCode(DomainErrorCodes.UserInvalidRole)
|
.WithErrorCode(DomainErrorCodes.User.UserInvalidRole)
|
||||||
.WithMessage("Role is not a valid role");
|
.WithMessage("Role is not a valid role");
|
||||||
}
|
}
|
||||||
}
|
}
|
12
CleanArchitecture.Domain/Constants/Ids.cs
Normal file
12
CleanArchitecture.Domain/Constants/Ids.cs
Normal file
@ -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");
|
||||||
|
}
|
||||||
|
}
|
17
CleanArchitecture.Domain/Constants/MaxLengths.cs
Normal file
17
CleanArchitecture.Domain/Constants/MaxLengths.cs
Normal file
@ -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;
|
||||||
|
}
|
||||||
|
}
|
@ -5,6 +5,8 @@ namespace CleanArchitecture.Domain.DomainEvents;
|
|||||||
|
|
||||||
public abstract class DomainEvent : Message, INotification
|
public abstract class DomainEvent : Message, INotification
|
||||||
{
|
{
|
||||||
|
public DateTime Timestamp { get; private set; }
|
||||||
|
|
||||||
protected DomainEvent(Guid aggregateId) : base(aggregateId)
|
protected DomainEvent(Guid aggregateId) : base(aggregateId)
|
||||||
{
|
{
|
||||||
Timestamp = DateTime.Now;
|
Timestamp = DateTime.Now;
|
||||||
@ -14,6 +16,4 @@ public abstract class DomainEvent : Message, INotification
|
|||||||
{
|
{
|
||||||
Timestamp = DateTime.Now;
|
Timestamp = DateTime.Now;
|
||||||
}
|
}
|
||||||
|
|
||||||
public DateTime Timestamp { get; private set; }
|
|
||||||
}
|
}
|
@ -5,6 +5,9 @@ namespace CleanArchitecture.Domain.DomainEvents;
|
|||||||
|
|
||||||
public abstract class Message : IRequest
|
public abstract class Message : IRequest
|
||||||
{
|
{
|
||||||
|
public Guid AggregateId { get; private set; }
|
||||||
|
public string MessageType { get; protected set; }
|
||||||
|
|
||||||
protected Message(Guid aggregateId)
|
protected Message(Guid aggregateId)
|
||||||
{
|
{
|
||||||
AggregateId = aggregateId;
|
AggregateId = aggregateId;
|
||||||
@ -16,7 +19,4 @@ public abstract class Message : IRequest
|
|||||||
AggregateId = aggregateId;
|
AggregateId = aggregateId;
|
||||||
MessageType = messageType ?? string.Empty;
|
MessageType = messageType ?? string.Empty;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Guid AggregateId { get; private set; }
|
|
||||||
public string MessageType { get; protected set; }
|
|
||||||
}
|
}
|
@ -24,5 +24,6 @@ public class StoredDomainEvent : DomainEvent
|
|||||||
|
|
||||||
// EF Constructor
|
// EF Constructor
|
||||||
protected StoredDomainEvent() : base(Guid.NewGuid())
|
protected StoredDomainEvent() : base(Guid.NewGuid())
|
||||||
{ }
|
{
|
||||||
|
}
|
||||||
}
|
}
|
@ -31,5 +31,6 @@ public class StoredDomainNotification : DomainNotification
|
|||||||
|
|
||||||
// EF Constructor
|
// EF Constructor
|
||||||
protected StoredDomainNotification() : base(string.Empty, string.Empty, string.Empty)
|
protected StoredDomainNotification() : base(string.Empty, string.Empty, string.Empty)
|
||||||
{ }
|
{
|
||||||
|
}
|
||||||
}
|
}
|
@ -4,14 +4,14 @@ namespace CleanArchitecture.Domain.Entities;
|
|||||||
|
|
||||||
public abstract class Entity
|
public abstract class Entity
|
||||||
{
|
{
|
||||||
|
public Guid Id { get; private set; }
|
||||||
|
public bool Deleted { get; private set; }
|
||||||
|
|
||||||
protected Entity(Guid id)
|
protected Entity(Guid id)
|
||||||
{
|
{
|
||||||
Id = id;
|
Id = id;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Guid Id { get; private set; }
|
|
||||||
public bool Deleted { get; private set; }
|
|
||||||
|
|
||||||
public void SetId(Guid id)
|
public void SetId(Guid id)
|
||||||
{
|
{
|
||||||
if (id == Guid.Empty)
|
if (id == Guid.Empty)
|
||||||
|
23
CleanArchitecture.Domain/Entities/Tenant.cs
Normal file
23
CleanArchitecture.Domain/Entities/Tenant.cs
Normal file
@ -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<User> Users { get; private set; } = new HashSet<User>();
|
||||||
|
|
||||||
|
public Tenant(
|
||||||
|
Guid id,
|
||||||
|
string name) : base(id)
|
||||||
|
{
|
||||||
|
Name = name;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetName(string name)
|
||||||
|
{
|
||||||
|
Name = name;
|
||||||
|
}
|
||||||
|
}
|
@ -1,26 +1,10 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Diagnostics.CodeAnalysis;
|
|
||||||
using CleanArchitecture.Domain.Enums;
|
using CleanArchitecture.Domain.Enums;
|
||||||
|
|
||||||
namespace CleanArchitecture.Domain.Entities;
|
namespace CleanArchitecture.Domain.Entities;
|
||||||
|
|
||||||
public class User : Entity
|
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 Email { get; private set; }
|
||||||
public string FirstName { get; private set; }
|
public string FirstName { get; private set; }
|
||||||
public string LastName { get; private set; }
|
public string LastName { get; private set; }
|
||||||
@ -29,71 +13,43 @@ public class User : Entity
|
|||||||
|
|
||||||
public string FullName => $"{FirstName}, {LastName}";
|
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)
|
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;
|
Email = email;
|
||||||
}
|
}
|
||||||
|
|
||||||
[MemberNotNull(nameof(FirstName))]
|
|
||||||
public void SetFirstName(string 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;
|
FirstName = firstName;
|
||||||
}
|
}
|
||||||
|
|
||||||
[MemberNotNull(nameof(LastName))]
|
|
||||||
public void SetLastName(string 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;
|
LastName = lastName;
|
||||||
}
|
}
|
||||||
|
|
||||||
[MemberNotNull(nameof(Password))]
|
|
||||||
public void SetPassword(string 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;
|
Password = password;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -101,4 +57,9 @@ public class User : Entity
|
|||||||
{
|
{
|
||||||
Role = role;
|
Role = role;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void SetTenant(Guid tenantId)
|
||||||
|
{
|
||||||
|
TenantId = tenantId;
|
||||||
|
}
|
||||||
}
|
}
|
@ -1,6 +1,8 @@
|
|||||||
namespace CleanArchitecture.Domain.Errors;
|
namespace CleanArchitecture.Domain.Errors;
|
||||||
|
|
||||||
public static class DomainErrorCodes
|
public static class DomainErrorCodes
|
||||||
|
{
|
||||||
|
public static class User
|
||||||
{
|
{
|
||||||
// User Validation
|
// User Validation
|
||||||
public const string UserEmptyId = "USER_EMPTY_ID";
|
public const string UserEmptyId = "USER_EMPTY_ID";
|
||||||
@ -21,7 +23,19 @@ public static class DomainErrorCodes
|
|||||||
public const string UserNumberPassword = "USER_PASSWORD_MUST_CONTAIN_A_NUMBER";
|
public const string UserNumberPassword = "USER_PASSWORD_MUST_CONTAIN_A_NUMBER";
|
||||||
public const string UserSpecialCharPassword = "USER_PASSWORD_MUST_CONTAIN_A_SPECIAL_CHARACTER";
|
public const string UserSpecialCharPassword = "USER_PASSWORD_MUST_CONTAIN_A_SPECIAL_CHARACTER";
|
||||||
|
|
||||||
// User
|
// General
|
||||||
public const string UserAlreadyExists = "USER_ALREADY_EXISTS";
|
public const string UserAlreadyExists = "USER_ALREADY_EXISTS";
|
||||||
public const string UserPasswordIncorrect = "USER_PASSWORD_INCORRECT";
|
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";
|
||||||
|
}
|
||||||
|
}
|
27
CleanArchitecture.Domain/EventHandler/TenantEventHandler.cs
Normal file
27
CleanArchitecture.Domain/EventHandler/TenantEventHandler.cs
Normal file
@ -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<TenantCreatedEvent>,
|
||||||
|
INotificationHandler<TenantDeletedEvent>,
|
||||||
|
INotificationHandler<TenantUpdatedEvent>
|
||||||
|
{
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
14
CleanArchitecture.Domain/Events/Tenant/TenantCreatedEvent.cs
Normal file
14
CleanArchitecture.Domain/Events/Tenant/TenantCreatedEvent.cs
Normal file
@ -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;
|
||||||
|
}
|
||||||
|
}
|
11
CleanArchitecture.Domain/Events/Tenant/TenantDeletedEvent.cs
Normal file
11
CleanArchitecture.Domain/Events/Tenant/TenantDeletedEvent.cs
Normal file
@ -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)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
14
CleanArchitecture.Domain/Events/Tenant/TenantUpdatedEvent.cs
Normal file
14
CleanArchitecture.Domain/Events/Tenant/TenantUpdatedEvent.cs
Normal file
@ -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;
|
||||||
|
}
|
||||||
|
}
|
@ -7,8 +7,5 @@ public sealed class PasswordChangedEvent : DomainEvent
|
|||||||
{
|
{
|
||||||
public PasswordChangedEvent(Guid userId) : base(userId)
|
public PasswordChangedEvent(Guid userId) : base(userId)
|
||||||
{
|
{
|
||||||
UserId = userId;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public Guid UserId { get; }
|
|
||||||
}
|
}
|
@ -7,8 +7,5 @@ public sealed class UserCreatedEvent : DomainEvent
|
|||||||
{
|
{
|
||||||
public UserCreatedEvent(Guid userId) : base(userId)
|
public UserCreatedEvent(Guid userId) : base(userId)
|
||||||
{
|
{
|
||||||
UserId = userId;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public Guid UserId { get; }
|
|
||||||
}
|
}
|
@ -7,8 +7,5 @@ public sealed class UserDeletedEvent : DomainEvent
|
|||||||
{
|
{
|
||||||
public UserDeletedEvent(Guid userId) : base(userId)
|
public UserDeletedEvent(Guid userId) : base(userId)
|
||||||
{
|
{
|
||||||
UserId = userId;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public Guid UserId { get; }
|
|
||||||
}
|
}
|
@ -7,8 +7,5 @@ public sealed class UserUpdatedEvent : DomainEvent
|
|||||||
{
|
{
|
||||||
public UserUpdatedEvent(Guid userId) : base(userId)
|
public UserUpdatedEvent(Guid userId) : base(userId)
|
||||||
{
|
{
|
||||||
UserId = userId;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public Guid UserId { get; }
|
|
||||||
}
|
}
|
@ -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.ChangePassword;
|
||||||
using CleanArchitecture.Domain.Commands.Users.CreateUser;
|
using CleanArchitecture.Domain.Commands.Users.CreateUser;
|
||||||
using CleanArchitecture.Domain.Commands.Users.DeleteUser;
|
using CleanArchitecture.Domain.Commands.Users.DeleteUser;
|
||||||
using CleanArchitecture.Domain.Commands.Users.LoginUser;
|
using CleanArchitecture.Domain.Commands.Users.LoginUser;
|
||||||
using CleanArchitecture.Domain.Commands.Users.UpdateUser;
|
using CleanArchitecture.Domain.Commands.Users.UpdateUser;
|
||||||
using CleanArchitecture.Domain.EventHandler;
|
using CleanArchitecture.Domain.EventHandler;
|
||||||
|
using CleanArchitecture.Domain.Events.Tenant;
|
||||||
using CleanArchitecture.Domain.Events.User;
|
using CleanArchitecture.Domain.Events.User;
|
||||||
using CleanArchitecture.Domain.Interfaces;
|
using CleanArchitecture.Domain.Interfaces;
|
||||||
using MediatR;
|
using MediatR;
|
||||||
@ -22,6 +26,10 @@ public static class ServiceCollectionExtension
|
|||||||
services.AddScoped<IRequestHandler<ChangePasswordCommand>, ChangePasswordCommandHandler>();
|
services.AddScoped<IRequestHandler<ChangePasswordCommand>, ChangePasswordCommandHandler>();
|
||||||
services.AddScoped<IRequestHandler<LoginUserCommand, string>, LoginUserCommandHandler>();
|
services.AddScoped<IRequestHandler<LoginUserCommand, string>, LoginUserCommandHandler>();
|
||||||
|
|
||||||
|
// Tenant
|
||||||
|
services.AddScoped<IRequestHandler<CreateTenantCommand>, CreateTenantCommandHandler>();
|
||||||
|
services.AddScoped<IRequestHandler<UpdateTenantCommand>, UpdateTenantCommandHandler>();
|
||||||
|
services.AddScoped<IRequestHandler<DeleteTenantCommand>, DeleteTenantCommandHandler>();
|
||||||
|
|
||||||
return services;
|
return services;
|
||||||
}
|
}
|
||||||
@ -34,6 +42,11 @@ public static class ServiceCollectionExtension
|
|||||||
services.AddScoped<INotificationHandler<UserDeletedEvent>, UserEventHandler>();
|
services.AddScoped<INotificationHandler<UserDeletedEvent>, UserEventHandler>();
|
||||||
services.AddScoped<INotificationHandler<PasswordChangedEvent>, UserEventHandler>();
|
services.AddScoped<INotificationHandler<PasswordChangedEvent>, UserEventHandler>();
|
||||||
|
|
||||||
|
// Tenant
|
||||||
|
services.AddScoped<INotificationHandler<TenantCreatedEvent>, TenantEventHandler>();
|
||||||
|
services.AddScoped<INotificationHandler<TenantUpdatedEvent>, TenantEventHandler>();
|
||||||
|
services.AddScoped<INotificationHandler<TenantDeletedEvent>, TenantEventHandler>();
|
||||||
|
|
||||||
return services;
|
return services;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -23,13 +23,13 @@ public static partial class CustomValidator
|
|||||||
int maxLength = 50)
|
int maxLength = 50)
|
||||||
{
|
{
|
||||||
var options = ruleBuilder
|
var options = ruleBuilder
|
||||||
.NotEmpty().WithErrorCode(DomainErrorCodes.UserEmptyPassword)
|
.NotEmpty().WithErrorCode(DomainErrorCodes.User.UserEmptyPassword)
|
||||||
.MinimumLength(minLength).WithErrorCode(DomainErrorCodes.UserShortPassword)
|
.MinimumLength(minLength).WithErrorCode(DomainErrorCodes.User.UserShortPassword)
|
||||||
.MaximumLength(maxLength).WithErrorCode(DomainErrorCodes.UserLongPassword)
|
.MaximumLength(maxLength).WithErrorCode(DomainErrorCodes.User.UserLongPassword)
|
||||||
.Matches("[A-Z]").WithErrorCode(DomainErrorCodes.UserUppercaseLetterPassword)
|
.Matches("[A-Z]").WithErrorCode(DomainErrorCodes.User.UserUppercaseLetterPassword)
|
||||||
.Matches("[a-z]").WithErrorCode(DomainErrorCodes.UserLowercaseLetterPassword)
|
.Matches("[a-z]").WithErrorCode(DomainErrorCodes.User.UserLowercaseLetterPassword)
|
||||||
.Matches("[0-9]").WithErrorCode(DomainErrorCodes.UserNumberPassword)
|
.Matches("[0-9]").WithErrorCode(DomainErrorCodes.User.UserNumberPassword)
|
||||||
.Matches("[^a-zA-Z0-9]").WithErrorCode(DomainErrorCodes.UserSpecialCharPassword);
|
.Matches("[^a-zA-Z0-9]").WithErrorCode(DomainErrorCodes.User.UserSpecialCharPassword);
|
||||||
return options;
|
return options;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user