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