---
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
