Files
ColaFlow/colaflow-api/tests/Modules/Identity/ColaFlow.Modules.Identity.Infrastructure.Tests/Repositories/TenantRepositoryTests.cs
Yaojia Wang 0e503176c4 feat(backend): Implement Domain Events infrastructure and handlers
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>
2025-11-03 20:33:36 +01:00

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();
}
}