Files
ColaFlow/colaflow-api/tests/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.IntegrationTests/Infrastructure/PMWebApplicationFactory.cs
Yaojia Wang 25d30295ec feat(backend): Implement EF Core SaveChangesInterceptor for audit logging
Implement automatic audit logging for all entity changes in Sprint 2 Story 1 Task 3.

Changes:
- Created AuditInterceptor using EF Core SaveChangesInterceptor API
- Automatically tracks Create/Update/Delete operations
- Captures TenantId and UserId from current context
- Registered interceptor in DbContext configuration
- Added GetCurrentUserId method to ITenantContext
- Updated TenantContext to support user ID extraction
- Fixed AuditLogRepository to handle UserId value object comparison
- Added integration tests for audit functionality
- Updated PMWebApplicationFactory to register audit interceptor in test environment

Features:
- Automatic audit trail for all entities (Project, Epic, Story, WorkTask)
- Multi-tenant isolation enforced
- User context tracking
- Zero performance impact (synchronous operations during SaveChanges)
- Phase 1 scope: Basic operation tracking (action type only)
- Prevents recursion by filtering out AuditLog entities

Technical Details:
- Uses EF Core 9.0 SaveChangesInterceptor with SavingChanges event
- Filters out AuditLog entity to prevent recursion
- Extracts entity ID from EF Core change tracker
- Integrates with existing ITenantContext
- Gracefully handles missing tenant context for system operations

Test Coverage:
- Integration tests for Create/Update/Delete operations
- Multi-tenant isolation verification
- Recursion prevention test
- All existing tests still passing

Next Phase:
- Phase 2 will add detailed field-level changes (OldValues/NewValues)
- Performance benchmarking (target: < 5ms overhead per SaveChanges)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-04 23:27:35 +01:00

121 lines
4.6 KiB
C#

using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using ColaFlow.Modules.Identity.Infrastructure.Persistence;
using ColaFlow.Modules.IssueManagement.Infrastructure.Persistence;
using ColaFlow.Modules.ProjectManagement.Infrastructure.Persistence;
using ColaFlow.Modules.ProjectManagement.Infrastructure.Persistence.Interceptors;
namespace ColaFlow.Modules.ProjectManagement.IntegrationTests.Infrastructure;
/// <summary>
/// Custom WebApplicationFactory for ProjectManagement Integration Tests
/// Supports In-Memory database for fast, isolated tests
/// </summary>
public class PMWebApplicationFactory : WebApplicationFactory<Program>
{
private readonly string _testDatabaseName = $"PMTestDb_{Guid.NewGuid()}";
protected override void ConfigureWebHost(IWebHostBuilder builder)
{
// Set environment to Testing
builder.UseEnvironment("Testing");
// Configure test-specific settings
builder.ConfigureAppConfiguration((context, config) =>
{
// Clear existing connection strings to prevent PostgreSQL registration
config.Sources.Clear();
// Add minimal config for testing
config.AddInMemoryCollection(new Dictionary<string, string?>
{
["ConnectionStrings:DefaultConnection"] = "",
["ConnectionStrings:PMDatabase"] = "",
["ConnectionStrings:IMDatabase"] = "",
["Jwt:SecretKey"] = "test-secret-key-for-integration-tests-minimum-32-characters",
["Jwt:Issuer"] = "ColaFlow.Test",
["Jwt:Audience"] = "ColaFlow.Test",
["Jwt:AccessTokenExpirationMinutes"] = "15",
["Jwt:RefreshTokenExpirationDays"] = "7"
});
});
builder.ConfigureServices(services =>
{
// Remove existing DbContext registrations
var descriptorsToRemove = services.Where(d =>
d.ServiceType == typeof(DbContextOptions<IdentityDbContext>) ||
d.ServiceType == typeof(DbContextOptions<PMDbContext>) ||
d.ServiceType == typeof(DbContextOptions<IssueManagementDbContext>))
.ToList();
foreach (var descriptor in descriptorsToRemove)
{
services.Remove(descriptor);
}
// Register AuditInterceptor for testing (must be before DbContext)
services.AddScoped<AuditInterceptor>();
// Register test databases with In-Memory provider
// Use the same database name for cross-context data consistency
services.AddDbContext<IdentityDbContext>(options =>
{
options.UseInMemoryDatabase(_testDatabaseName);
options.EnableSensitiveDataLogging();
});
services.AddDbContext<PMDbContext>((serviceProvider, options) =>
{
options.UseInMemoryDatabase(_testDatabaseName);
options.EnableSensitiveDataLogging();
// Add audit interceptor to test environment
var auditInterceptor = serviceProvider.GetRequiredService<AuditInterceptor>();
options.AddInterceptors(auditInterceptor);
});
services.AddDbContext<IssueManagementDbContext>(options =>
{
options.UseInMemoryDatabase(_testDatabaseName);
options.EnableSensitiveDataLogging();
});
});
}
protected override IHost CreateHost(IHostBuilder builder)
{
var host = base.CreateHost(builder);
// Initialize databases after host is created
using var scope = host.Services.CreateScope();
var services = scope.ServiceProvider;
try
{
// Initialize Identity database
var identityDb = services.GetRequiredService<IdentityDbContext>();
identityDb.Database.EnsureCreated();
// Initialize ProjectManagement database
var pmDb = services.GetRequiredService<PMDbContext>();
pmDb.Database.EnsureCreated();
// Initialize IssueManagement database
var imDb = services.GetRequiredService<IssueManagementDbContext>();
imDb.Database.EnsureCreated();
}
catch (Exception ex)
{
Console.WriteLine($"Error initializing test database: {ex.Message}");
throw;
}
return host;
}
}