mirror of
https://github.com/alex289/CleanArchitecture.git
synced 2025-06-30 02:31:08 +00:00
test: Add tenant tests
This commit is contained in:
parent
816d92fc85
commit
a3152580a2
@ -91,6 +91,7 @@ app.MapHealthChecks("/healthz", new HealthCheckOptions
|
||||
});
|
||||
app.MapControllers();
|
||||
app.MapGrpcService<UsersApiImplementation>();
|
||||
app.MapGrpcService<TenantsApiImplementation>();
|
||||
|
||||
app.Run();
|
||||
|
||||
|
@ -0,0 +1,37 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using CleanArchitecture.Application.Queries.Tenants.GetAll;
|
||||
using CleanArchitecture.Domain.Entities;
|
||||
using CleanArchitecture.Domain.Interfaces.Repositories;
|
||||
using MockQueryable.NSubstitute;
|
||||
using NSubstitute;
|
||||
|
||||
namespace CleanArchitecture.Application.Tests.Fixtures.Queries.Tenants;
|
||||
|
||||
public sealed class GetAllTenantsTestFixture : QueryHandlerBaseFixture
|
||||
{
|
||||
public GetAllTenantsQueryHandler QueryHandler { get; }
|
||||
private ITenantRepository TenantRepository { get; }
|
||||
|
||||
public GetAllTenantsTestFixture()
|
||||
{
|
||||
TenantRepository = Substitute.For<ITenantRepository>();
|
||||
|
||||
QueryHandler = new(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(
|
||||
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;
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -25,6 +25,7 @@ public sealed class GetAllTenantsQueryHandler :
|
||||
{
|
||||
return await _tenantRepository
|
||||
.GetAllNoTracking()
|
||||
.Include(x => x.Users)
|
||||
.Where(x => !x.Deleted)
|
||||
.Select(x => TenantViewModel.FromTenant(x))
|
||||
.ToListAsync(cancellationToken);
|
||||
|
@ -7,6 +7,7 @@ using CleanArchitecture.Domain.Interfaces;
|
||||
using CleanArchitecture.Domain.Interfaces.Repositories;
|
||||
using CleanArchitecture.Domain.Notifications;
|
||||
using MediatR;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace CleanArchitecture.Application.Queries.Tenants.GetTenantById;
|
||||
|
||||
@ -26,6 +27,7 @@ public sealed class GetTenantByIdQueryHandler :
|
||||
{
|
||||
var tenant = _tenantRepository
|
||||
.GetAllNoTracking()
|
||||
.Include(x => x.Users)
|
||||
.FirstOrDefault(x =>
|
||||
x.Id == request.TenantId &&
|
||||
x.Deleted == request.IsDeleted);
|
||||
|
@ -62,7 +62,8 @@ public sealed class UserService : IUserService
|
||||
user.Email,
|
||||
user.FirstName,
|
||||
user.LastName,
|
||||
user.Role));
|
||||
user.Role,
|
||||
user.TenantId));
|
||||
}
|
||||
|
||||
public async Task DeleteUserAsync(Guid userId)
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
@ -8,4 +8,5 @@ public sealed record UpdateUserViewModel(
|
||||
string Email,
|
||||
string FirstName,
|
||||
string LastName,
|
||||
UserRole Role);
|
||||
UserRole Role,
|
||||
Guid TenantId);
|
@ -0,0 +1,69 @@
|
||||
using System;
|
||||
using CleanArchitecture.Domain.Commands.Tenants.CreateTenant;
|
||||
using CleanArchitecture.Domain.Errors;
|
||||
using CleanArchitecture.Domain.Events.Tenant;
|
||||
using Xunit;
|
||||
|
||||
namespace CleanArchitecture.Domain.Tests.CommandHandler.Tenant.CreateTenant;
|
||||
|
||||
public sealed class CreateTenantCommandHandlerTests
|
||||
{
|
||||
private readonly CreateTenantCommandTestFixture _fixture = new();
|
||||
|
||||
[Fact]
|
||||
public void Should_Create_Tenant()
|
||||
{
|
||||
var command = new CreateTenantCommand(
|
||||
Guid.NewGuid(),
|
||||
"Test Tenant");
|
||||
|
||||
_fixture.CommandHandler.Handle(command, default!).Wait();
|
||||
|
||||
_fixture
|
||||
.VerifyNoDomainNotification()
|
||||
.VerifyCommit()
|
||||
.VerifyRaisedEvent<TenantCreatedEvent>(x =>
|
||||
x.AggregateId == command.AggregateId &&
|
||||
x.Name == command.Name);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Should_Not_Create_Tenant_Insufficient_Permissions()
|
||||
{
|
||||
_fixture.SetupUser();
|
||||
|
||||
var command = new CreateTenantCommand(
|
||||
Guid.NewGuid(),
|
||||
"Test Tenant");
|
||||
|
||||
_fixture.CommandHandler.Handle(command, default!).Wait();
|
||||
|
||||
_fixture
|
||||
.VerifyNoCommit()
|
||||
.VerifyNoRaisedEvent<TenantCreatedEvent>()
|
||||
.VerifyAnyDomainNotification()
|
||||
.VerifyExistingNotification(
|
||||
ErrorCodes.InsufficientPermissions,
|
||||
$"No permission to create tenant {command.AggregateId}");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Should_Not_Create_Tenant_Already_Exists()
|
||||
{
|
||||
var command = new CreateTenantCommand(
|
||||
Guid.NewGuid(),
|
||||
"Test Tenant");
|
||||
|
||||
_fixture.SetupExistingTenant(command.AggregateId);
|
||||
|
||||
_fixture.CommandHandler.Handle(command, default!).Wait();
|
||||
|
||||
_fixture
|
||||
.VerifyNoCommit()
|
||||
.VerifyNoRaisedEvent<TenantCreatedEvent>()
|
||||
.VerifyAnyDomainNotification()
|
||||
.VerifyExistingNotification(
|
||||
DomainErrorCodes.Tenant.TenantAlreadyExists,
|
||||
$"There is already a tenant with Id {command.AggregateId}");
|
||||
}
|
||||
}
|
@ -0,0 +1,38 @@
|
||||
using System;
|
||||
using CleanArchitecture.Domain.Commands.Tenants.CreateTenant;
|
||||
using CleanArchitecture.Domain.Enums;
|
||||
using CleanArchitecture.Domain.Interfaces.Repositories;
|
||||
using NSubstitute;
|
||||
|
||||
namespace CleanArchitecture.Domain.Tests.CommandHandler.Tenant.CreateTenant;
|
||||
|
||||
public sealed class CreateTenantCommandTestFixture : CommandHandlerFixtureBase
|
||||
{
|
||||
public CreateTenantCommandHandler CommandHandler { get;}
|
||||
|
||||
private ITenantRepository TenantRepository { get; }
|
||||
|
||||
public CreateTenantCommandTestFixture()
|
||||
{
|
||||
TenantRepository = Substitute.For<ITenantRepository>();
|
||||
|
||||
CommandHandler = new(
|
||||
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(
|
||||
id ?? Guid.NewGuid(),
|
||||
name ?? "Test Tenant");
|
||||
}
|
||||
}
|
@ -0,0 +1,45 @@
|
||||
using System;
|
||||
using CleanArchitecture.Domain.Commands.Tenants.DeleteTenant;
|
||||
using CleanArchitecture.Domain.Errors;
|
||||
using CleanArchitecture.Domain.Events.Tenant;
|
||||
using Xunit;
|
||||
|
||||
namespace CleanArchitecture.Domain.Tests.CommandHandler.Tenant.DeleteTenant;
|
||||
|
||||
public sealed class DeleteTenantCommandHandlerTests
|
||||
{
|
||||
private readonly DeleteTenantCommandTestFixture _fixture = new();
|
||||
|
||||
[Fact]
|
||||
public void Should_Delete_Tenant()
|
||||
{
|
||||
var tenant = _fixture.SetupTenant();
|
||||
|
||||
var command = new DeleteTenantCommand(tenant.Id);
|
||||
|
||||
_fixture.CommandHandler.Handle(command, default).Wait();
|
||||
|
||||
_fixture
|
||||
.VerifyNoDomainNotification()
|
||||
.VerifyCommit()
|
||||
.VerifyRaisedEvent<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}");
|
||||
}
|
||||
}
|
@ -0,0 +1,39 @@
|
||||
using System;
|
||||
using CleanArchitecture.Domain.Commands.Tenants.DeleteTenant;
|
||||
using CleanArchitecture.Domain.Interfaces.Repositories;
|
||||
using NSubstitute;
|
||||
|
||||
namespace CleanArchitecture.Domain.Tests.CommandHandler.Tenant.DeleteTenant;
|
||||
|
||||
public sealed class DeleteTenantCommandTestFixture : CommandHandlerFixtureBase
|
||||
{
|
||||
public DeleteTenantCommandHandler CommandHandler { get;}
|
||||
|
||||
private ITenantRepository TenantRepository { get; }
|
||||
private IUserRepository UserRepository { get; }
|
||||
|
||||
public DeleteTenantCommandTestFixture()
|
||||
{
|
||||
TenantRepository = Substitute.For<ITenantRepository>();
|
||||
UserRepository = Substitute.For<IUserRepository>();
|
||||
|
||||
CommandHandler = new(
|
||||
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;
|
||||
}
|
||||
}
|
@ -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(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,39 @@
|
||||
using System;
|
||||
using CleanArchitecture.Domain.Commands.Tenants.UpdateTenant;
|
||||
using CleanArchitecture.Domain.Enums;
|
||||
using CleanArchitecture.Domain.Interfaces;
|
||||
using CleanArchitecture.Domain.Interfaces.Repositories;
|
||||
using NSubstitute;
|
||||
|
||||
namespace CleanArchitecture.Domain.Tests.CommandHandler.Tenant.UpdateTenant;
|
||||
|
||||
public sealed class UpdateTenantCommandTestFixture : CommandHandlerFixtureBase
|
||||
{
|
||||
public UpdateTenantCommandHandler CommandHandler { get;}
|
||||
|
||||
private ITenantRepository TenantRepository { get; }
|
||||
|
||||
public UpdateTenantCommandTestFixture()
|
||||
{
|
||||
TenantRepository = Substitute.For<ITenantRepository>();
|
||||
|
||||
CommandHandler = new(
|
||||
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(
|
||||
id ?? Guid.NewGuid(),
|
||||
name ?? "Test Tenant");
|
||||
}
|
||||
}
|
@ -13,11 +13,15 @@ public sealed class CreateUserCommandHandlerTests
|
||||
[Fact]
|
||||
public void Should_Create_User()
|
||||
{
|
||||
_fixture.SetupUser();
|
||||
// Todo: Fix tests
|
||||
_fixture.SetupCurrentUser();
|
||||
|
||||
var user = _fixture.SetupUser();
|
||||
_fixture.SetupTenant(user.TenantId);
|
||||
|
||||
var command = new CreateUserCommand(
|
||||
Guid.NewGuid(),
|
||||
Guid.NewGuid(),
|
||||
user.TenantId,
|
||||
"test@email.com",
|
||||
"Test",
|
||||
"Email",
|
||||
@ -34,6 +38,8 @@ public sealed class CreateUserCommandHandlerTests
|
||||
[Fact]
|
||||
public void Should_Not_Create_Already_Existing_User()
|
||||
{
|
||||
_fixture.SetupCurrentUser();
|
||||
|
||||
var user = _fixture.SetupUser();
|
||||
|
||||
var command = new CreateUserCommand(
|
||||
@ -54,4 +60,54 @@ public sealed class CreateUserCommandHandlerTests
|
||||
DomainErrorCodes.User.UserAlreadyExists,
|
||||
$"There is already a user with Id {command.UserId}");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Should_Not_Create_User_Tenant_Does_Not_Exist()
|
||||
{
|
||||
_fixture.SetupCurrentUser();
|
||||
|
||||
_fixture.SetupUser();
|
||||
|
||||
var command = new CreateUserCommand(
|
||||
Guid.NewGuid(),
|
||||
Guid.NewGuid(),
|
||||
"test@email.com",
|
||||
"Test",
|
||||
"Email",
|
||||
"Po=PF]PC6t.?8?ks)A6W");
|
||||
|
||||
_fixture.CommandHandler.Handle(command, default).Wait();
|
||||
|
||||
_fixture
|
||||
.VerifyNoCommit()
|
||||
.VerifyNoRaisedEvent<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");
|
||||
}
|
||||
}
|
@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using CleanArchitecture.Domain.Commands.Users.CreateUser;
|
||||
using CleanArchitecture.Domain.Enums;
|
||||
using CleanArchitecture.Domain.Interfaces;
|
||||
using CleanArchitecture.Domain.Interfaces.Repositories;
|
||||
using NSubstitute;
|
||||
|
||||
@ -11,16 +12,23 @@ public sealed class CreateUserCommandTestFixture : CommandHandlerFixtureBase
|
||||
public CreateUserCommandTestFixture()
|
||||
{
|
||||
UserRepository = Substitute.For<IUserRepository>();
|
||||
TenantRepository = Substitute.For<ITenantRepository>();
|
||||
User = Substitute.For<IUser>();
|
||||
|
||||
CommandHandler = new CreateUserCommandHandler(
|
||||
Bus,
|
||||
UnitOfWork,
|
||||
NotificationHandler,
|
||||
UserRepository);
|
||||
UserRepository,
|
||||
TenantRepository,
|
||||
User);
|
||||
}
|
||||
|
||||
// Todo: Properties over ctor
|
||||
public CreateUserCommandHandler CommandHandler { get; }
|
||||
private IUserRepository UserRepository { get; }
|
||||
private ITenantRepository TenantRepository { get; }
|
||||
private IUser User { get; }
|
||||
|
||||
public Entities.User SetupUser()
|
||||
{
|
||||
@ -39,4 +47,29 @@ public sealed class CreateUserCommandTestFixture : CommandHandlerFixtureBase
|
||||
|
||||
return user;
|
||||
}
|
||||
|
||||
public void SetupCurrentUser()
|
||||
{
|
||||
var userId = Guid.NewGuid();
|
||||
|
||||
User.GetUserId().Returns(userId);
|
||||
|
||||
UserRepository
|
||||
.GetByIdAsync(Arg.Is<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;
|
||||
|
||||
@ -58,12 +59,12 @@ public sealed class CreateUserCommandValidationTests :
|
||||
[Fact]
|
||||
public void Should_Be_Invalid_For_Email_Exceeds_Max_Length()
|
||||
{
|
||||
var command = CreateTestCommand(email: new string('a', 320) + "@test.com");
|
||||
var command = CreateTestCommand(email: new string('a', MaxLengths.User.Email) + "@test.com");
|
||||
|
||||
ShouldHaveSingleError(
|
||||
command,
|
||||
DomainErrorCodes.User.UserEmailExceedsMaxLength,
|
||||
"Email may not be longer than 320 characters");
|
||||
$"Email may not be longer than {MaxLengths.User.Email} characters");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@ -80,12 +81,12 @@ public sealed class CreateUserCommandValidationTests :
|
||||
[Fact]
|
||||
public void Should_Be_Invalid_For_First_Name_Exceeds_Max_Length()
|
||||
{
|
||||
var command = CreateTestCommand(firstName: new string('a', 101));
|
||||
var command = CreateTestCommand(firstName: new string('a', MaxLengths.User.FirstName + 1));
|
||||
|
||||
ShouldHaveSingleError(
|
||||
command,
|
||||
DomainErrorCodes.User.UserFirstNameExceedsMaxLength,
|
||||
"FirstName may not be longer than 100 characters");
|
||||
$"FirstName may not be longer than {MaxLengths.User.FirstName} characters");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@ -102,12 +103,12 @@ public sealed class CreateUserCommandValidationTests :
|
||||
[Fact]
|
||||
public void Should_Be_Invalid_For_Last_Name_Exceeds_Max_Length()
|
||||
{
|
||||
var command = CreateTestCommand(lastName: new string('a', 101));
|
||||
var command = CreateTestCommand(lastName: new string('a', MaxLengths.User.LastName + 1));
|
||||
|
||||
ShouldHaveSingleError(
|
||||
command,
|
||||
DomainErrorCodes.User.UserLastNameExceedsMaxLength,
|
||||
"LastName may not be longer than 100 characters");
|
||||
$"LastName may not be longer than {MaxLengths.User.LastName} characters");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@ -175,6 +176,14 @@ public sealed class CreateUserCommandValidationTests :
|
||||
|
||||
ShouldHaveSingleError(command, DomainErrorCodes.User.UserLongPassword);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Should_Be_Invalid_For_Empty_Tenant_Id()
|
||||
{
|
||||
var command = CreateTestCommand(tenantId: Guid.Empty);
|
||||
|
||||
ShouldHaveSingleError(command, DomainErrorCodes.Tenant.TenantEmptyId);
|
||||
}
|
||||
|
||||
private static CreateUserCommand CreateTestCommand(
|
||||
Guid? userId = null,
|
||||
|
@ -1,6 +1,7 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using CleanArchitecture.Domain.Commands.Users.LoginUser;
|
||||
using CleanArchitecture.Domain.Constants;
|
||||
using CleanArchitecture.Domain.Errors;
|
||||
using Xunit;
|
||||
|
||||
@ -46,12 +47,12 @@ public sealed class LoginUserCommandValidationTests :
|
||||
[Fact]
|
||||
public void Should_Be_Invalid_For_Email_Exceeds_Max_Length()
|
||||
{
|
||||
var command = CreateTestCommand(new string('a', 320) + "@test.com");
|
||||
var command = CreateTestCommand(new string('a', MaxLengths.User.Email) + "@test.com");
|
||||
|
||||
ShouldHaveSingleError(
|
||||
command,
|
||||
DomainErrorCodes.User.UserEmailExceedsMaxLength,
|
||||
"Email may not be longer than 320 characters");
|
||||
$"Email may not be longer than {MaxLengths.User.Email} characters");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
@ -23,7 +23,8 @@ public sealed class UpdateUserCommandHandlerTests
|
||||
"test@email.com",
|
||||
"Test",
|
||||
"Email",
|
||||
UserRole.User);
|
||||
UserRole.User,
|
||||
Guid.NewGuid());
|
||||
|
||||
await _fixture.CommandHandler.Handle(command, default);
|
||||
|
||||
@ -43,7 +44,8 @@ public sealed class UpdateUserCommandHandlerTests
|
||||
"test@email.com",
|
||||
"Test",
|
||||
"Email",
|
||||
UserRole.User);
|
||||
UserRole.User,
|
||||
Guid.NewGuid());
|
||||
|
||||
await _fixture.CommandHandler.Handle(command, default);
|
||||
|
||||
@ -66,7 +68,8 @@ public sealed class UpdateUserCommandHandlerTests
|
||||
"test@email.com",
|
||||
"Test",
|
||||
"Email",
|
||||
UserRole.User);
|
||||
UserRole.User,
|
||||
Guid.NewGuid());
|
||||
|
||||
_fixture.UserRepository
|
||||
.GetByEmailAsync(command.Email)
|
||||
|
@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using CleanArchitecture.Domain.Commands.Users.UpdateUser;
|
||||
using CleanArchitecture.Domain.Constants;
|
||||
using CleanArchitecture.Domain.Enums;
|
||||
using CleanArchitecture.Domain.Errors;
|
||||
using Xunit;
|
||||
@ -57,12 +58,12 @@ public sealed class UpdateUserCommandValidationTests :
|
||||
[Fact]
|
||||
public void Should_Be_Invalid_For_Email_Exceeds_Max_Length()
|
||||
{
|
||||
var command = CreateTestCommand(email: new string('a', 320) + "@test.com");
|
||||
var command = CreateTestCommand(email: new string('a', MaxLengths.User.Email) + "@test.com");
|
||||
|
||||
ShouldHaveSingleError(
|
||||
command,
|
||||
DomainErrorCodes.User.UserEmailExceedsMaxLength,
|
||||
"Email may not be longer than 320 characters");
|
||||
$"Email may not be longer than {MaxLengths.User.Email} characters");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@ -79,12 +80,12 @@ public sealed class UpdateUserCommandValidationTests :
|
||||
[Fact]
|
||||
public void Should_Be_Invalid_For_First_Name_Exceeds_Max_Length()
|
||||
{
|
||||
var command = CreateTestCommand(firstName: new string('a', 101));
|
||||
var command = CreateTestCommand(firstName: new string('a', MaxLengths.User.FirstName + 1));
|
||||
|
||||
ShouldHaveSingleError(
|
||||
command,
|
||||
DomainErrorCodes.User.UserFirstNameExceedsMaxLength,
|
||||
"FirstName may not be longer than 100 characters");
|
||||
$"FirstName may not be longer than {MaxLengths.User.FirstName} characters");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@ -101,16 +102,28 @@ public sealed class UpdateUserCommandValidationTests :
|
||||
[Fact]
|
||||
public void Should_Be_Invalid_For_Last_Name_Exceeds_Max_Length()
|
||||
{
|
||||
var command = CreateTestCommand(lastName: new string('a', 101));
|
||||
var command = CreateTestCommand(lastName: new string('a', MaxLengths.User.LastName + 1));
|
||||
|
||||
ShouldHaveSingleError(
|
||||
command,
|
||||
DomainErrorCodes.User.UserLastNameExceedsMaxLength,
|
||||
"LastName may not be longer than 100 characters");
|
||||
$"LastName may not be longer than {MaxLengths.User.LastName} characters");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Should_Be_Invalid_For_Empty_Tenant_Id()
|
||||
{
|
||||
var command = CreateTestCommand(tenantId: Guid.Empty);
|
||||
|
||||
ShouldHaveSingleError(
|
||||
command,
|
||||
DomainErrorCodes.Tenant.TenantEmptyId,
|
||||
"Tenant id may not be empty");
|
||||
}
|
||||
|
||||
private static UpdateUserCommand CreateTestCommand(
|
||||
Guid? userId = null,
|
||||
Guid? tenantId = null,
|
||||
string? email = null,
|
||||
string? firstName = null,
|
||||
string? lastName = null,
|
||||
@ -121,6 +134,7 @@ public sealed class UpdateUserCommandValidationTests :
|
||||
email ?? "test@email.com",
|
||||
firstName ?? "test",
|
||||
lastName ?? "email",
|
||||
role ?? UserRole.User);
|
||||
role ?? UserRole.User,
|
||||
tenantId ?? Guid.NewGuid());
|
||||
}
|
||||
}
|
@ -1,6 +1,7 @@
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using CleanArchitecture.Domain.Entities;
|
||||
using CleanArchitecture.Domain.Enums;
|
||||
using CleanArchitecture.Domain.Errors;
|
||||
using CleanArchitecture.Domain.Events.Tenant;
|
||||
using CleanArchitecture.Domain.Interfaces;
|
||||
@ -14,14 +15,17 @@ public sealed class CreateTenantCommandHandler : CommandHandlerBase,
|
||||
IRequestHandler<CreateTenantCommand>
|
||||
{
|
||||
private readonly ITenantRepository _tenantRepository;
|
||||
|
||||
private readonly IUser _user;
|
||||
|
||||
public CreateTenantCommandHandler(
|
||||
IMediatorHandler bus,
|
||||
IUnitOfWork unitOfWork,
|
||||
INotificationHandler<DomainNotification> notifications,
|
||||
ITenantRepository tenantRepository) : base(bus, unitOfWork, notifications)
|
||||
ITenantRepository tenantRepository,
|
||||
IUser user) : base(bus, unitOfWork, notifications)
|
||||
{
|
||||
_tenantRepository = tenantRepository;
|
||||
_user = user;
|
||||
}
|
||||
|
||||
public async Task Handle(CreateTenantCommand request, CancellationToken cancellationToken)
|
||||
@ -30,6 +34,17 @@ public sealed class CreateTenantCommandHandler : CommandHandlerBase,
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (_user.GetUserRole() != UserRole.Admin)
|
||||
{
|
||||
await NotifyAsync(
|
||||
new DomainNotification(
|
||||
request.MessageType,
|
||||
$"No permission to create tenant {request.AggregateId}",
|
||||
ErrorCodes.InsufficientPermissions));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (await _tenantRepository.ExistsAsync(request.AggregateId))
|
||||
{
|
||||
|
@ -1,6 +1,7 @@
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using CleanArchitecture.Domain.Enums;
|
||||
using CleanArchitecture.Domain.Errors;
|
||||
using CleanArchitecture.Domain.Events.Tenant;
|
||||
using CleanArchitecture.Domain.Interfaces;
|
||||
@ -15,16 +16,19 @@ public sealed class DeleteTenantCommandHandler : CommandHandlerBase,
|
||||
{
|
||||
private readonly ITenantRepository _tenantRepository;
|
||||
private readonly IUserRepository _userRepository;
|
||||
|
||||
private readonly IUser _user;
|
||||
|
||||
public DeleteTenantCommandHandler(
|
||||
IMediatorHandler bus,
|
||||
IUnitOfWork unitOfWork,
|
||||
INotificationHandler<DomainNotification> notifications,
|
||||
ITenantRepository tenantRepository,
|
||||
IUserRepository userRepository) : base(bus, unitOfWork, notifications)
|
||||
IUserRepository userRepository,
|
||||
IUser user) : base(bus, unitOfWork, notifications)
|
||||
{
|
||||
_tenantRepository = tenantRepository;
|
||||
_userRepository = userRepository;
|
||||
_user = user;
|
||||
}
|
||||
|
||||
public async Task Handle(DeleteTenantCommand request, CancellationToken cancellationToken)
|
||||
@ -33,6 +37,19 @@ public sealed class DeleteTenantCommandHandler : CommandHandlerBase,
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Todo: Test following
|
||||
|
||||
if (_user.GetUserRole() != UserRole.Admin)
|
||||
{
|
||||
await NotifyAsync(
|
||||
new DomainNotification(
|
||||
request.MessageType,
|
||||
$"No permission to delete tenant {request.AggregateId}",
|
||||
ErrorCodes.InsufficientPermissions));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
var tenant = await _tenantRepository.GetByIdAsync(request.AggregateId);
|
||||
|
||||
|
@ -1,5 +1,6 @@
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using CleanArchitecture.Domain.Enums;
|
||||
using CleanArchitecture.Domain.Errors;
|
||||
using CleanArchitecture.Domain.Events.Tenant;
|
||||
using CleanArchitecture.Domain.Interfaces;
|
||||
@ -13,14 +14,17 @@ public sealed class UpdateTenantCommandHandler : CommandHandlerBase,
|
||||
IRequestHandler<UpdateTenantCommand>
|
||||
{
|
||||
private readonly ITenantRepository _tenantRepository;
|
||||
|
||||
private readonly IUser _user;
|
||||
|
||||
public UpdateTenantCommandHandler(
|
||||
IMediatorHandler bus,
|
||||
IUnitOfWork unitOfWork,
|
||||
INotificationHandler<DomainNotification> notifications,
|
||||
ITenantRepository tenantRepository) : base(bus, unitOfWork, notifications)
|
||||
ITenantRepository tenantRepository,
|
||||
IUser user) : base(bus, unitOfWork, notifications)
|
||||
{
|
||||
_tenantRepository = tenantRepository;
|
||||
_user = user;
|
||||
}
|
||||
|
||||
public async Task Handle(UpdateTenantCommand request, CancellationToken cancellationToken)
|
||||
@ -29,6 +33,17 @@ public sealed class UpdateTenantCommandHandler : CommandHandlerBase,
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (_user.GetUserRole() != UserRole.Admin)
|
||||
{
|
||||
await NotifyAsync(
|
||||
new DomainNotification(
|
||||
request.MessageType,
|
||||
$"No permission to update tenant {request.AggregateId}",
|
||||
ErrorCodes.InsufficientPermissions));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
var tenant = await _tenantRepository.GetByIdAsync(request.AggregateId);
|
||||
|
||||
|
@ -16,14 +16,20 @@ public sealed class CreateUserCommandHandler : CommandHandlerBase,
|
||||
IRequestHandler<CreateUserCommand>
|
||||
{
|
||||
private readonly IUserRepository _userRepository;
|
||||
private readonly ITenantRepository _tenantRepository;
|
||||
private readonly IUser _user;
|
||||
|
||||
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)
|
||||
@ -32,12 +38,24 @@ public sealed class CreateUserCommandHandler : CommandHandlerBase,
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var currentUser = await _userRepository.GetByIdAsync(_user.GetUserId());
|
||||
|
||||
if (currentUser is null || currentUser.Role != UserRole.Admin)
|
||||
{
|
||||
await NotifyAsync(
|
||||
new DomainNotification(
|
||||
request.MessageType,
|
||||
"You are not allowed to create users",
|
||||
ErrorCodes.InsufficientPermissions));
|
||||
return;
|
||||
}
|
||||
|
||||
var existingUser = await _userRepository.GetByIdAsync(request.UserId);
|
||||
|
||||
if (existingUser is not null)
|
||||
{
|
||||
await Bus.RaiseEventAsync(
|
||||
await NotifyAsync(
|
||||
new DomainNotification(
|
||||
request.MessageType,
|
||||
$"There is already a user with Id {request.UserId}",
|
||||
@ -49,7 +67,7 @@ public sealed class CreateUserCommandHandler : CommandHandlerBase,
|
||||
|
||||
if (existingUser is not null)
|
||||
{
|
||||
await Bus.RaiseEventAsync(
|
||||
await NotifyAsync(
|
||||
new DomainNotification(
|
||||
request.MessageType,
|
||||
$"There is already a user with email {request.Email}",
|
||||
@ -57,6 +75,16 @@ public sealed class CreateUserCommandHandler : CommandHandlerBase,
|
||||
return;
|
||||
}
|
||||
|
||||
if (!await _tenantRepository.ExistsAsync(request.TenantId))
|
||||
{
|
||||
await NotifyAsync(
|
||||
new DomainNotification(
|
||||
request.MessageType,
|
||||
$"There is no tenant with Id {request.TenantId}",
|
||||
ErrorCodes.ObjectNotFound));
|
||||
return;
|
||||
}
|
||||
|
||||
var passwordHash = BC.HashPassword(request.Password);
|
||||
|
||||
var user = new User(
|
||||
|
@ -10,6 +10,7 @@ public sealed class CreateUserCommandValidation : AbstractValidator<CreateUserCo
|
||||
public CreateUserCommandValidation()
|
||||
{
|
||||
AddRuleForId();
|
||||
AddRuleForTenantId();
|
||||
AddRuleForEmail();
|
||||
AddRuleForFirstName();
|
||||
AddRuleForLastName();
|
||||
@ -24,6 +25,14 @@ public sealed class CreateUserCommandValidation : AbstractValidator<CreateUserCo
|
||||
.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)
|
||||
@ -32,7 +41,7 @@ public sealed class CreateUserCommandValidation : AbstractValidator<CreateUserCo
|
||||
.WithMessage("Email is not a valid email address")
|
||||
.MaximumLength(MaxLengths.User.Email)
|
||||
.WithErrorCode(DomainErrorCodes.User.UserEmailExceedsMaxLength)
|
||||
.WithMessage("Email may not be longer than 320 characters");
|
||||
.WithMessage($"Email may not be longer than {MaxLengths.User.Email} characters");
|
||||
}
|
||||
|
||||
private void AddRuleForFirstName()
|
||||
@ -43,7 +52,7 @@ public sealed class CreateUserCommandValidation : AbstractValidator<CreateUserCo
|
||||
.WithMessage("FirstName may not be empty")
|
||||
.MaximumLength(MaxLengths.User.FirstName)
|
||||
.WithErrorCode(DomainErrorCodes.User.UserFirstNameExceedsMaxLength)
|
||||
.WithMessage("FirstName may not be longer than 100 characters");
|
||||
.WithMessage($"FirstName may not be longer than {MaxLengths.User.FirstName} characters");
|
||||
}
|
||||
|
||||
private void AddRuleForLastName()
|
||||
@ -54,7 +63,7 @@ public sealed class CreateUserCommandValidation : AbstractValidator<CreateUserCo
|
||||
.WithMessage("LastName may not be empty")
|
||||
.MaximumLength(MaxLengths.User.LastName)
|
||||
.WithErrorCode(DomainErrorCodes.User.UserLastNameExceedsMaxLength)
|
||||
.WithMessage("LastName may not be longer than 100 characters");
|
||||
.WithMessage($"LastName may not be longer than {MaxLengths.User.LastName} characters");
|
||||
}
|
||||
|
||||
private void AddRuleForPassword()
|
||||
|
@ -21,7 +21,7 @@ public sealed class LoginUserCommandValidation : AbstractValidator<LoginUserComm
|
||||
.WithMessage("Email is not a valid email address")
|
||||
.MaximumLength(MaxLengths.User.Email)
|
||||
.WithErrorCode(DomainErrorCodes.User.UserEmailExceedsMaxLength)
|
||||
.WithMessage("Email may not be longer than 320 characters");
|
||||
.WithMessage($"Email may not be longer than {MaxLengths.User.Email} characters");
|
||||
}
|
||||
|
||||
private void AddRuleForPassword()
|
||||
|
@ -12,16 +12,18 @@ public sealed class UpdateUserCommand : CommandBase
|
||||
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 Guid TenantId { get; }
|
||||
public string Email { get; }
|
||||
public string FirstName { get; }
|
||||
public string LastName { get; }
|
||||
|
@ -38,7 +38,7 @@ public sealed class UpdateUserCommandHandler : CommandHandlerBase,
|
||||
|
||||
if (user is null)
|
||||
{
|
||||
await Bus.RaiseEventAsync(
|
||||
await NotifyAsync(
|
||||
new DomainNotification(
|
||||
request.MessageType,
|
||||
$"There is no user with Id {request.UserId}",
|
||||
@ -63,7 +63,7 @@ public sealed class UpdateUserCommandHandler : CommandHandlerBase,
|
||||
|
||||
if (existingUser is not null)
|
||||
{
|
||||
await Bus.RaiseEventAsync(
|
||||
await NotifyAsync(
|
||||
new DomainNotification(
|
||||
request.MessageType,
|
||||
$"There is already a user with email {request.Email}",
|
||||
@ -75,6 +75,10 @@ public sealed class UpdateUserCommandHandler : CommandHandlerBase,
|
||||
if (_user.GetUserRole() == UserRole.Admin)
|
||||
{
|
||||
user.SetRole(request.Role);
|
||||
|
||||
// Todo: Test
|
||||
// Todo: Check if tenant exists first
|
||||
user.SetTenant(request.TenantId);
|
||||
}
|
||||
|
||||
user.SetEmail(request.Email);
|
||||
|
@ -9,6 +9,7 @@ public sealed class UpdateUserCommandValidation : AbstractValidator<UpdateUserCo
|
||||
public UpdateUserCommandValidation()
|
||||
{
|
||||
AddRuleForId();
|
||||
AddRuleForTenantId();
|
||||
AddRuleForEmail();
|
||||
AddRuleForFirstName();
|
||||
AddRuleForLastName();
|
||||
@ -22,6 +23,14 @@ public sealed class UpdateUserCommandValidation : AbstractValidator<UpdateUserCo
|
||||
.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()
|
||||
{
|
||||
@ -31,7 +40,7 @@ public sealed class UpdateUserCommandValidation : AbstractValidator<UpdateUserCo
|
||||
.WithMessage("Email is not a valid email address")
|
||||
.MaximumLength(MaxLengths.User.Email)
|
||||
.WithErrorCode(DomainErrorCodes.User.UserEmailExceedsMaxLength)
|
||||
.WithMessage("Email may not be longer than 320 characters");
|
||||
.WithMessage($"Email may not be longer than {MaxLengths.User.Email} characters");
|
||||
}
|
||||
|
||||
private void AddRuleForFirstName()
|
||||
@ -42,7 +51,7 @@ public sealed class UpdateUserCommandValidation : AbstractValidator<UpdateUserCo
|
||||
.WithMessage("FirstName may not be empty")
|
||||
.MaximumLength(MaxLengths.User.FirstName)
|
||||
.WithErrorCode(DomainErrorCodes.User.UserFirstNameExceedsMaxLength)
|
||||
.WithMessage("FirstName may not be longer than 100 characters");
|
||||
.WithMessage($"FirstName may not be longer than {MaxLengths.User.FirstName} characters");
|
||||
}
|
||||
|
||||
private void AddRuleForLastName()
|
||||
@ -53,7 +62,7 @@ public sealed class UpdateUserCommandValidation : AbstractValidator<UpdateUserCo
|
||||
.WithMessage("LastName may not be empty")
|
||||
.MaximumLength(MaxLengths.User.LastName)
|
||||
.WithErrorCode(DomainErrorCodes.User.UserLastNameExceedsMaxLength)
|
||||
.WithMessage("LastName may not be longer than 100 characters");
|
||||
.WithMessage($"LastName may not be longer than {MaxLengths.User.LastName} characters");
|
||||
}
|
||||
|
||||
private void AddRuleForRole()
|
||||
|
@ -1,6 +1,6 @@
|
||||
namespace CleanArchitecture.Domain.Constants;
|
||||
|
||||
public sealed class MaxLengths
|
||||
public static class MaxLengths
|
||||
{
|
||||
public static class User
|
||||
{
|
||||
|
@ -11,7 +11,7 @@ public class User : Entity
|
||||
public string LastName { get; private set; }
|
||||
public string Password { get; private set; }
|
||||
public UserRole Role { get; private set; }
|
||||
|
||||
|
||||
public string FullName => $"{FirstName}, {LastName}";
|
||||
|
||||
public Guid TenantId { get; private set; }
|
||||
@ -58,4 +58,9 @@ public class User : Entity
|
||||
{
|
||||
Role = role;
|
||||
}
|
||||
|
||||
public void SetTenant(Guid tenantId)
|
||||
{
|
||||
TenantId = tenantId;
|
||||
}
|
||||
}
|
@ -34,7 +34,7 @@ public static class DomainErrorCodes
|
||||
public const string TenantEmptyId = "TENANT_EMPTY_ID";
|
||||
public const string TenantEmptyName = "TENANT_EMPTY_NAME";
|
||||
public const string TenantNameExceedsMaxLength = "TENANT_NAME_EXCEEDS_MAX_LENGTH";
|
||||
|
||||
|
||||
// General
|
||||
public const string TenantAlreadyExists = "TENANT_ALREADY_EXISTS";
|
||||
}
|
||||
|
@ -55,7 +55,7 @@ public class BaseRepository<TEntity> : IRepository<TEntity> where TEntity : Enti
|
||||
DbSet.Update(entity);
|
||||
}
|
||||
|
||||
public async Task<bool> ExistsAsync(Guid id)
|
||||
public virtual async Task<bool> ExistsAsync(Guid id)
|
||||
{
|
||||
return await DbSet.AnyAsync(entity => entity.Id == id);
|
||||
}
|
||||
|
@ -0,0 +1,136 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Threading.Tasks;
|
||||
using CleanArchitecture.Application.ViewModels.Tenants;
|
||||
using CleanArchitecture.IntegrationTests.Extensions;
|
||||
using CleanArchitecture.IntegrationTests.Fixtures;
|
||||
using FluentAssertions;
|
||||
using Xunit;
|
||||
using Xunit.Priority;
|
||||
|
||||
namespace CleanArchitecture.IntegrationTests.Controller;
|
||||
|
||||
[Collection("IntegrationTests")]
|
||||
[TestCaseOrderer(PriorityOrderer.Name, PriorityOrderer.Assembly)]
|
||||
public sealed class TenantControllerTests : IClassFixture<TenantTestFixture>
|
||||
{
|
||||
private readonly TenantTestFixture _fixture;
|
||||
|
||||
public TenantControllerTests(TenantTestFixture fixture)
|
||||
{
|
||||
_fixture = fixture;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Priority(0)]
|
||||
public async Task Should_Get_Tenant_By_Id()
|
||||
{
|
||||
await _fixture.AuthenticateUserAsync();
|
||||
|
||||
var response = await _fixture.ServerClient.GetAsync($"/api/v1/Tenant/{_fixture.CreatedTenantId}");
|
||||
|
||||
response.StatusCode.Should().Be(HttpStatusCode.OK);
|
||||
|
||||
var message = await response.Content.ReadAsJsonAsync<TenantViewModel>();
|
||||
|
||||
message?.Data.Should().NotBeNull();
|
||||
|
||||
message!.Data!.Id.Should().Be(_fixture.CreatedTenantId);
|
||||
message.Data.Name.Should().Be("Test Tenant");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Priority(5)]
|
||||
public async Task Should_Get_All_Tenants()
|
||||
{
|
||||
await _fixture.AuthenticateUserAsync();
|
||||
|
||||
var response = await _fixture.ServerClient.GetAsync("api/v1/Tenant");
|
||||
|
||||
response.StatusCode.Should().Be(HttpStatusCode.OK);
|
||||
|
||||
var message = await response.Content.ReadAsJsonAsync<IEnumerable<TenantViewModel>>();
|
||||
|
||||
message?.Data.Should().NotBeEmpty();
|
||||
message!.Data.Should().HaveCountGreaterOrEqualTo(2);
|
||||
message.Data!
|
||||
.FirstOrDefault(x => x.Id == _fixture.CreatedTenantId)
|
||||
.Should().NotBeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Priority(10)]
|
||||
public async Task Should_Create_Tenant()
|
||||
{
|
||||
await _fixture.AuthenticateUserAsync();
|
||||
|
||||
var request = new CreateTenantViewModel("Test Tenant 2");
|
||||
|
||||
var response = await _fixture.ServerClient.PostAsJsonAsync("/api/v1/Tenant", request);
|
||||
|
||||
response.StatusCode.Should().Be(HttpStatusCode.OK);
|
||||
|
||||
var message = await response.Content.ReadAsJsonAsync<Guid>();
|
||||
var tenantId = message?.Data;
|
||||
|
||||
// Check if tenant exists
|
||||
var tenantResponse = await _fixture.ServerClient.GetAsync($"/api/v1/Tenant/{tenantId}");
|
||||
|
||||
tenantResponse.StatusCode.Should().Be(HttpStatusCode.OK);
|
||||
|
||||
var tenantMessage = await tenantResponse.Content.ReadAsJsonAsync<TenantViewModel>();
|
||||
|
||||
tenantMessage?.Data.Should().NotBeNull();
|
||||
|
||||
tenantMessage!.Data!.Id.Should().Be(tenantId!.Value);
|
||||
tenantMessage.Data.Name.Should().Be(request.Name);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Priority(15)]
|
||||
public async Task Should_Update_Tenant()
|
||||
{
|
||||
await _fixture.AuthenticateUserAsync();
|
||||
|
||||
var request = new UpdateTenantViewModel(_fixture.CreatedTenantId, "Test Tenant 3");
|
||||
|
||||
var response = await _fixture.ServerClient.PutAsJsonAsync("/api/v1/Tenant", request);
|
||||
|
||||
response.StatusCode.Should().Be(HttpStatusCode.OK);
|
||||
|
||||
var message = await response.Content.ReadAsJsonAsync<UpdateTenantViewModel>();
|
||||
|
||||
message?.Data.Should().NotBeNull();
|
||||
message!.Data.Should().BeEquivalentTo(request);
|
||||
|
||||
// Check if tenant is updated
|
||||
var tenantResponse = await _fixture.ServerClient.GetAsync($"/api/v1/Tenant/{_fixture.CreatedTenantId}");
|
||||
|
||||
tenantResponse.StatusCode.Should().Be(HttpStatusCode.OK);
|
||||
|
||||
var tenantMessage = await response.Content.ReadAsJsonAsync<TenantViewModel>();
|
||||
|
||||
tenantMessage?.Data.Should().NotBeNull();
|
||||
|
||||
tenantMessage!.Data!.Id.Should().Be(_fixture.CreatedTenantId);
|
||||
tenantMessage.Data.Name.Should().Be(request.Name);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Priority(20)]
|
||||
public async Task Should_Delete_Tenant()
|
||||
{
|
||||
await _fixture.AuthenticateUserAsync();
|
||||
|
||||
var response = await _fixture.ServerClient.DeleteAsync($"/api/v1/Tenant/{_fixture.CreatedTenantId}");
|
||||
|
||||
response.StatusCode.Should().Be(HttpStatusCode.OK);
|
||||
|
||||
// Check if tenant is deleted
|
||||
var tenantResponse = await _fixture.ServerClient.GetAsync($"/api/v1/Tenant/{_fixture.CreatedTenantId}");
|
||||
|
||||
tenantResponse.StatusCode.Should().Be(HttpStatusCode.NotFound);
|
||||
}
|
||||
}
|
@ -25,10 +25,14 @@ public sealed class UserControllerTests : IClassFixture<UserTestFixture>
|
||||
_fixture = fixture;
|
||||
}
|
||||
|
||||
// Todo: Refactor tests to work alone
|
||||
|
||||
[Fact]
|
||||
[Priority(0)]
|
||||
public async Task Should_Create_User()
|
||||
{
|
||||
await _fixture.AuthenticateUserAsync();
|
||||
|
||||
var user = new CreateUserViewModel(
|
||||
_fixture.CreatedUserEmail,
|
||||
"Test",
|
||||
@ -116,7 +120,8 @@ public sealed class UserControllerTests : IClassFixture<UserTestFixture>
|
||||
"newtest@email.com",
|
||||
"NewTest",
|
||||
"NewEmail",
|
||||
UserRole.User);
|
||||
UserRole.User,
|
||||
Ids.Seed.TenantId);
|
||||
|
||||
var response = await _fixture.ServerClient.PutAsJsonAsync("/api/v1/user", user);
|
||||
|
||||
@ -232,5 +237,7 @@ public sealed class UserControllerTests : IClassFixture<UserTestFixture>
|
||||
|
||||
var content = message!.Data;
|
||||
content.Should().Be(_fixture.CreatedUserId);
|
||||
|
||||
// Todo: Check if stuff is done
|
||||
}
|
||||
}
|
@ -0,0 +1,19 @@
|
||||
using System;
|
||||
using CleanArchitecture.Domain.Entities;
|
||||
using CleanArchitecture.Infrastructure.Database;
|
||||
|
||||
namespace CleanArchitecture.IntegrationTests.Fixtures;
|
||||
|
||||
public sealed class TenantTestFixture : TestFixtureBase
|
||||
{
|
||||
public Guid CreatedTenantId { get; } = Guid.NewGuid();
|
||||
|
||||
protected override void SeedTestData(ApplicationDbContext context)
|
||||
{
|
||||
context.Tenants.Add(new Tenant(
|
||||
CreatedTenantId,
|
||||
"Test Tenant"));
|
||||
|
||||
context.SaveChanges();
|
||||
}
|
||||
}
|
@ -1,6 +1,10 @@
|
||||
using System;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Threading.Tasks;
|
||||
using CleanArchitecture.Application.ViewModels.Users;
|
||||
using CleanArchitecture.Infrastructure.Database;
|
||||
using CleanArchitecture.IntegrationTests.Extensions;
|
||||
using CleanArchitecture.IntegrationTests.Infrastructure;
|
||||
using Microsoft.AspNetCore.Mvc.Testing;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
@ -32,4 +36,18 @@ public class TestFixtureBase
|
||||
IServiceProvider scopedServices)
|
||||
{
|
||||
}
|
||||
|
||||
// Todo: Fix auth
|
||||
public virtual async Task AuthenticateUserAsync()
|
||||
{
|
||||
ServerClient.DefaultRequestHeaders.Clear();
|
||||
var user = new LoginUserViewModel(
|
||||
"admin@email.com",
|
||||
"!Password123#");
|
||||
|
||||
var response = await ServerClient.PostAsJsonAsync("/api/v1/user/login", user);
|
||||
|
||||
var message = await response.Content.ReadAsJsonAsync<string>();
|
||||
ServerClient.DefaultRequestHeaders.Add("Authorization", $"Bearer {message!.Data}");
|
||||
}
|
||||
}
|
@ -11,6 +11,7 @@ public sealed class UserTestFixture : TestFixtureBase
|
||||
|
||||
public void EnableAuthentication()
|
||||
{
|
||||
ServerClient.DefaultRequestHeaders.Clear();
|
||||
ServerClient.DefaultRequestHeaders.Add("Authorization", $"Bearer {CreatedUserToken}");
|
||||
}
|
||||
}
|
@ -0,0 +1,37 @@
|
||||
using System;
|
||||
using CleanArchitecture.Domain.Entities;
|
||||
using CleanArchitecture.Infrastructure.Database;
|
||||
using Grpc.Net.Client;
|
||||
|
||||
namespace CleanArchitecture.IntegrationTests.Fixtures.gRPC;
|
||||
|
||||
public sealed class GetTenantsByIdsTestFixture : TestFixtureBase
|
||||
{
|
||||
public GrpcChannel GrpcChannel { get; }
|
||||
public Guid CreatedTenantId { get; } = Guid.NewGuid();
|
||||
|
||||
public GetTenantsByIdsTestFixture()
|
||||
{
|
||||
GrpcChannel = GrpcChannel.ForAddress("http://localhost", new GrpcChannelOptions
|
||||
{
|
||||
HttpHandler = Factory.Server.CreateHandler()
|
||||
});
|
||||
}
|
||||
|
||||
protected override void SeedTestData(ApplicationDbContext context)
|
||||
{
|
||||
base.SeedTestData(context);
|
||||
|
||||
var tenant = CreateTenant();
|
||||
|
||||
context.Tenants.Add(tenant);
|
||||
context.SaveChanges();
|
||||
}
|
||||
|
||||
public Tenant CreateTenant()
|
||||
{
|
||||
return new(
|
||||
CreatedTenantId,
|
||||
"Test Tenant");
|
||||
}
|
||||
}
|
@ -0,0 +1,38 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using CleanArchitecture.IntegrationTests.Fixtures.gRPC;
|
||||
using CleanArchitecture.Proto.Tenants;
|
||||
using FluentAssertions;
|
||||
using Xunit;
|
||||
|
||||
namespace CleanArchitecture.IntegrationTests.gRPC;
|
||||
|
||||
public sealed class GetTenantsByIdsTests : IClassFixture<GetTenantsByIdsTestFixture>
|
||||
{
|
||||
private readonly GetTenantsByIdsTestFixture _fixture;
|
||||
|
||||
public GetTenantsByIdsTests(GetTenantsByIdsTestFixture fixture)
|
||||
{
|
||||
_fixture = fixture;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Should_Get_Tenants_By_Ids()
|
||||
{
|
||||
var client = new TenantsApi.TenantsApiClient(_fixture.GrpcChannel);
|
||||
|
||||
var request = new GetTenantsByIdsRequest();
|
||||
request.Ids.Add(_fixture.CreatedTenantId.ToString());
|
||||
|
||||
var response = await client.GetByIdsAsync(request);
|
||||
|
||||
response.Tenants.Should().HaveCount(1);
|
||||
|
||||
var tenant = response.Tenants.First();
|
||||
var createdTenant = _fixture.CreateTenant();
|
||||
|
||||
new Guid(tenant.Id).Should().Be(createdTenant.Id);
|
||||
tenant.Name.Should().Be(createdTenant.Name);
|
||||
}
|
||||
}
|
@ -26,7 +26,7 @@ public sealed class GetUsersByIdsTests : IClassFixture<GetUsersByIdsTestFixture>
|
||||
|
||||
var response = await client.GetByIdsAsync(request);
|
||||
|
||||
response.Users.Count.Should().Be(1);
|
||||
response.Users.Should().HaveCount(1);
|
||||
|
||||
var user = response.Users.First();
|
||||
var createdUser = _fixture.CreateUser();
|
||||
|
33
CleanArchitecture.gRPC.Tests/Fixtures/TenantTestFixture.cs
Normal file
33
CleanArchitecture.gRPC.Tests/Fixtures/TenantTestFixture.cs
Normal file
@ -0,0 +1,33 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using CleanArchitecture.Application.gRPC;
|
||||
using CleanArchitecture.Domain.Entities;
|
||||
using CleanArchitecture.Domain.Interfaces.Repositories;
|
||||
using MockQueryable.NSubstitute;
|
||||
using NSubstitute;
|
||||
|
||||
namespace CleanArchitecture.gRPC.Tests.Fixtures;
|
||||
|
||||
public sealed class TenantTestFixture
|
||||
{
|
||||
public TenantsApiImplementation TenantsApiImplementation { get; }
|
||||
private ITenantRepository TenantRepository { get; }
|
||||
|
||||
public IEnumerable<Tenant> ExistingTenants { get; }
|
||||
|
||||
public TenantTestFixture()
|
||||
{
|
||||
TenantRepository = Substitute.For<ITenantRepository>();
|
||||
|
||||
ExistingTenants = new List<Tenant>
|
||||
{
|
||||
new Tenant(Guid.NewGuid(), "Tenant 1"),
|
||||
new Tenant(Guid.NewGuid(), "Tenant 2"),
|
||||
new Tenant(Guid.NewGuid(), "Tenant 3"),
|
||||
};
|
||||
|
||||
TenantRepository.GetAllNoTracking().Returns(ExistingTenants.BuildMock());
|
||||
|
||||
TenantsApiImplementation = new(TenantRepository);
|
||||
}
|
||||
}
|
@ -9,9 +9,9 @@ using NSubstitute;
|
||||
|
||||
namespace CleanArchitecture.gRPC.Tests.Fixtures;
|
||||
|
||||
public sealed class UserTestsFixture
|
||||
public sealed class UserTestFixture
|
||||
{
|
||||
public UserTestsFixture()
|
||||
public UserTestFixture()
|
||||
{
|
||||
ExistingUsers = new List<User>
|
||||
{
|
72
CleanArchitecture.gRPC.Tests/Tenants/GetTenantsByIdsTests.cs
Normal file
72
CleanArchitecture.gRPC.Tests/Tenants/GetTenantsByIdsTests.cs
Normal file
@ -0,0 +1,72 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using CleanArchitecture.gRPC.Tests.Fixtures;
|
||||
using CleanArchitecture.Proto.Tenants;
|
||||
using FluentAssertions;
|
||||
using Xunit;
|
||||
|
||||
namespace CleanArchitecture.gRPC.Tests.Tenants;
|
||||
|
||||
public sealed class GetTenantsByIdsTests : IClassFixture<TenantTestFixture>
|
||||
{
|
||||
private readonly TenantTestFixture _fixture;
|
||||
|
||||
public GetTenantsByIdsTests(TenantTestFixture fixture)
|
||||
{
|
||||
_fixture = fixture;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Should_Get_Empty_List_If_No_Ids_Are_Given()
|
||||
{
|
||||
var result = await _fixture.TenantsApiImplementation.GetByIds(
|
||||
SetupRequest(Enumerable.Empty<Guid>()),
|
||||
default!);
|
||||
|
||||
result.Tenants.Should().HaveCount(0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task? Should_Get_Requested_Tenants()
|
||||
{
|
||||
var nonExistingId = Guid.NewGuid();
|
||||
|
||||
var ids = _fixture.ExistingTenants
|
||||
.Take(2)
|
||||
.Select(tenant => tenant.Id)
|
||||
.ToList();
|
||||
|
||||
ids.Add(nonExistingId);
|
||||
|
||||
var result = await _fixture.TenantsApiImplementation.GetByIds(
|
||||
SetupRequest(ids),
|
||||
default!);
|
||||
|
||||
result.Tenants.Should().HaveCount(2);
|
||||
|
||||
foreach (var tenant in result.Tenants)
|
||||
{
|
||||
var tenantId = Guid.Parse(tenant.Id);
|
||||
|
||||
tenantId.Should().NotBe(nonExistingId);
|
||||
|
||||
var mockTenant = _fixture.ExistingTenants.First(t => t.Id == tenantId);
|
||||
|
||||
mockTenant.Should().NotBeNull();
|
||||
|
||||
tenant.Name.Should().Be(mockTenant.Name);
|
||||
}
|
||||
}
|
||||
|
||||
private static GetTenantsByIdsRequest SetupRequest(IEnumerable<Guid> ids)
|
||||
{
|
||||
var request = new GetTenantsByIdsRequest();
|
||||
|
||||
request.Ids.AddRange(ids.Select(id => id.ToString()));
|
||||
request.Ids.Add("Not a guid");
|
||||
|
||||
return request;
|
||||
}
|
||||
}
|
@ -9,11 +9,11 @@ using Xunit;
|
||||
|
||||
namespace CleanArchitecture.gRPC.Tests.Users;
|
||||
|
||||
public sealed class GetUsersByIdsTests : IClassFixture<UserTestsFixture>
|
||||
public sealed class GetUsersByIdsTests : IClassFixture<UserTestFixture>
|
||||
{
|
||||
private readonly UserTestsFixture _fixture;
|
||||
private readonly UserTestFixture _fixture;
|
||||
|
||||
public GetUsersByIdsTests(UserTestsFixture fixture)
|
||||
public GetUsersByIdsTests(UserTestFixture fixture)
|
||||
{
|
||||
_fixture = fixture;
|
||||
}
|
||||
@ -23,13 +23,13 @@ public sealed class GetUsersByIdsTests : IClassFixture<UserTestsFixture>
|
||||
{
|
||||
var result = await _fixture.UsersApiImplementation.GetByIds(
|
||||
SetupRequest(Enumerable.Empty<Guid>()),
|
||||
null!);
|
||||
default!);
|
||||
|
||||
result.Users.Should().HaveCount(0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Should_Get_Requested_Asked_Ids()
|
||||
public async Task Should_Get_Requested_Users()
|
||||
{
|
||||
var nonExistingId = Guid.NewGuid();
|
||||
|
||||
@ -40,9 +40,10 @@ public sealed class GetUsersByIdsTests : IClassFixture<UserTestsFixture>
|
||||
|
||||
ids.Add(nonExistingId);
|
||||
|
||||
// Todo: Use default instead of null everywhere
|
||||
var result = await _fixture.UsersApiImplementation.GetByIds(
|
||||
SetupRequest(ids),
|
||||
null!);
|
||||
default!);
|
||||
|
||||
result.Users.Should().HaveCount(2);
|
||||
|
||||
|
@ -2,7 +2,9 @@
|
||||
|
||||

|
||||
|
||||
This repository contains a sample API project built using the Clean Architecture principles, Onion Architecture, MediatR, and Entity Framework. The project also includes unit tests for all layers and integration tests using xUnit.
|
||||
This repository contains a sample API project built using the Clean Architecture principles, Onion Architecture, MediatR, and Entity Framework. The project also includes unit tests for all layers and integration tests using xUnit and Nsubstitute.
|
||||
|
||||
The purpose of this project is to create a clean boilerplate for an API and to show how to implement specific features.
|
||||
|
||||
## Project Structure
|
||||
The project follows the Onion Architecture, which means that the codebase is organized into layers, with the domain model at the center and the outer layers dependent on the inner layers.
|
||||
@ -20,6 +22,7 @@ The project uses the following dependencies:
|
||||
- **MediatR**: A lightweight library that provides a mediator pattern implementation for .NET.
|
||||
- **Entity Framework Core**: A modern object-relational mapper for .NET that provides data access to the application.
|
||||
- **FluentValidation**: A validation library that provides a fluent API for validating objects.
|
||||
- **gRPC**: gRPC is an open-source remote procedure call framework that enables efficient communication between distributed systems using a variety of programming languages and protocols.
|
||||
|
||||
## Running the Project
|
||||
To run the project, follow these steps:
|
||||
|
Loading…
Reference in New Issue
Block a user