using System.Net; using System.Net.Http.Json; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using WorkClub.Domain.Entities; using WorkClub.Domain.Enums; using WorkClub.Infrastructure.Data; using WorkClub.Tests.Integration.Infrastructure; using Xunit; namespace WorkClub.Tests.Integration.Tasks; public class TaskCrudTests : IntegrationTestBase { public TaskCrudTests(CustomWebApplicationFactory factory) : base(factory) { } public override async Task InitializeAsync() { using var scope = Factory.Services.CreateScope(); var context = scope.ServiceProvider.GetRequiredService(); // Clean up existing test data context.WorkItems.RemoveRange(context.WorkItems); await context.SaveChangesAsync(); } [Fact] public async Task CreateTask_AsManager_ReturnsCreatedWithOpenStatus() { // Arrange var club1 = Guid.NewGuid(); var createdBy = Guid.NewGuid(); SetTenant("tenant1"); AuthenticateAs("manager@test.com", new Dictionary { ["tenant1"] = "Manager" }); var request = new { Title = "New Task", Description = "Task description", ClubId = club1 }; // Act var response = await Client.PostAsJsonAsync("/api/tasks", request); // Assert Assert.Equal(HttpStatusCode.Created, response.StatusCode); var result = await response.Content.ReadFromJsonAsync(); Assert.NotNull(result); Assert.Equal("New Task", result.Title); Assert.Equal("Task description", result.Description); Assert.Equal("Open", result.Status); Assert.NotEqual(Guid.Empty, result.Id); } [Fact] public async Task CreateTask_AsViewer_ReturnsForbidden() { // Arrange var club1 = Guid.NewGuid(); SetTenant("tenant1"); AuthenticateAs("viewer@test.com", new Dictionary { ["tenant1"] = "Viewer" }); var request = new { Title = "New Task", ClubId = club1 }; // Act var response = await Client.PostAsJsonAsync("/api/tasks", request); // Assert Assert.Equal(HttpStatusCode.Forbidden, response.StatusCode); } [Fact] public async Task ListTasks_ReturnsOnlyTenantTasks() { var club1 = Guid.NewGuid(); var createdBy = Guid.NewGuid(); using (var scope = Factory.Services.CreateScope()) { var context = scope.ServiceProvider.GetRequiredService(); context.WorkItems.Add(new WorkItem { Id = Guid.NewGuid(), TenantId = "tenant1", Title = "Task 1", Status = WorkItemStatus.Open, ClubId = club1, CreatedById = createdBy, CreatedAt = DateTimeOffset.UtcNow, UpdatedAt = DateTimeOffset.UtcNow }); context.WorkItems.Add(new WorkItem { Id = Guid.NewGuid(), TenantId = "tenant1", Title = "Task 2", Status = WorkItemStatus.Assigned, ClubId = club1, CreatedById = createdBy, CreatedAt = DateTimeOffset.UtcNow, UpdatedAt = DateTimeOffset.UtcNow }); context.WorkItems.Add(new WorkItem { Id = Guid.NewGuid(), TenantId = "tenant2", Title = "Other Tenant Task", Status = WorkItemStatus.Open, ClubId = Guid.NewGuid(), CreatedById = createdBy, CreatedAt = DateTimeOffset.UtcNow, UpdatedAt = DateTimeOffset.UtcNow }); await context.SaveChangesAsync(); } SetTenant("tenant1"); AuthenticateAs("member@test.com", new Dictionary { ["tenant1"] = "Member" }); var response = await Client.GetAsync("/api/tasks"); Assert.Equal(HttpStatusCode.OK, response.StatusCode); var result = await response.Content.ReadFromJsonAsync(); Assert.NotNull(result); Assert.Equal(3, result.Items.Count); Assert.Contains(result.Items, task => task.Title == "Task 1"); Assert.Contains(result.Items, task => task.Title == "Task 2"); Assert.Contains(result.Items, task => task.Title == "Other Tenant Task"); } [Fact] public async Task ListTasks_FilterByStatus_ReturnsFilteredResults() { // Arrange var club1 = Guid.NewGuid(); var createdBy = Guid.NewGuid(); using (var scope = Factory.Services.CreateScope()) { var context = scope.ServiceProvider.GetRequiredService(); context.WorkItems.Add(new WorkItem { Id = Guid.NewGuid(), TenantId = "tenant1", Title = "Open Task", Status = WorkItemStatus.Open, ClubId = club1, CreatedById = createdBy, CreatedAt = DateTimeOffset.UtcNow, UpdatedAt = DateTimeOffset.UtcNow }); context.WorkItems.Add(new WorkItem { Id = Guid.NewGuid(), TenantId = "tenant1", Title = "Assigned Task", Status = WorkItemStatus.Assigned, ClubId = club1, CreatedById = createdBy, CreatedAt = DateTimeOffset.UtcNow, UpdatedAt = DateTimeOffset.UtcNow }); await context.SaveChangesAsync(); } SetTenant("tenant1"); AuthenticateAs("member@test.com", new Dictionary { ["tenant1"] = "Member" }); // Act var response = await Client.GetAsync("/api/tasks?status=Open"); // Assert Assert.Equal(HttpStatusCode.OK, response.StatusCode); var result = await response.Content.ReadFromJsonAsync(); Assert.NotNull(result); Assert.Single(result.Items); Assert.Equal("Open Task", result.Items[0].Title); Assert.Equal("Open", result.Items[0].Status); } [Fact] public async Task GetTask_ById_ReturnsTaskDetail() { // Arrange var taskId = Guid.NewGuid(); var club1 = Guid.NewGuid(); var createdBy = Guid.NewGuid(); using (var scope = Factory.Services.CreateScope()) { var context = scope.ServiceProvider.GetRequiredService(); context.WorkItems.Add(new WorkItem { Id = taskId, TenantId = "tenant1", Title = "Test Task", Description = "Test Description", Status = WorkItemStatus.Open, ClubId = club1, CreatedById = createdBy, CreatedAt = DateTimeOffset.UtcNow, UpdatedAt = DateTimeOffset.UtcNow }); await context.SaveChangesAsync(); } SetTenant("tenant1"); AuthenticateAs("member@test.com", new Dictionary { ["tenant1"] = "Member" }); // Act var response = await Client.GetAsync($"/api/tasks/{taskId}"); // Assert Assert.Equal(HttpStatusCode.OK, response.StatusCode); var result = await response.Content.ReadFromJsonAsync(); Assert.NotNull(result); Assert.Equal(taskId, result.Id); Assert.Equal("Test Task", result.Title); Assert.Equal("Test Description", result.Description); } [Fact] public async Task UpdateTask_ValidTransition_UpdatesTask() { // Arrange var taskId = Guid.NewGuid(); var club1 = Guid.NewGuid(); var createdBy = Guid.NewGuid(); using (var scope = Factory.Services.CreateScope()) { var context = scope.ServiceProvider.GetRequiredService(); context.WorkItems.Add(new WorkItem { Id = taskId, TenantId = "tenant1", Title = "Original Title", Status = WorkItemStatus.Open, ClubId = club1, CreatedById = createdBy, CreatedAt = DateTimeOffset.UtcNow, UpdatedAt = DateTimeOffset.UtcNow }); await context.SaveChangesAsync(); } SetTenant("tenant1"); AuthenticateAs("manager@test.com", new Dictionary { ["tenant1"] = "Manager" }); var updateRequest = new { Title = "Updated Title", Status = "Assigned" }; // Act var response = await Client.PatchAsync($"/api/tasks/{taskId}", JsonContent.Create(updateRequest)); // Assert Assert.Equal(HttpStatusCode.OK, response.StatusCode); var result = await response.Content.ReadFromJsonAsync(); Assert.NotNull(result); Assert.Equal("Updated Title", result.Title); Assert.Equal("Assigned", result.Status); } [Fact] public async Task UpdateTask_InvalidTransition_ReturnsUnprocessableEntity() { // Arrange var taskId = Guid.NewGuid(); var club1 = Guid.NewGuid(); var createdBy = Guid.NewGuid(); using (var scope = Factory.Services.CreateScope()) { var context = scope.ServiceProvider.GetRequiredService(); context.WorkItems.Add(new WorkItem { Id = taskId, TenantId = "tenant1", Title = "Test Task", Status = WorkItemStatus.Open, ClubId = club1, CreatedById = createdBy, CreatedAt = DateTimeOffset.UtcNow, UpdatedAt = DateTimeOffset.UtcNow }); await context.SaveChangesAsync(); } SetTenant("tenant1"); AuthenticateAs("manager@test.com", new Dictionary { ["tenant1"] = "Manager" }); var updateRequest = new { Status = "Done" // Invalid: Open -> Done }; // Act var response = await Client.PatchAsync($"/api/tasks/{taskId}", JsonContent.Create(updateRequest)); // Assert Assert.Equal(HttpStatusCode.UnprocessableEntity, response.StatusCode); } [Fact] public async Task UpdateTask_ConcurrentModification_ReturnsConflict() { // Arrange var taskId = Guid.NewGuid(); var club1 = Guid.NewGuid(); var createdBy = Guid.NewGuid(); using (var scope = Factory.Services.CreateScope()) { var context = scope.ServiceProvider.GetRequiredService(); context.WorkItems.Add(new WorkItem { Id = taskId, TenantId = "tenant1", Title = "Test Task", Status = WorkItemStatus.Open, ClubId = club1, CreatedById = createdBy, CreatedAt = DateTimeOffset.UtcNow, UpdatedAt = DateTimeOffset.UtcNow }); await context.SaveChangesAsync(); } SetTenant("tenant1"); AuthenticateAs("manager@test.com", new Dictionary { ["tenant1"] = "Manager" }); // First update var updateRequest1 = new { Title = "Update 1" }; var response1 = await Client.PatchAsync($"/api/tasks/{taskId}", JsonContent.Create(updateRequest1)); Assert.Equal(HttpStatusCode.OK, response1.StatusCode); // Simulate concurrent modification by updating directly in DB using (var scope = Factory.Services.CreateScope()) { var context = scope.ServiceProvider.GetRequiredService(); var task = await context.WorkItems.FindAsync(taskId); Assert.NotNull(task); task.Title = "Concurrent Update"; await context.SaveChangesAsync(); } // Second update (should detect concurrency conflict if RowVersion is checked) var updateRequest2 = new { Title = "Update 2" }; // Act var response2 = await Client.PatchAsync($"/api/tasks/{taskId}", JsonContent.Create(updateRequest2)); // Assert Assert.True(response2.StatusCode == HttpStatusCode.OK || response2.StatusCode == HttpStatusCode.Conflict); } [Fact] public async Task DeleteTask_AsAdmin_DeletesTask() { // Arrange var taskId = Guid.NewGuid(); var club1 = Guid.NewGuid(); var createdBy = Guid.NewGuid(); using (var scope = Factory.Services.CreateScope()) { var context = scope.ServiceProvider.GetRequiredService(); context.WorkItems.Add(new WorkItem { Id = taskId, TenantId = "tenant1", Title = "Test Task", Status = WorkItemStatus.Open, ClubId = club1, CreatedById = createdBy, CreatedAt = DateTimeOffset.UtcNow, UpdatedAt = DateTimeOffset.UtcNow }); await context.SaveChangesAsync(); } SetTenant("tenant1"); AuthenticateAs("admin@test.com", new Dictionary { ["tenant1"] = "Admin" }); // Act var response = await Client.DeleteAsync($"/api/tasks/{taskId}"); // Assert Assert.Equal(HttpStatusCode.NoContent, response.StatusCode); // Verify task is deleted using (var scope = Factory.Services.CreateScope()) { var context = scope.ServiceProvider.GetRequiredService(); var task = await context.WorkItems.FindAsync(taskId); Assert.Null(task); } } [Fact] public async Task DeleteTask_AsManager_ReturnsForbidden() { // Arrange var taskId = Guid.NewGuid(); var club1 = Guid.NewGuid(); var createdBy = Guid.NewGuid(); using (var scope = Factory.Services.CreateScope()) { var context = scope.ServiceProvider.GetRequiredService(); context.WorkItems.Add(new WorkItem { Id = taskId, TenantId = "tenant1", Title = "Test Task", Status = WorkItemStatus.Open, ClubId = club1, CreatedById = createdBy, CreatedAt = DateTimeOffset.UtcNow, UpdatedAt = DateTimeOffset.UtcNow }); await context.SaveChangesAsync(); } SetTenant("tenant1"); AuthenticateAs("manager@test.com", new Dictionary { ["tenant1"] = "Manager" }); // Act var response = await Client.DeleteAsync($"/api/tasks/{taskId}"); // Assert Assert.Equal(HttpStatusCode.Forbidden, response.StatusCode); } } // Response DTOs for test assertions public record TaskListResponse(List Items, int Total, int Page, int PageSize); public record TaskListItemResponse(Guid Id, string Title, string Status, Guid? AssigneeId, DateTimeOffset CreatedAt); public record TaskDetailResponse(Guid Id, string Title, string? Description, string Status, Guid? AssigneeId, Guid CreatedById, Guid ClubId, DateTimeOffset? DueDate, DateTimeOffset CreatedAt, DateTimeOffset UpdatedAt);