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:
Yaojia Wang
2025-11-04 22:56:31 +01:00
parent d6cf86a4da
commit ebb56cc9f8
19 changed files with 4030 additions and 0 deletions

View File

@@ -0,0 +1,415 @@
---
task_id: sprint_2_story_3_task_6
story: sprint_2_story_3
status: not_started
estimated_hours: 5
created_date: 2025-11-05
assignee: Backend Team
---
# Task 6: Write Integration Tests
**Story**: Story 3 - Sprint Management Module
**Estimated**: 5 hours
## Description
Create comprehensive integration tests for Sprint management functionality including CRUD operations, status transitions, burndown calculation, multi-tenant isolation, and SignalR notifications.
## Acceptance Criteria
- [ ] Integration tests for all 9 API endpoints
- [ ] Tests for status transitions (Planned → Active → Completed)
- [ ] Tests for business rule violations
- [ ] Multi-tenant isolation tests
- [ ] Burndown calculation tests
- [ ] SignalR notification tests
- [ ] Test coverage >= 90%
- [ ] All tests passing
## Implementation Details
**Files to Create**:
1. **Integration Test Base**: `colaflow-api/tests/ColaFlow.Application.IntegrationTests/Sprints/SprintIntegrationTestBase.cs`
```csharp
public class SprintIntegrationTestBase : IntegrationTestBase
{
protected async Task<Guid> CreateTestSprintAsync(
Guid? projectId = null,
string name = "Test Sprint",
int durationDays = 14)
{
var command = new CreateSprintCommand
{
ProjectId = projectId ?? TestProjectId,
Name = name,
Goal = "Test Goal",
StartDate = DateTime.UtcNow,
EndDate = DateTime.UtcNow.AddDays(durationDays)
};
return await Mediator.Send(command);
}
protected async Task<Sprint> GetSprintAsync(Guid sprintId)
{
return await Context.Sprints
.Include(s => s.Project)
.Include(s => s.Tasks)
.FirstAsync(s => s.Id == sprintId);
}
}
```
2. **CRUD Tests**: `colaflow-api/tests/ColaFlow.Application.IntegrationTests/Sprints/SprintCrudTests.cs`
```csharp
public class SprintCrudTests : SprintIntegrationTestBase
{
[Fact]
public async Task CreateSprint_ShouldCreateValidSprint()
{
// Arrange
var command = new CreateSprintCommand
{
ProjectId = TestProjectId,
Name = "Sprint 1",
Goal = "Complete Feature X",
StartDate = DateTime.UtcNow,
EndDate = DateTime.UtcNow.AddDays(14)
};
// Act
var sprintId = await Mediator.Send(command);
// Assert
var sprint = await GetSprintAsync(sprintId);
Assert.NotNull(sprint);
Assert.Equal("Sprint 1", sprint.Name);
Assert.Equal(SprintStatus.Planned, sprint.Status);
Assert.Equal(TenantId, sprint.TenantId);
}
[Fact]
public async Task CreateSprint_ShouldThrowException_WhenEndDateBeforeStartDate()
{
// Arrange
var command = new CreateSprintCommand
{
ProjectId = TestProjectId,
Name = "Invalid Sprint",
StartDate = DateTime.UtcNow,
EndDate = DateTime.UtcNow.AddDays(-1)
};
// Act & Assert
await Assert.ThrowsAsync<ValidationException>(() => Mediator.Send(command));
}
[Fact]
public async Task UpdateSprint_ShouldUpdateSprintDetails()
{
// Arrange
var sprintId = await CreateTestSprintAsync(name: "Original Name");
var command = new UpdateSprintCommand
{
SprintId = sprintId,
Name = "Updated Name",
Goal = "Updated Goal",
StartDate = DateTime.UtcNow,
EndDate = DateTime.UtcNow.AddDays(21)
};
// Act
await Mediator.Send(command);
// Assert
var sprint = await GetSprintAsync(sprintId);
Assert.Equal("Updated Name", sprint.Name);
Assert.Equal("Updated Goal", sprint.Goal);
}
[Fact]
public async Task DeleteSprint_ShouldRemoveSprint()
{
// Arrange
var sprintId = await CreateTestSprintAsync();
// Act
await Mediator.Send(new DeleteSprintCommand(sprintId));
// Assert
var sprint = await Context.Sprints.FindAsync(sprintId);
Assert.Null(sprint);
}
[Fact]
public async Task GetSprintById_ShouldReturnSprintWithStatistics()
{
// Arrange
var sprintId = await CreateTestSprintAsync();
// Add some tasks
await CreateTestTaskInSprintAsync(sprintId, storyPoints: 5);
await CreateTestTaskInSprintAsync(sprintId, storyPoints: 8);
// Act
var dto = await Mediator.Send(new GetSprintByIdQuery(sprintId));
// Assert
Assert.NotNull(dto);
Assert.Equal(sprintId, dto.Id);
Assert.Equal(2, dto.TotalTasks);
Assert.Equal(13, dto.TotalStoryPoints);
}
}
```
3. **Status Transition Tests**: `colaflow-api/tests/ColaFlow.Application.IntegrationTests/Sprints/SprintStatusTests.cs`
```csharp
public class SprintStatusTests : SprintIntegrationTestBase
{
[Fact]
public async Task StartSprint_ShouldChangeStatusToActive()
{
// Arrange
var sprintId = await CreateTestSprintAsync();
// Act
await Mediator.Send(new StartSprintCommand(sprintId));
// Assert
var sprint = await GetSprintAsync(sprintId);
Assert.Equal(SprintStatus.Active, sprint.Status);
}
[Fact]
public async Task StartSprint_ShouldThrowException_WhenAlreadyStarted()
{
// Arrange
var sprintId = await CreateTestSprintAsync();
await Mediator.Send(new StartSprintCommand(sprintId));
// Act & Assert
await Assert.ThrowsAsync<InvalidOperationException>(
() => Mediator.Send(new StartSprintCommand(sprintId))
);
}
[Fact]
public async Task CompleteSprint_ShouldChangeStatusToCompleted()
{
// Arrange
var sprintId = await CreateTestSprintAsync();
await Mediator.Send(new StartSprintCommand(sprintId));
// Act
await Mediator.Send(new CompleteSprintCommand(sprintId));
// Assert
var sprint = await GetSprintAsync(sprintId);
Assert.Equal(SprintStatus.Completed, sprint.Status);
}
[Fact]
public async Task CompleteSprint_ShouldThrowException_WhenNotActive()
{
// Arrange
var sprintId = await CreateTestSprintAsync(); // Status = Planned
// Act & Assert
await Assert.ThrowsAsync<InvalidOperationException>(
() => Mediator.Send(new CompleteSprintCommand(sprintId))
);
}
[Fact]
public async Task UpdateSprint_ShouldThrowException_WhenCompleted()
{
// Arrange
var sprintId = await CreateTestSprintAsync();
await Mediator.Send(new StartSprintCommand(sprintId));
await Mediator.Send(new CompleteSprintCommand(sprintId));
var command = new UpdateSprintCommand
{
SprintId = sprintId,
Name = "New Name",
StartDate = DateTime.UtcNow,
EndDate = DateTime.UtcNow.AddDays(14)
};
// Act & Assert
await Assert.ThrowsAsync<InvalidOperationException>(() => Mediator.Send(command));
}
}
```
4. **Multi-Tenant Tests**: `colaflow-api/tests/ColaFlow.Application.IntegrationTests/Sprints/SprintMultiTenantTests.cs`
```csharp
public class SprintMultiTenantTests : SprintIntegrationTestBase
{
[Fact]
public async Task GetSprintById_ShouldOnlyReturnCurrentTenantSprint()
{
// Arrange
var tenant1Id = Guid.NewGuid();
var tenant2Id = Guid.NewGuid();
SetCurrentTenant(tenant1Id);
var sprint1Id = await CreateTestSprintAsync();
SetCurrentTenant(tenant2Id);
// Act & Assert
await Assert.ThrowsAsync<NotFoundException>(
() => Mediator.Send(new GetSprintByIdQuery(sprint1Id))
);
}
[Fact]
public async Task GetSprintsByProjectId_ShouldFilterByTenant()
{
// Arrange
var tenant1Id = Guid.NewGuid();
var tenant2Id = Guid.NewGuid();
SetCurrentTenant(tenant1Id);
var project1Id = await CreateTestProjectAsync();
await CreateTestSprintAsync(project1Id, "Tenant 1 Sprint");
SetCurrentTenant(tenant2Id);
var project2Id = await CreateTestProjectAsync();
await CreateTestSprintAsync(project2Id, "Tenant 2 Sprint");
// Act
var sprints = await Mediator.Send(new GetSprintsByProjectIdQuery(project2Id));
// Assert
Assert.Single(sprints);
Assert.Equal("Tenant 2 Sprint", sprints[0].Name);
}
}
```
5. **Burndown Tests**: `colaflow-api/tests/ColaFlow.Application.IntegrationTests/Sprints/SprintBurndownTests.cs`
```csharp
public class SprintBurndownTests : SprintIntegrationTestBase
{
[Fact]
public async Task GetSprintBurndown_ShouldCalculateIdealBurndown()
{
// Arrange
var sprintId = await CreateTestSprintAsync(durationDays: 14);
// Act
var burndown = await Mediator.Send(new GetSprintBurndownQuery(sprintId));
// Assert
Assert.NotNull(burndown.IdealBurndown);
Assert.Equal(15, burndown.IdealBurndown.Count); // 14 days + 1 (start day)
Assert.True(burndown.IdealBurndown.First().StoryPoints >= burndown.IdealBurndown.Last().StoryPoints);
}
[Fact]
public async Task GetSprintBurndown_ShouldCalculateActualBurndown()
{
// Arrange
var sprintId = await CreateTestSprintAsync();
// Add tasks
await CreateTestTaskInSprintAsync(sprintId, storyPoints: 5, status: WorkTaskStatus.Done);
await CreateTestTaskInSprintAsync(sprintId, storyPoints: 8, status: WorkTaskStatus.InProgress);
await CreateTestTaskInSprintAsync(sprintId, storyPoints: 3, status: WorkTaskStatus.Todo);
// Act
var burndown = await Mediator.Send(new GetSprintBurndownQuery(sprintId));
// Assert
Assert.Equal(16, burndown.TotalStoryPoints);
Assert.Equal(11, burndown.RemainingStoryPoints); // 8 + 3
Assert.Equal(31.25, burndown.CompletionPercentage, 2); // 5/16 = 31.25%
}
}
```
6. **API Tests**: `colaflow-api/tests/ColaFlow.API.IntegrationTests/Sprints/SprintsControllerTests.cs`
```csharp
public class SprintsControllerTests : ApiIntegrationTestBase
{
[Fact]
public async Task CreateSprint_ShouldReturn201Created()
{
// Arrange
var command = new CreateSprintCommand
{
ProjectId = TestProjectId,
Name = "Sprint 1",
StartDate = DateTime.UtcNow,
EndDate = DateTime.UtcNow.AddDays(14)
};
// Act
var response = await Client.PostAsJsonAsync("/api/sprints", command);
// Assert
response.StatusCode.ShouldBe(HttpStatusCode.Created);
var sprintId = await response.Content.ReadFromJsonAsync<Guid>();
Assert.NotEqual(Guid.Empty, sprintId);
}
[Fact]
public async Task GetSprints_WithProjectIdFilter_ShouldReturnFilteredSprints()
{
// Arrange
var projectId = await CreateTestProjectAsync();
await CreateTestSprintAsync(projectId, "Sprint 1");
await CreateTestSprintAsync(projectId, "Sprint 2");
// Act
var response = await Client.GetAsync($"/api/sprints?projectId={projectId}");
// Assert
response.EnsureSuccessStatusCode();
var sprints = await response.Content.ReadFromJsonAsync<List<SprintDto>>();
Assert.Equal(2, sprints.Count);
}
}
```
## Test Coverage Goals
| Component | Coverage Target |
|-----------|----------------|
| Sprint Entity | >= 95% |
| Sprint Repository | >= 90% |
| Command Handlers | >= 90% |
| Query Handlers | >= 90% |
| Event Handlers | >= 85% |
| Controllers | >= 85% |
## Testing Commands
```bash
# Run all sprint tests
dotnet test --filter "FullyQualifiedName~Sprint"
# Run specific test file
dotnet test --filter "FullyQualifiedName~SprintCrudTests"
# Run with coverage
dotnet test --collect:"XPlat Code Coverage"
```
## Definition of Done
- All test categories implemented (CRUD, Status, Multi-Tenant, Burndown, API)
- >= 90% code coverage achieved
- All tests passing
- Integration with CI/CD pipeline
- Performance tests verify acceptable response times
---
**Created**: 2025-11-05 by Backend Agent