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