Files
work-club-manager/backend/WorkClub.Tests.Integration/Tasks/TaskCrudTests.cs
WorkClub Automation f8f3e0f01e test(harness): stabilize backend+frontend QA test suite (12/12+63/63 unit+integration, 45/45 frontend)
Stabilize test harness across full stack:

Backend integration tests:
- Fix Auth/Club/Migration/RLS/Member/Tenant/RLS Isolation/Shift/Task test suites
- Add AssemblyInfo.cs for test configuration
- Enhance CustomWebApplicationFactory + TestAuthHandler for stable test environment
- Expand RlsIsolationTests with comprehensive multi-tenant RLS verification

Frontend test harness:
- Align vitest.config.ts with backend API changes
- Add bunfig.toml for bun test environment stability
- Enhance api.test.ts with proper test setup integration
- Expand test/setup.ts with fixture initialization

All tests now passing: backend 12/12 unit + 63/63 integration, frontend 45/45
2026-03-06 09:19:32 +01:00

476 lines
15 KiB
C#

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<Program> factory) : base(factory)
{
}
public override async Task InitializeAsync()
{
using var scope = Factory.Services.CreateScope();
var context = scope.ServiceProvider.GetRequiredService<AppDbContext>();
// 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<string, string> { ["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<TaskDetailResponse>();
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<string, string> { ["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<AppDbContext>();
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<string, string> { ["tenant1"] = "Member" });
var response = await Client.GetAsync("/api/tasks");
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
var result = await response.Content.ReadFromJsonAsync<TaskListResponse>();
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<AppDbContext>();
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<string, string> { ["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<TaskListResponse>();
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<AppDbContext>();
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<string, string> { ["tenant1"] = "Member" });
// Act
var response = await Client.GetAsync($"/api/tasks/{taskId}");
// Assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
var result = await response.Content.ReadFromJsonAsync<TaskDetailResponse>();
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<AppDbContext>();
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<string, string> { ["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<TaskDetailResponse>();
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<AppDbContext>();
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<string, string> { ["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<AppDbContext>();
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<string, string> { ["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<AppDbContext>();
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<AppDbContext>();
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<string, string> { ["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<AppDbContext>();
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<AppDbContext>();
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<string, string> { ["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<TaskListItemResponse> 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);