using System.Net.Http.Json; using FluentAssertions; using Microsoft.Extensions.DependencyInjection; using ColaFlow.Modules.ProjectManagement.IntegrationTests.Infrastructure; using ColaFlow.Modules.ProjectManagement.Infrastructure.Persistence; using ColaFlow.Modules.ProjectManagement.Domain.Entities; using ColaFlow.Modules.ProjectManagement.Application.DTOs; using Microsoft.EntityFrameworkCore; namespace ColaFlow.Modules.ProjectManagement.IntegrationTests; /// /// Integration tests for AuditInterceptor /// Verifies that all Create/Update/Delete operations are automatically logged /// public class AuditInterceptorTests : IClassFixture { private readonly PMWebApplicationFactory _factory; private readonly HttpClient _client; // Test tenant and user IDs private readonly Guid _tenantId = Guid.NewGuid(); private readonly Guid _userId = Guid.NewGuid(); public AuditInterceptorTests(PMWebApplicationFactory factory) { _factory = factory; _client = _factory.CreateClient(); // Set up authentication var token = TestAuthHelper.GenerateJwtToken(_userId, _tenantId, "test-tenant", "user@test.com"); _client.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token); } [Fact] public async Task CreateProject_Should_CreateAuditLog() { // Arrange var createRequest = new { Name = "Test Project for Audit", Key = "TPFA", Description = "Testing audit logging" }; // Act var createResponse = await _client.PostAsJsonAsync("/api/v1/projects", createRequest); createResponse.EnsureSuccessStatusCode(); var project = await createResponse.Content.ReadFromJsonAsync(); var projectId = project!.Id; // Assert: Check audit log was created using var scope = _factory.Services.CreateScope(); var context = scope.ServiceProvider.GetRequiredService(); var auditLog = await context.AuditLogs .IgnoreQueryFilters() // Bypass tenant filter when querying directly in tests .Where(a => a.EntityType == "Project" && a.EntityId == projectId && a.Action == "Create") .FirstOrDefaultAsync(); auditLog.Should().NotBeNull(); auditLog!.Action.Should().Be("Create"); auditLog.EntityType.Should().Be("Project"); auditLog.EntityId.Should().Be(projectId); auditLog.TenantId.Value.Should().Be(_tenantId); auditLog.UserId.Should().NotBeNull(); auditLog.UserId!.Value.Should().Be(_userId); auditLog.Timestamp.Should().BeCloseTo(DateTime.UtcNow, TimeSpan.FromSeconds(5)); } [Fact] public async Task UpdateProject_Should_CreateAuditLog() { // Arrange: Create a project first var createResponse = await _client.PostAsJsonAsync("/api/v1/projects", new { Name = "Original Project", Key = "ORIG", Description = "Original description" }); var project = await createResponse.Content.ReadFromJsonAsync(); var projectId = project!.Id; // Act: Update the project var updateResponse = await _client.PutAsJsonAsync($"/api/v1/projects/{projectId}", new { Name = "Updated Project Name", Description = "Updated description" }); updateResponse.EnsureSuccessStatusCode(); // Assert: Check update audit log was created using var scope = _factory.Services.CreateScope(); var context = scope.ServiceProvider.GetRequiredService(); var auditLog = await context.AuditLogs .IgnoreQueryFilters() .Where(a => a.EntityType == "Project" && a.EntityId == projectId && a.Action == "Update") .FirstOrDefaultAsync(); auditLog.Should().NotBeNull(); auditLog!.Action.Should().Be("Update"); auditLog.EntityType.Should().Be("Project"); auditLog.EntityId.Should().Be(projectId); auditLog.TenantId.Value.Should().Be(_tenantId); auditLog.UserId.Should().NotBeNull(); auditLog.UserId!.Value.Should().Be(_userId); } [Fact] public async Task DeleteProject_Should_CreateAuditLog() { // Arrange: Create a project first var createResponse = await _client.PostAsJsonAsync("/api/v1/projects", new { Name = "Project To Delete", Key = "PTDEL", Description = "This project will be deleted" }); var project = await createResponse.Content.ReadFromJsonAsync(); var projectId = project!.Id; // Act: Delete the project var deleteResponse = await _client.DeleteAsync($"/api/v1/projects/{projectId}"); deleteResponse.EnsureSuccessStatusCode(); // Assert: Check delete audit log was created using var scope = _factory.Services.CreateScope(); var context = scope.ServiceProvider.GetRequiredService(); var auditLog = await context.AuditLogs .IgnoreQueryFilters() .Where(a => a.EntityType == "Project" && a.EntityId == projectId && a.Action == "Delete") .FirstOrDefaultAsync(); auditLog.Should().NotBeNull(); auditLog!.Action.Should().Be("Delete"); auditLog.EntityType.Should().Be("Project"); auditLog.EntityId.Should().Be(projectId); auditLog.TenantId.Value.Should().Be(_tenantId); } [Fact] public async Task CreateEpic_Should_CreateAuditLog() { // Arrange: Create project first var projectResponse = await _client.PostAsJsonAsync("/api/v1/projects", new { Name = "Project for Epic", Key = "PFE", Description = "Test project" }); var project = await projectResponse.Content.ReadFromJsonAsync(); var projectId = project!.Id; // Act: Create epic var epicResponse = await _client.PostAsJsonAsync("/api/v1/epics", new { ProjectId = projectId, Name = "Test Epic", Description = "Testing epic audit", CreatedBy = _userId }); epicResponse.EnsureSuccessStatusCode(); var epic = await epicResponse.Content.ReadFromJsonAsync(); var epicId = epic!.Id; // Assert: Check audit log using var scope = _factory.Services.CreateScope(); var context = scope.ServiceProvider.GetRequiredService(); var auditLog = await context.AuditLogs .IgnoreQueryFilters() .Where(a => a.EntityType == "Epic" && a.EntityId == epicId && a.Action == "Create") .FirstOrDefaultAsync(); auditLog.Should().NotBeNull(); auditLog!.Action.Should().Be("Create"); auditLog.EntityType.Should().Be("Epic"); auditLog.TenantId.Value.Should().Be(_tenantId); } [Fact] public async Task CreateStory_Should_CreateAuditLog() { // Arrange: Create project and epic var projectResponse = await _client.PostAsJsonAsync("/api/v1/projects", new { Name = "Project for Story", Key = "PFS", Description = "Test" }); var project = await projectResponse.Content.ReadFromJsonAsync(); var epicResponse = await _client.PostAsJsonAsync("/api/v1/epics", new { ProjectId = project!.Id, Name = "Epic for Story", Description = "Test", CreatedBy = _userId }); var epic = await epicResponse.Content.ReadFromJsonAsync(); // Act: Create story var storyResponse = await _client.PostAsJsonAsync("/api/v1/stories", new { EpicId = epic!.Id, Title = "Test Story", Description = "Testing story audit", Priority = "Medium", CreatedBy = _userId }); storyResponse.EnsureSuccessStatusCode(); var story = await storyResponse.Content.ReadFromJsonAsync(); var storyId = story!.Id; // Assert: Check audit log using var scope = _factory.Services.CreateScope(); var context = scope.ServiceProvider.GetRequiredService(); var auditLog = await context.AuditLogs .IgnoreQueryFilters() .Where(a => a.EntityType == "Story" && a.EntityId == storyId && a.Action == "Create") .FirstOrDefaultAsync(); auditLog.Should().NotBeNull(); auditLog!.EntityType.Should().Be("Story"); auditLog.TenantId.Value.Should().Be(_tenantId); } [Fact] public async Task CreateTask_Should_CreateAuditLog() { // Arrange: Create project, epic, and story var projectResponse = await _client.PostAsJsonAsync("/api/v1/projects", new { Name = "Project for Task", Key = "PFT", Description = "Test" }); var project = await projectResponse.Content.ReadFromJsonAsync(); var epicResponse = await _client.PostAsJsonAsync("/api/v1/epics", new { ProjectId = project!.Id, Name = "Epic for Task", Description = "Test", CreatedBy = _userId }); var epic = await epicResponse.Content.ReadFromJsonAsync(); var storyResponse = await _client.PostAsJsonAsync("/api/v1/stories", new { EpicId = epic!.Id, Title = "Story for Task", Description = "Test", Priority = "Medium", CreatedBy = _userId }); var story = await storyResponse.Content.ReadFromJsonAsync(); // Act: Create task var taskResponse = await _client.PostAsJsonAsync("/api/v1/tasks", new { StoryId = story!.Id, Title = "Test Task", Description = "Testing task audit", Priority = "High", CreatedBy = _userId }); taskResponse.EnsureSuccessStatusCode(); var task = await taskResponse.Content.ReadFromJsonAsync(); var taskId = task!.Id; // Assert: Check audit log using var scope = _factory.Services.CreateScope(); var context = scope.ServiceProvider.GetRequiredService(); var auditLog = await context.AuditLogs .IgnoreQueryFilters() .Where(a => a.EntityType == "WorkTask" && a.EntityId == taskId && a.Action == "Create") .FirstOrDefaultAsync(); auditLog.Should().NotBeNull(); auditLog!.EntityType.Should().Be("WorkTask"); auditLog.TenantId.Value.Should().Be(_tenantId); } [Fact] public async Task AuditLog_Should_NotAuditItself() { // Arrange & Act: Create a project (which triggers audit log creation) var createResponse = await _client.PostAsJsonAsync("/api/v1/projects", new { Name = "Test Recursion", Key = "TREC", Description = "Test" }); createResponse.EnsureSuccessStatusCode(); // Assert: Verify no AuditLog entity has been audited using var scope = _factory.Services.CreateScope(); var context = scope.ServiceProvider.GetRequiredService(); var selfAudit = await context.AuditLogs .IgnoreQueryFilters() .Where(a => a.EntityType == "AuditLog") .FirstOrDefaultAsync(); selfAudit.Should().BeNull("AuditLog should not audit itself to prevent recursion"); } [Fact] public async Task MultipleOperations_Should_CreateMultipleAuditLogs() { // Act: Create, update, and delete a project var createResponse = await _client.PostAsJsonAsync("/api/v1/projects", new { Name = "Multi-Op Project", Key = "MOP", Description = "Test" }); var project = await createResponse.Content.ReadFromJsonAsync(); var projectId = project!.Id; await _client.PutAsJsonAsync($"/api/v1/projects/{projectId}", new { Name = "Updated Multi-Op", Description = "Updated" }); await _client.DeleteAsync($"/api/v1/projects/{projectId}"); // Assert: Verify all 3 audit logs exist using var scope = _factory.Services.CreateScope(); var context = scope.ServiceProvider.GetRequiredService(); var auditLogs = await context.AuditLogs .IgnoreQueryFilters() .Where(a => a.EntityType == "Project" && a.EntityId == projectId) .OrderBy(a => a.Timestamp) .ToListAsync(); auditLogs.Should().HaveCount(3); auditLogs[0].Action.Should().Be("Create"); auditLogs[1].Action.Should().Be("Update"); auditLogs[2].Action.Should().Be("Delete"); } [Fact] public async Task AuditLog_Should_IsolateTenants() { // Arrange: Tenant 1 creates a project var tenant1Id = Guid.NewGuid(); var tenant1Token = TestAuthHelper.GenerateJwtToken(_userId, tenant1Id, "tenant1", "user@tenant1.com"); _client.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", tenant1Token); var response1 = await _client.PostAsJsonAsync("/api/v1/projects", new { Name = "Tenant 1 Project", Key = "T1P", Description = "Test" }); var project1 = await response1.Content.ReadFromJsonAsync(); var project1Id = project1!.Id; // Arrange: Tenant 2 creates a project var tenant2Id = Guid.NewGuid(); var tenant2Token = TestAuthHelper.GenerateJwtToken(_userId, tenant2Id, "tenant2", "user@tenant2.com"); _client.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", tenant2Token); var response2 = await _client.PostAsJsonAsync("/api/v1/projects", new { Name = "Tenant 2 Project", Key = "T2P", Description = "Test" }); // Assert: Verify audit logs are isolated by tenant using var scope = _factory.Services.CreateScope(); var context = scope.ServiceProvider.GetRequiredService(); // Bypass query filter to see all audit logs var allAuditLogs = await context.AuditLogs .IgnoreQueryFilters() .Where(a => a.EntityType == "Project") .ToListAsync(); var tenant1Logs = allAuditLogs.Where(a => a.TenantId.Value == tenant1Id).ToList(); var tenant2Logs = allAuditLogs.Where(a => a.TenantId.Value == tenant2Id).ToList(); tenant1Logs.Should().HaveCountGreaterOrEqualTo(1); tenant2Logs.Should().HaveCountGreaterOrEqualTo(1); tenant1Logs.All(a => a.TenantId.Value == tenant1Id).Should().BeTrue(); tenant2Logs.All(a => a.TenantId.Value == tenant2Id).Should().BeTrue(); } }