Add complete domain events dispatching infrastructure and critical event handlers for Identity module. Changes: - Added IMediator injection to IdentityDbContext - Implemented SaveChangesAsync override to dispatch domain events before persisting - Made DomainEvent base class implement INotification (added MediatR.Contracts dependency) - Created 3 new domain events: UserRoleAssignedEvent, UserRemovedFromTenantEvent, UserLoggedInEvent - Implemented 4 event handlers with structured logging: - UserRoleAssignedEventHandler (audit log, cache invalidation placeholder) - UserRemovedFromTenantEventHandler (notification placeholder) - UserLoggedInEventHandler (login tracking placeholder) - TenantCreatedEventHandler (welcome email placeholder) - Updated unit tests to inject mock IMediator into IdentityDbContext Technical Details: - Domain events are now published via MediatR within the same transaction - Events are dispatched BEFORE SaveChangesAsync to ensure atomicity - Event handlers auto-registered by MediatR assembly scanning - All handlers include structured logging for observability Next Steps (Phase 3): - Update command handlers to raise new events (UserLoggedInEvent, UserRoleAssignedEvent) - Add event raising logic to User/Tenant aggregates - Implement audit logging persistence (currently just logging) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
164 lines
4.9 KiB
C#
164 lines
4.9 KiB
C#
using ColaFlow.Modules.Identity.Domain.Aggregates.Tenants;
|
|
using ColaFlow.Modules.Identity.Infrastructure.Persistence;
|
|
using ColaFlow.Modules.Identity.Infrastructure.Persistence.Repositories;
|
|
using ColaFlow.Modules.Identity.Infrastructure.Services;
|
|
using FluentAssertions;
|
|
using MediatR;
|
|
using Microsoft.EntityFrameworkCore;
|
|
using Moq;
|
|
|
|
namespace ColaFlow.Modules.Identity.Infrastructure.Tests.Repositories;
|
|
|
|
public class TenantRepositoryTests : IDisposable
|
|
{
|
|
private readonly IdentityDbContext _context;
|
|
private readonly TenantRepository _repository;
|
|
|
|
public TenantRepositoryTests()
|
|
{
|
|
var options = new DbContextOptionsBuilder<IdentityDbContext>()
|
|
.UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString())
|
|
.Options;
|
|
|
|
var mockTenantContext = new Mock<ITenantContext>();
|
|
mockTenantContext.Setup(x => x.IsSet).Returns(false);
|
|
|
|
var mockMediator = new Mock<IMediator>();
|
|
|
|
_context = new IdentityDbContext(options, mockTenantContext.Object, mockMediator.Object);
|
|
_repository = new TenantRepository(_context);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task AddAsync_ShouldPersistTenant()
|
|
{
|
|
// Arrange
|
|
var tenant = Tenant.Create(
|
|
TenantName.Create("Test Company"),
|
|
TenantSlug.Create("test-company"),
|
|
SubscriptionPlan.Professional);
|
|
|
|
// Act
|
|
await _repository.AddAsync(tenant);
|
|
|
|
// Assert
|
|
var retrieved = await _repository.GetByIdAsync(TenantId.Create(tenant.Id));
|
|
retrieved.Should().NotBeNull();
|
|
retrieved!.Name.Value.Should().Be("Test Company");
|
|
retrieved.Slug.Value.Should().Be("test-company");
|
|
retrieved.Plan.Should().Be(SubscriptionPlan.Professional);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task GetBySlugAsync_ShouldReturnTenant()
|
|
{
|
|
// Arrange
|
|
var slug = TenantSlug.Create("acme-corp");
|
|
var tenant = Tenant.Create(
|
|
TenantName.Create("Acme Corp"),
|
|
slug,
|
|
SubscriptionPlan.Enterprise);
|
|
await _repository.AddAsync(tenant);
|
|
|
|
// Act
|
|
var retrieved = await _repository.GetBySlugAsync(slug);
|
|
|
|
// Assert
|
|
retrieved.Should().NotBeNull();
|
|
retrieved!.Slug.Value.Should().Be("acme-corp");
|
|
retrieved.Name.Value.Should().Be("Acme Corp");
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ExistsBySlugAsync_ShouldReturnTrue_WhenSlugExists()
|
|
{
|
|
// Arrange
|
|
var slug = TenantSlug.Create("unique-slug");
|
|
var tenant = Tenant.Create(
|
|
TenantName.Create("Unique Company"),
|
|
slug,
|
|
SubscriptionPlan.Free);
|
|
await _repository.AddAsync(tenant);
|
|
|
|
// Act
|
|
var exists = await _repository.ExistsBySlugAsync(slug);
|
|
|
|
// Assert
|
|
exists.Should().BeTrue();
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ExistsBySlugAsync_ShouldReturnFalse_WhenSlugDoesNotExist()
|
|
{
|
|
// Arrange
|
|
var slug = TenantSlug.Create("non-existent");
|
|
|
|
// Act
|
|
var exists = await _repository.ExistsBySlugAsync(slug);
|
|
|
|
// Assert
|
|
exists.Should().BeFalse();
|
|
}
|
|
|
|
[Fact]
|
|
public async Task UpdateAsync_ShouldModifyTenant()
|
|
{
|
|
// Arrange
|
|
var tenant = Tenant.Create(
|
|
TenantName.Create("Original Name"),
|
|
TenantSlug.Create("original-slug"),
|
|
SubscriptionPlan.Free);
|
|
await _repository.AddAsync(tenant);
|
|
|
|
// Act
|
|
tenant.UpdateName(TenantName.Create("Updated Name"));
|
|
await _repository.UpdateAsync(tenant);
|
|
|
|
// Assert
|
|
var retrieved = await _repository.GetByIdAsync(TenantId.Create(tenant.Id));
|
|
retrieved.Should().NotBeNull();
|
|
retrieved!.Name.Value.Should().Be("Updated Name");
|
|
}
|
|
|
|
[Fact]
|
|
public async Task GetAllAsync_ShouldReturnAllTenants()
|
|
{
|
|
// Arrange
|
|
var tenant1 = Tenant.Create(TenantName.Create("Tenant 1"), TenantSlug.Create("tenant-1"), SubscriptionPlan.Free);
|
|
var tenant2 = Tenant.Create(TenantName.Create("Tenant 2"), TenantSlug.Create("tenant-2"), SubscriptionPlan.Starter);
|
|
await _repository.AddAsync(tenant1);
|
|
await _repository.AddAsync(tenant2);
|
|
|
|
// Act
|
|
var allTenants = await _repository.GetAllAsync();
|
|
|
|
// Assert
|
|
allTenants.Should().HaveCount(2);
|
|
allTenants.Should().Contain(t => t.Name.Value == "Tenant 1");
|
|
allTenants.Should().Contain(t => t.Name.Value == "Tenant 2");
|
|
}
|
|
|
|
[Fact]
|
|
public async Task DeleteAsync_ShouldRemoveTenant()
|
|
{
|
|
// Arrange
|
|
var tenant = Tenant.Create(
|
|
TenantName.Create("To Delete"),
|
|
TenantSlug.Create("to-delete"),
|
|
SubscriptionPlan.Free);
|
|
await _repository.AddAsync(tenant);
|
|
|
|
// Act
|
|
await _repository.DeleteAsync(tenant);
|
|
|
|
// Assert
|
|
var retrieved = await _repository.GetByIdAsync(TenantId.Create(tenant.Id));
|
|
retrieved.Should().BeNull();
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
_context.Dispose();
|
|
}
|
|
}
|