feat(backend): Create Sprint 2 backend Stories and Tasks
Created detailed implementation plans for Sprint 2 backend work: Story 1: Audit Log Foundation (Phase 1) - Task 1: Design AuditLog database schema and create migration - Task 2: Create AuditLog entity and Repository - Task 3: Implement EF Core SaveChangesInterceptor - Task 4: Write unit tests for audit logging - Task 5: Integrate with ProjectManagement Module Story 2: Audit Log Core Features (Phase 2) - Task 1: Implement Changed Fields Detection (JSON Diff) - Task 2: Integrate User Context Tracking - Task 3: Add Multi-Tenant Isolation - Task 4: Implement Audit Query API - Task 5: Write Integration Tests Story 3: Sprint Management Module - Task 1: Create Sprint Aggregate Root and Domain Events - Task 2: Implement Sprint Repository and EF Core Configuration - Task 3: Create CQRS Commands and Queries - Task 4: Implement Burndown Chart Calculation - Task 5: Add SignalR Real-Time Notifications - Task 6: Write Integration Tests Total: 3 Stories, 16 Tasks, 24 Story Points (8+8+8) Estimated Duration: 10-12 days All tasks include: - Detailed technical implementation guidance - Code examples and file paths - Testing requirements (>= 90% coverage) - Performance benchmarks (< 5ms audit overhead) - Multi-tenant security validation 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
341
docs/plans/sprint_2_story_2_task_5.md
Normal file
341
docs/plans/sprint_2_story_2_task_5.md
Normal file
@@ -0,0 +1,341 @@
|
||||
---
|
||||
task_id: sprint_2_story_2_task_5
|
||||
story: sprint_2_story_2
|
||||
status: not_started
|
||||
estimated_hours: 5
|
||||
created_date: 2025-11-05
|
||||
assignee: Backend Team
|
||||
---
|
||||
|
||||
# Task 5: Write Integration Tests
|
||||
|
||||
**Story**: Story 2 - Audit Log Core Features (Phase 2)
|
||||
**Estimated**: 5 hours
|
||||
|
||||
## Description
|
||||
|
||||
Create comprehensive integration tests for all audit log features including changed fields tracking, user context, multi-tenant isolation, and query API. Target >= 90% code coverage.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] Integration tests for changed fields detection
|
||||
- [ ] Integration tests for user context tracking
|
||||
- [ ] Integration tests for multi-tenant isolation
|
||||
- [ ] Integration tests for query API endpoints
|
||||
- [ ] Test coverage >= 90%
|
||||
- [ ] All tests passing
|
||||
- [ ] Performance tests verify < 5ms overhead
|
||||
|
||||
## Implementation Details
|
||||
|
||||
**Files to Create**:
|
||||
|
||||
1. **Integration Test Base**: `colaflow-api/tests/ColaFlow.Application.IntegrationTests/AuditLog/AuditLogIntegrationTestBase.cs`
|
||||
```csharp
|
||||
public class AuditLogIntegrationTestBase : IntegrationTestBase
|
||||
{
|
||||
protected async Task<Guid> CreateTestProjectAsync(string name = "Test Project")
|
||||
{
|
||||
var command = new CreateProjectCommand
|
||||
{
|
||||
Name = name,
|
||||
Key = name.ToUpper().Replace(" ", ""),
|
||||
Description = "Test Description"
|
||||
};
|
||||
|
||||
return await Mediator.Send(command);
|
||||
}
|
||||
|
||||
protected async Task<Guid> CreateTestEpicAsync(Guid projectId, string title = "Test Epic")
|
||||
{
|
||||
var command = new CreateEpicCommand
|
||||
{
|
||||
ProjectId = projectId,
|
||||
Title = title,
|
||||
Description = "Test Epic Description"
|
||||
};
|
||||
|
||||
return await Mediator.Send(command);
|
||||
}
|
||||
|
||||
protected async Task<List<AuditLog>> GetAuditLogsForEntityAsync(string entityType, Guid entityId)
|
||||
{
|
||||
return await Context.AuditLogs
|
||||
.Include(a => a.User)
|
||||
.Where(a => a.EntityType == entityType && a.EntityId == entityId)
|
||||
.OrderByDescending(a => a.Timestamp)
|
||||
.ToListAsync();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
2. **Changed Fields Tests**: `colaflow-api/tests/ColaFlow.Application.IntegrationTests/AuditLog/ChangedFieldsTests.cs`
|
||||
```csharp
|
||||
public class ChangedFieldsTests : AuditLogIntegrationTestBase
|
||||
{
|
||||
[Fact]
|
||||
public async Task UpdateProject_ShouldLogOnlyChangedFields()
|
||||
{
|
||||
// Arrange
|
||||
var projectId = await CreateTestProjectAsync("Original Name");
|
||||
|
||||
// Act - Update only the name
|
||||
await Mediator.Send(new UpdateProjectCommand
|
||||
{
|
||||
ProjectId = projectId,
|
||||
Name = "Updated Name"
|
||||
});
|
||||
|
||||
// Assert
|
||||
var auditLogs = await GetAuditLogsForEntityAsync("Project", projectId);
|
||||
var updateLog = auditLogs.First(a => a.Action == AuditAction.Update);
|
||||
|
||||
Assert.NotNull(updateLog.NewValues);
|
||||
|
||||
// Deserialize and verify only Name field was changed
|
||||
var changedFields = JsonSerializer.Deserialize<Dictionary<string, FieldChangeDto>>(updateLog.NewValues);
|
||||
Assert.NotNull(changedFields);
|
||||
Assert.Single(changedFields); // Only one field changed
|
||||
Assert.True(changedFields.ContainsKey("Name"));
|
||||
Assert.Equal("Original Name", changedFields["Name"].OldValue?.ToString());
|
||||
Assert.Equal("Updated Name", changedFields["Name"].NewValue?.ToString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task UpdateMultipleFields_ShouldLogAllChangedFields()
|
||||
{
|
||||
// Arrange
|
||||
var projectId = await CreateTestProjectAsync();
|
||||
|
||||
// Act - Update name and description
|
||||
await Mediator.Send(new UpdateProjectCommand
|
||||
{
|
||||
ProjectId = projectId,
|
||||
Name = "New Name",
|
||||
Description = "New Description"
|
||||
});
|
||||
|
||||
// Assert
|
||||
var auditLogs = await GetAuditLogsForEntityAsync("Project", projectId);
|
||||
var updateLog = auditLogs.First(a => a.Action == AuditAction.Update);
|
||||
|
||||
var changedFields = JsonSerializer.Deserialize<Dictionary<string, FieldChangeDto>>(updateLog.NewValues);
|
||||
Assert.Equal(2, changedFields.Count); // Two fields changed
|
||||
Assert.True(changedFields.ContainsKey("Name"));
|
||||
Assert.True(changedFields.ContainsKey("Description"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CreateEntity_ShouldLogAllFields()
|
||||
{
|
||||
// Act
|
||||
var projectId = await CreateTestProjectAsync("Test Project");
|
||||
|
||||
// Assert
|
||||
var auditLogs = await GetAuditLogsForEntityAsync("Project", projectId);
|
||||
var createLog = auditLogs.First(a => a.Action == AuditAction.Create);
|
||||
|
||||
Assert.NotNull(createLog.NewValues);
|
||||
Assert.Null(createLog.OldValues); // No old values for Create
|
||||
|
||||
// Verify all fields are logged
|
||||
var fields = JsonSerializer.Deserialize<Dictionary<string, object>>(createLog.NewValues);
|
||||
Assert.True(fields.ContainsKey("Name"));
|
||||
Assert.True(fields.ContainsKey("Key"));
|
||||
Assert.True(fields.ContainsKey("Description"));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
3. **User Context Tests**: `colaflow-api/tests/ColaFlow.Application.IntegrationTests/AuditLog/UserContextTests.cs`
|
||||
```csharp
|
||||
public class UserContextTests : AuditLogIntegrationTestBase
|
||||
{
|
||||
[Fact]
|
||||
public async Task CreateProject_ShouldCaptureCurrentUserId()
|
||||
{
|
||||
// Arrange
|
||||
var userId = Guid.NewGuid();
|
||||
SetCurrentUser(userId);
|
||||
|
||||
// Act
|
||||
var projectId = await CreateTestProjectAsync();
|
||||
|
||||
// Assert
|
||||
var auditLogs = await GetAuditLogsForEntityAsync("Project", projectId);
|
||||
var createLog = auditLogs.First(a => a.Action == AuditAction.Create);
|
||||
|
||||
Assert.Equal(userId, createLog.UserId);
|
||||
Assert.NotNull(createLog.User); // User navigation property loaded
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SystemOperation_ShouldAllowNullUserId()
|
||||
{
|
||||
// Arrange
|
||||
ClearCurrentUser(); // Simulate system operation
|
||||
|
||||
// Act
|
||||
var projectId = await CreateTestProjectAsync();
|
||||
|
||||
// Assert
|
||||
var auditLogs = await GetAuditLogsForEntityAsync("Project", projectId);
|
||||
var createLog = auditLogs.First(a => a.Action == AuditAction.Create);
|
||||
|
||||
Assert.Null(createLog.UserId); // System operation
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
4. **API Tests**: `colaflow-api/tests/ColaFlow.API.IntegrationTests/AuditLog/AuditLogsControllerTests.cs`
|
||||
```csharp
|
||||
public class AuditLogsControllerTests : ApiIntegrationTestBase
|
||||
{
|
||||
[Fact]
|
||||
public async Task GetEntityAuditHistory_ShouldReturnAuditLogs()
|
||||
{
|
||||
// Arrange
|
||||
var projectId = await CreateTestProjectAsync();
|
||||
await UpdateTestProjectAsync(projectId, "Updated Name");
|
||||
|
||||
// Act
|
||||
var response = await Client.GetAsync($"/api/auditlogs/entity/Project/{projectId}");
|
||||
|
||||
// Assert
|
||||
response.EnsureSuccessStatusCode();
|
||||
var logs = await response.Content.ReadFromJsonAsync<List<AuditLogDto>>();
|
||||
|
||||
Assert.NotNull(logs);
|
||||
Assert.Equal(2, logs.Count); // Create + Update
|
||||
Assert.Contains(logs, l => l.Action == "Create");
|
||||
Assert.Contains(logs, l => l.Action == "Update");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetEntityAuditHistory_WithDateFilter_ShouldReturnFilteredResults()
|
||||
{
|
||||
// Arrange
|
||||
var projectId = await CreateTestProjectAsync();
|
||||
await Task.Delay(100);
|
||||
var filterDate = DateTime.UtcNow;
|
||||
await Task.Delay(100);
|
||||
await UpdateTestProjectAsync(projectId, "Updated");
|
||||
|
||||
// Act
|
||||
var response = await Client.GetAsync($"/api/auditlogs/entity/Project/{projectId}?fromDate={filterDate:O}");
|
||||
|
||||
// Assert
|
||||
response.EnsureSuccessStatusCode();
|
||||
var logs = await response.Content.ReadFromJsonAsync<List<AuditLogDto>>();
|
||||
|
||||
Assert.Single(logs); // Only the update after filterDate
|
||||
Assert.Equal("Update", logs[0].Action);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetEntityAuditHistory_WithLimit_ShouldRespectLimit()
|
||||
{
|
||||
// Arrange
|
||||
var projectId = await CreateTestProjectAsync();
|
||||
|
||||
// Make 10 updates
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
await UpdateTestProjectAsync(projectId, $"Update {i}");
|
||||
}
|
||||
|
||||
// Act
|
||||
var response = await Client.GetAsync($"/api/auditlogs/entity/Project/{projectId}?limit=5");
|
||||
|
||||
// Assert
|
||||
response.EnsureSuccessStatusCode();
|
||||
var logs = await response.Content.ReadFromJsonAsync<List<AuditLogDto>>();
|
||||
|
||||
Assert.Equal(5, logs.Count); // Respects limit
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetEntityAuditHistory_DifferentTenant_ShouldReturnEmpty()
|
||||
{
|
||||
// Arrange
|
||||
var tenant1Id = Guid.NewGuid();
|
||||
SetCurrentTenant(tenant1Id);
|
||||
var projectId = await CreateTestProjectAsync();
|
||||
|
||||
// Act - Switch to different tenant
|
||||
var tenant2Id = Guid.NewGuid();
|
||||
SetCurrentTenant(tenant2Id);
|
||||
var response = await Client.GetAsync($"/api/auditlogs/entity/Project/{projectId}");
|
||||
|
||||
// Assert
|
||||
response.EnsureSuccessStatusCode();
|
||||
var logs = await response.Content.ReadFromJsonAsync<List<AuditLogDto>>();
|
||||
|
||||
Assert.Empty(logs); // Different tenant should not see logs
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
5. **Performance Tests**: `colaflow-api/tests/ColaFlow.Performance.Tests/AuditLog/AuditLogPerformanceTests.cs`
|
||||
```csharp
|
||||
public class AuditLogPerformanceTests : IntegrationTestBase
|
||||
{
|
||||
[Fact]
|
||||
public async Task AuditLogging_ShouldHaveMinimalOverhead()
|
||||
{
|
||||
// Arrange
|
||||
var iterations = 100;
|
||||
var stopwatch = new Stopwatch();
|
||||
|
||||
// Act
|
||||
stopwatch.Start();
|
||||
for (int i = 0; i < iterations; i++)
|
||||
{
|
||||
await CreateTestProjectAsync($"Project {i}");
|
||||
}
|
||||
stopwatch.Stop();
|
||||
|
||||
// Assert
|
||||
var avgTime = stopwatch.ElapsedMilliseconds / (double)iterations;
|
||||
Assert.True(avgTime < 100, $"Average time {avgTime}ms exceeds 100ms target");
|
||||
|
||||
// Overhead should be < 5ms (audit logging overhead)
|
||||
// Total time includes DB write (50-70ms) + audit overhead (< 5ms)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Test Coverage Goals
|
||||
|
||||
| Component | Coverage Target |
|
||||
|-----------|----------------|
|
||||
| AuditLogInterceptor | >= 95% |
|
||||
| JsonDiffService | >= 95% |
|
||||
| AuditLogRepository | >= 90% |
|
||||
| Query Handlers | >= 90% |
|
||||
| Controllers | >= 85% |
|
||||
|
||||
## Testing Commands
|
||||
|
||||
```bash
|
||||
# Run all audit log tests
|
||||
dotnet test --filter "FullyQualifiedName~AuditLog"
|
||||
|
||||
# Run specific test file
|
||||
dotnet test --filter "FullyQualifiedName~ChangedFieldsTests"
|
||||
|
||||
# Run with coverage
|
||||
dotnet test --collect:"XPlat Code Coverage"
|
||||
```
|
||||
|
||||
## Definition of Done
|
||||
|
||||
- All test categories implemented (Changed Fields, User Context, Multi-Tenant, API, Performance)
|
||||
- >= 90% code coverage achieved
|
||||
- All tests passing
|
||||
- Performance tests verify < 5ms overhead
|
||||
- Integration with CI/CD pipeline
|
||||
|
||||
---
|
||||
|
||||
**Created**: 2025-11-05 by Backend Agent
|
||||
Reference in New Issue
Block a user