using System.Net; 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.Application.DTOs; using ColaFlow.Modules.ProjectManagement.Application.Commands.CreateProject; using ColaFlow.Modules.ProjectManagement.Application.Commands.CreateSprint; using ColaFlow.Modules.ProjectManagement.Application.Commands.UpdateSprint; using ColaFlow.Modules.ProjectManagement.Application.Commands.DeleteSprint; using ColaFlow.Modules.ProjectManagement.Application.Commands.StartSprint; using ColaFlow.Modules.ProjectManagement.Application.Commands.CompleteSprint; using ColaFlow.Modules.ProjectManagement.Application.Commands.AddTaskToSprint; using ColaFlow.Modules.ProjectManagement.Application.Commands.RemoveTaskFromSprint; using ColaFlow.Modules.ProjectManagement.Application.Queries.GetSprintBurndown; using Microsoft.EntityFrameworkCore; using MediatR; namespace ColaFlow.Modules.ProjectManagement.IntegrationTests; /// /// Comprehensive Integration Tests for Sprint Management (Sprint 2 Story 3 Task 6) /// Tests all 11 API endpoints, status transitions, business rules, multi-tenant isolation, and burndown calculation /// public class SprintIntegrationTests : IClassFixture { private readonly PMWebApplicationFactory _factory; private readonly HttpClient _client; private readonly Guid _tenantId = Guid.NewGuid(); private readonly Guid _userId = Guid.NewGuid(); public SprintIntegrationTests(PMWebApplicationFactory factory) { _factory = factory; _client = _factory.CreateClient(); var token = TestAuthHelper.GenerateJwtToken(_userId, _tenantId, "test-tenant", "user@test.com"); _client.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token); } // ==================== CRUD Tests ==================== [Fact] public async Task CreateSprint_WithValidData_ShouldSucceed() { // Arrange var project = await CreateTestProjectAsync("CRUD Project", "CRUD"); var startDate = DateTime.UtcNow; var endDate = startDate.AddDays(14); // Act var response = await _client.PostAsJsonAsync("/api/v1/sprints", new { ProjectId = project.Id, Name = "Sprint 1", Goal = "Complete user authentication", StartDate = startDate, EndDate = endDate, CreatedBy = _userId }); // Assert response.StatusCode.Should().Be(HttpStatusCode.Created); var sprint = await response.Content.ReadFromJsonAsync(); sprint.Should().NotBeNull(); sprint!.Name.Should().Be("Sprint 1"); sprint.Goal.Should().Be("Complete user authentication"); sprint.Status.Should().Be("Planned"); sprint.ProjectId.Should().Be(project.Id); } [Fact] public async Task CreateSprint_WithInvalidDateRange_ShouldFail() { // Arrange var project = await CreateTestProjectAsync("Invalid Project", "INV"); // Act var response = await _client.PostAsJsonAsync("/api/v1/sprints", new { ProjectId = project.Id, Name = "Invalid Sprint", StartDate = DateTime.UtcNow, EndDate = DateTime.UtcNow.AddDays(-1), // End date before start date CreatedBy = _userId }); // Assert response.StatusCode.Should().Be(HttpStatusCode.BadRequest); } [Fact] public async Task UpdateSprint_WithValidData_ShouldSucceed() { // Arrange var sprint = await CreateTestSprintAsync("Original Sprint"); // Act var response = await _client.PutAsync($"/api/v1/sprints/{sprint.Id}", JsonContent.Create(new { SprintId = sprint.Id, Name = "Updated Sprint", Goal = "Updated goal", StartDate = DateTime.UtcNow, EndDate = DateTime.UtcNow.AddDays(21) })); // Assert response.StatusCode.Should().Be(HttpStatusCode.NoContent); // Verify update var getResponse = await _client.GetAsync($"/api/v1/sprints/{sprint.Id}"); var updatedSprint = await getResponse.Content.ReadFromJsonAsync(); updatedSprint!.Name.Should().Be("Updated Sprint"); updatedSprint.Goal.Should().Be("Updated goal"); } [Fact] public async Task DeleteSprint_InPlannedStatus_ShouldSucceed() { // Arrange var sprint = await CreateTestSprintAsync("Sprint to Delete"); // Act var response = await _client.DeleteAsync($"/api/v1/sprints/{sprint.Id}"); // Assert response.StatusCode.Should().Be(HttpStatusCode.NoContent); // Verify deletion var getResponse = await _client.GetAsync($"/api/v1/sprints/{sprint.Id}"); getResponse.StatusCode.Should().Be(HttpStatusCode.NotFound); } [Fact] public async Task DeleteSprint_InActiveStatus_ShouldFail() { // Arrange var sprint = await CreateTestSprintAsync("Active Sprint"); await _client.PostAsync($"/api/v1/sprints/{sprint.Id}/start", null); // Act var response = await _client.DeleteAsync($"/api/v1/sprints/{sprint.Id}"); // Assert response.StatusCode.Should().Be(HttpStatusCode.BadRequest); } [Fact] public async Task GetSprintById_ShouldReturnSprintWithStatistics() { // Arrange var sprint = await CreateTestSprintAsync("Stats Sprint"); // Act var response = await _client.GetAsync($"/api/v1/sprints/{sprint.Id}"); // Assert response.StatusCode.Should().Be(HttpStatusCode.OK); var result = await response.Content.ReadFromJsonAsync(); result.Should().NotBeNull(); result!.Id.Should().Be(sprint.Id); result.Name.Should().Be("Stats Sprint"); result.Status.Should().Be("Planned"); } // ==================== Status Transition Tests ==================== [Fact] public async Task StartSprint_FromPlanned_ShouldTransitionToActive() { // Arrange var sprint = await CreateTestSprintAsync("Sprint to Start"); // Act var response = await _client.PostAsync($"/api/v1/sprints/{sprint.Id}/start", null); // Assert response.StatusCode.Should().Be(HttpStatusCode.NoContent); // Verify status var getResponse = await _client.GetAsync($"/api/v1/sprints/{sprint.Id}"); var updatedSprint = await getResponse.Content.ReadFromJsonAsync(); updatedSprint!.Status.Should().Be("Active"); } [Fact] public async Task CompleteSprint_FromActive_ShouldTransitionToCompleted() { // Arrange var sprint = await CreateTestSprintAsync("Sprint to Complete"); await _client.PostAsync($"/api/v1/sprints/{sprint.Id}/start", null); // Act var response = await _client.PostAsync($"/api/v1/sprints/{sprint.Id}/complete", null); // Assert response.StatusCode.Should().Be(HttpStatusCode.NoContent); // Verify status var getResponse = await _client.GetAsync($"/api/v1/sprints/{sprint.Id}"); var updatedSprint = await getResponse.Content.ReadFromJsonAsync(); updatedSprint!.Status.Should().Be("Completed"); } [Fact] public async Task StartSprint_FromActive_ShouldFail() { // Arrange var sprint = await CreateTestSprintAsync("Already Active Sprint"); await _client.PostAsync($"/api/v1/sprints/{sprint.Id}/start", null); // Act var response = await _client.PostAsync($"/api/v1/sprints/{sprint.Id}/start", null); // Assert response.StatusCode.Should().Be(HttpStatusCode.BadRequest); } [Fact] public async Task CompleteSprint_FromPlanned_ShouldFail() { // Arrange var sprint = await CreateTestSprintAsync("Planned Sprint"); // Act var response = await _client.PostAsync($"/api/v1/sprints/{sprint.Id}/complete", null); // Assert response.StatusCode.Should().Be(HttpStatusCode.BadRequest); } [Fact] public async Task UpdateSprint_InCompletedStatus_ShouldFail() { // Arrange var sprint = await CreateTestSprintAsync("Completed Sprint"); await _client.PostAsync($"/api/v1/sprints/{sprint.Id}/start", null); await _client.PostAsync($"/api/v1/sprints/{sprint.Id}/complete", null); // Act var response = await _client.PutAsync($"/api/v1/sprints/{sprint.Id}", JsonContent.Create(new { SprintId = sprint.Id, Name = "Try to Update", StartDate = DateTime.UtcNow, EndDate = DateTime.UtcNow.AddDays(14) })); // Assert response.StatusCode.Should().Be(HttpStatusCode.BadRequest); } // ==================== Task Management Tests ==================== // NOTE: These tests require Task infrastructure to be implemented first. // They are skipped for now and will be enabled once Task CRUD APIs are available. [Fact(Skip = "Requires Task infrastructure - will be enabled once Task APIs are implemented")] public async Task AddTaskToSprint_ShouldSucceed() { // Arrange var sprint = await CreateTestSprintAsync("Task Sprint"); var taskId = Guid.NewGuid(); // TODO: Create real task once Task API is available // Act var response = await _client.PostAsync($"/api/v1/sprints/{sprint.Id}/tasks/{taskId}", null); // Assert response.StatusCode.Should().Be(HttpStatusCode.NoContent); } [Fact(Skip = "Requires Task infrastructure - will be enabled once Task APIs are implemented")] public async Task RemoveTaskFromSprint_ShouldSucceed() { // Arrange var sprint = await CreateTestSprintAsync("Task Removal Sprint"); var taskId = Guid.NewGuid(); // TODO: Create real task once Task API is available await _client.PostAsync($"/api/v1/sprints/{sprint.Id}/tasks/{taskId}", null); // Act var response = await _client.DeleteAsync($"/api/v1/sprints/{sprint.Id}/tasks/{taskId}"); // Assert response.StatusCode.Should().Be(HttpStatusCode.NoContent); } [Fact(Skip = "Requires Task infrastructure - will be enabled once Task APIs are implemented")] public async Task AddTaskToCompletedSprint_ShouldFail() { // Arrange var sprint = await CreateTestSprintAsync("Completed Task Sprint"); await _client.PostAsync($"/api/v1/sprints/{sprint.Id}/start", null); await _client.PostAsync($"/api/v1/sprints/{sprint.Id}/complete", null); var taskId = Guid.NewGuid(); // TODO: Create real task once Task API is available // Act var response = await _client.PostAsync($"/api/v1/sprints/{sprint.Id}/tasks/{taskId}", null); // Assert response.StatusCode.Should().Be(HttpStatusCode.BadRequest); } // ==================== Query Tests ==================== [Fact] public async Task GetSprintsByProjectId_ShouldReturnProjectSprints() { // Arrange var project = await CreateTestProjectAsync("Query Project", "QRY"); var sprint1 = await CreateTestSprintForProjectAsync(project.Id, "Query Sprint 1"); var sprint2 = await CreateTestSprintForProjectAsync(project.Id, "Query Sprint 2"); // Act var response = await _client.GetAsync($"/api/v1/sprints?projectId={project.Id}"); // Assert response.StatusCode.Should().Be(HttpStatusCode.OK); var sprints = await response.Content.ReadFromJsonAsync>(); sprints.Should().NotBeNull(); sprints!.Should().HaveCountGreaterOrEqualTo(2); sprints.Should().Contain(s => s.Id == sprint1.Id); sprints.Should().Contain(s => s.Id == sprint2.Id); sprints.Should().AllSatisfy(s => s.ProjectId.Should().Be(project.Id)); } [Fact] public async Task GetActiveSprints_ShouldReturnOnlyActiveSprints() { // Arrange var sprint1 = await CreateTestSprintAsync("Active Test 1"); var sprint2 = await CreateTestSprintAsync("Active Test 2"); await _client.PostAsync($"/api/v1/sprints/{sprint1.Id}/start", null); // sprint2 stays in Planned status // Act var response = await _client.GetAsync("/api/v1/sprints/active"); // Assert response.StatusCode.Should().Be(HttpStatusCode.OK); var activeSprints = await response.Content.ReadFromJsonAsync>(); activeSprints.Should().NotBeNull(); activeSprints!.Should().Contain(s => s.Id == sprint1.Id); activeSprints.Should().AllSatisfy(s => s.Status.Should().Be("Active")); } // ==================== Burndown Tests ==================== [Fact] public async Task GetSprintBurndown_ShouldReturnBurndownData() { // Arrange var sprint = await CreateTestSprintAsync("Burndown Sprint"); // Act var response = await _client.GetAsync($"/api/v1/sprints/{sprint.Id}/burndown"); // Assert response.StatusCode.Should().Be(HttpStatusCode.OK); var burndown = await response.Content.ReadFromJsonAsync(); burndown.Should().NotBeNull(); burndown!.SprintId.Should().Be(sprint.Id); burndown.IdealBurndown.Should().NotBeEmpty(); burndown.ActualBurndown.Should().NotBeEmpty(); } [Fact] public async Task GetSprintBurndown_NonExistentSprint_ShouldReturn404() { // Act var response = await _client.GetAsync($"/api/v1/sprints/{Guid.NewGuid()}/burndown"); // Assert response.StatusCode.Should().Be(HttpStatusCode.NotFound); } // ==================== Multi-Tenant Tests ==================== [Fact] public async Task GetSprintById_DifferentTenant_ShouldReturn404() { // Arrange: Tenant 1 creates a sprint 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 sprint = await CreateTestSprintAsync("Tenant 1 Sprint"); // Act: Tenant 2 tries to access the sprint 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 response = await _client.GetAsync($"/api/v1/sprints/{sprint.Id}"); // Assert response.StatusCode.Should().Be(HttpStatusCode.NotFound); } [Fact] public async Task GetSprintsByProjectId_ShouldFilterByTenant() { // Arrange: Tenant 1 creates a project and sprint 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 project1 = await CreateTestProjectAsync("Tenant 1 Project", "T1P"); var sprint1 = await CreateTestSprintForProjectAsync(project1.Id, "Tenant 1 Sprint"); // Act: Tenant 2 creates their own project and sprint 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 project2 = await CreateTestProjectAsync("Tenant 2 Project", "T2P"); var sprint2 = await CreateTestSprintForProjectAsync(project2.Id, "Tenant 2 Sprint"); var response = await _client.GetAsync($"/api/v1/sprints?projectId={project2.Id}"); // Assert response.StatusCode.Should().Be(HttpStatusCode.OK); var sprints = await response.Content.ReadFromJsonAsync>(); sprints.Should().NotBeNull(); sprints!.Should().Contain(s => s.Id == sprint2.Id); sprints.Should().NotContain(s => s.Id == sprint1.Id); } [Fact] public async Task GetActiveSprints_ShouldFilterByTenant() { // Arrange: Tenant 1 creates and starts a sprint 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 sprint1 = await CreateTestSprintAsync("Tenant 1 Active Sprint"); await _client.PostAsync($"/api/v1/sprints/{sprint1.Id}/start", null); // Act: Tenant 2 gets active sprints 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 response = await _client.GetAsync("/api/v1/sprints/active"); // Assert var activeSprints = await response.Content.ReadFromJsonAsync>(); activeSprints.Should().NotBeNull(); activeSprints!.Should().NotContain(s => s.Id == sprint1.Id, "Tenant 2 should not see Tenant 1's sprints"); } // ==================== Business Rule Validation Tests ==================== [Fact] public async Task CreateSprint_WithEmptyName_ShouldFail() { // Arrange var project = await CreateTestProjectAsync("Validation Project", "VAL"); var command = new CreateSprintCommand { ProjectId = project.Id, Name = "", // Empty name StartDate = DateTime.UtcNow, EndDate = DateTime.UtcNow.AddDays(14), CreatedBy = _userId }; // Act var response = await _client.PostAsJsonAsync("/api/v1/sprints", command); // Assert response.StatusCode.Should().Be(HttpStatusCode.BadRequest); } [Fact] public async Task CreateSprint_WithNonExistentProject_ShouldFail() { // Act var response = await _client.PostAsJsonAsync("/api/v1/sprints", new { ProjectId = Guid.NewGuid(), // Non-existent project Name = "Orphan Sprint", StartDate = DateTime.UtcNow, EndDate = DateTime.UtcNow.AddDays(14), CreatedBy = _userId }); // Assert response.StatusCode.Should().Be(HttpStatusCode.NotFound); } // ==================== Helper Methods ==================== private async Task CreateTestProjectAsync(string name, string key) { var response = await _client.PostAsJsonAsync("/api/v1/projects", new { Name = name, Key = key, Description = "Test project for Sprint integration tests" }); response.EnsureSuccessStatusCode(); var project = await response.Content.ReadFromJsonAsync(); return project!; } private async Task CreateTestSprintAsync(string name) { var randomSuffix = Random.Shared.Next(100, 999); var project = await CreateTestProjectAsync($"Project for {name}", $"P{randomSuffix}"); return await CreateTestSprintForProjectAsync(project.Id, name); } private async Task CreateTestSprintForProjectAsync(Guid projectId, string name) { var response = await _client.PostAsJsonAsync("/api/v1/sprints", new { ProjectId = projectId, Name = name, Goal = "Test goal", StartDate = DateTime.UtcNow, EndDate = DateTime.UtcNow.AddDays(14), CreatedBy = _userId }); response.EnsureSuccessStatusCode(); var sprint = await response.Content.ReadFromJsonAsync(); return sprint!; } }