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:
415
docs/plans/sprint_2_story_3_task_6.md
Normal file
415
docs/plans/sprint_2_story_3_task_6.md
Normal 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
|
||||
Reference in New Issue
Block a user