feat(tasks): add Task CRUD API with 5-state workflow
- TaskService with CRUD operations + state machine enforcement
- 5 DTOs: TaskListDto, TaskDetailDto, CreateTaskRequest, UpdateTaskRequest
- Minimal API endpoints: GET /api/tasks, GET /api/tasks/{id}, POST, PATCH, DELETE
- State machine: Open→Assigned→InProgress→Review→Done (invalid transitions → 422)
- Concurrency: DbUpdateConcurrencyException → 409 Conflict
- Authorization: POST (Manager+), DELETE (Admin), GET/PATCH (Member+)
- RLS: Automatic tenant filtering via TenantDbConnectionInterceptor
- 10 TDD integration tests: CRUD, state transitions, concurrency, role enforcement
- Build: 0 errors (6 BouncyCastle warnings expected)
Task 14 complete. Wave 3: 2/5 tasks done.
2026-03-03 19:19:21 +01:00
|
|
|
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>();
|
2026-03-05 11:07:19 +01:00
|
|
|
|
feat(tasks): add Task CRUD API with 5-state workflow
- TaskService with CRUD operations + state machine enforcement
- 5 DTOs: TaskListDto, TaskDetailDto, CreateTaskRequest, UpdateTaskRequest
- Minimal API endpoints: GET /api/tasks, GET /api/tasks/{id}, POST, PATCH, DELETE
- State machine: Open→Assigned→InProgress→Review→Done (invalid transitions → 422)
- Concurrency: DbUpdateConcurrencyException → 409 Conflict
- Authorization: POST (Manager+), DELETE (Admin), GET/PATCH (Member+)
- RLS: Automatic tenant filtering via TenantDbConnectionInterceptor
- 10 TDD integration tests: CRUD, state transitions, concurrency, role enforcement
- Build: 0 errors (6 BouncyCastle warnings expected)
Task 14 complete. Wave 3: 2/5 tasks done.
2026-03-03 19:19:21 +01:00
|
|
|
// 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();
|
2026-03-05 11:07:19 +01:00
|
|
|
|
feat(tasks): add Task CRUD API with 5-state workflow
- TaskService with CRUD operations + state machine enforcement
- 5 DTOs: TaskListDto, TaskDetailDto, CreateTaskRequest, UpdateTaskRequest
- Minimal API endpoints: GET /api/tasks, GET /api/tasks/{id}, POST, PATCH, DELETE
- State machine: Open→Assigned→InProgress→Review→Done (invalid transitions → 422)
- Concurrency: DbUpdateConcurrencyException → 409 Conflict
- Authorization: POST (Manager+), DELETE (Admin), GET/PATCH (Member+)
- RLS: Automatic tenant filtering via TenantDbConnectionInterceptor
- 10 TDD integration tests: CRUD, state transitions, concurrency, role enforcement
- Build: 0 errors (6 BouncyCastle warnings expected)
Task 14 complete. Wave 3: 2/5 tasks done.
2026-03-03 19:19:21 +01:00
|
|
|
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);
|
2026-03-05 11:07:19 +01:00
|
|
|
|
feat(tasks): add Task CRUD API with 5-state workflow
- TaskService with CRUD operations + state machine enforcement
- 5 DTOs: TaskListDto, TaskDetailDto, CreateTaskRequest, UpdateTaskRequest
- Minimal API endpoints: GET /api/tasks, GET /api/tasks/{id}, POST, PATCH, DELETE
- State machine: Open→Assigned→InProgress→Review→Done (invalid transitions → 422)
- Concurrency: DbUpdateConcurrencyException → 409 Conflict
- Authorization: POST (Manager+), DELETE (Admin), GET/PATCH (Member+)
- RLS: Automatic tenant filtering via TenantDbConnectionInterceptor
- 10 TDD integration tests: CRUD, state transitions, concurrency, role enforcement
- Build: 0 errors (6 BouncyCastle warnings expected)
Task 14 complete. Wave 3: 2/5 tasks done.
2026-03-03 19:19:21 +01:00
|
|
|
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();
|
2026-03-05 11:07:19 +01:00
|
|
|
|
feat(tasks): add Task CRUD API with 5-state workflow
- TaskService with CRUD operations + state machine enforcement
- 5 DTOs: TaskListDto, TaskDetailDto, CreateTaskRequest, UpdateTaskRequest
- Minimal API endpoints: GET /api/tasks, GET /api/tasks/{id}, POST, PATCH, DELETE
- State machine: Open→Assigned→InProgress→Review→Done (invalid transitions → 422)
- Concurrency: DbUpdateConcurrencyException → 409 Conflict
- Authorization: POST (Manager+), DELETE (Admin), GET/PATCH (Member+)
- RLS: Automatic tenant filtering via TenantDbConnectionInterceptor
- 10 TDD integration tests: CRUD, state transitions, concurrency, role enforcement
- Build: 0 errors (6 BouncyCastle warnings expected)
Task 14 complete. Wave 3: 2/5 tasks done.
2026-03-03 19:19:21 +01:00
|
|
|
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();
|
2026-03-05 11:07:19 +01:00
|
|
|
|
feat(tasks): add Task CRUD API with 5-state workflow
- TaskService with CRUD operations + state machine enforcement
- 5 DTOs: TaskListDto, TaskDetailDto, CreateTaskRequest, UpdateTaskRequest
- Minimal API endpoints: GET /api/tasks, GET /api/tasks/{id}, POST, PATCH, DELETE
- State machine: Open→Assigned→InProgress→Review→Done (invalid transitions → 422)
- Concurrency: DbUpdateConcurrencyException → 409 Conflict
- Authorization: POST (Manager+), DELETE (Admin), GET/PATCH (Member+)
- RLS: Automatic tenant filtering via TenantDbConnectionInterceptor
- 10 TDD integration tests: CRUD, state transitions, concurrency, role enforcement
- Build: 0 errors (6 BouncyCastle warnings expected)
Task 14 complete. Wave 3: 2/5 tasks done.
2026-03-03 19:19:21 +01:00
|
|
|
using (var scope = Factory.Services.CreateScope())
|
|
|
|
|
{
|
|
|
|
|
var context = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
2026-03-05 11:07:19 +01:00
|
|
|
|
feat(tasks): add Task CRUD API with 5-state workflow
- TaskService with CRUD operations + state machine enforcement
- 5 DTOs: TaskListDto, TaskDetailDto, CreateTaskRequest, UpdateTaskRequest
- Minimal API endpoints: GET /api/tasks, GET /api/tasks/{id}, POST, PATCH, DELETE
- State machine: Open→Assigned→InProgress→Review→Done (invalid transitions → 422)
- Concurrency: DbUpdateConcurrencyException → 409 Conflict
- Authorization: POST (Manager+), DELETE (Admin), GET/PATCH (Member+)
- RLS: Automatic tenant filtering via TenantDbConnectionInterceptor
- 10 TDD integration tests: CRUD, state transitions, concurrency, role enforcement
- Build: 0 errors (6 BouncyCastle warnings expected)
Task 14 complete. Wave 3: 2/5 tasks done.
2026-03-03 19:19:21 +01:00
|
|
|
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
|
|
|
|
|
});
|
2026-03-05 11:07:19 +01:00
|
|
|
|
feat(tasks): add Task CRUD API with 5-state workflow
- TaskService with CRUD operations + state machine enforcement
- 5 DTOs: TaskListDto, TaskDetailDto, CreateTaskRequest, UpdateTaskRequest
- Minimal API endpoints: GET /api/tasks, GET /api/tasks/{id}, POST, PATCH, DELETE
- State machine: Open→Assigned→InProgress→Review→Done (invalid transitions → 422)
- Concurrency: DbUpdateConcurrencyException → 409 Conflict
- Authorization: POST (Manager+), DELETE (Admin), GET/PATCH (Member+)
- RLS: Automatic tenant filtering via TenantDbConnectionInterceptor
- 10 TDD integration tests: CRUD, state transitions, concurrency, role enforcement
- Build: 0 errors (6 BouncyCastle warnings expected)
Task 14 complete. Wave 3: 2/5 tasks done.
2026-03-03 19:19:21 +01:00
|
|
|
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
|
|
|
|
|
});
|
2026-03-05 11:07:19 +01:00
|
|
|
|
feat(tasks): add Task CRUD API with 5-state workflow
- TaskService with CRUD operations + state machine enforcement
- 5 DTOs: TaskListDto, TaskDetailDto, CreateTaskRequest, UpdateTaskRequest
- Minimal API endpoints: GET /api/tasks, GET /api/tasks/{id}, POST, PATCH, DELETE
- State machine: Open→Assigned→InProgress→Review→Done (invalid transitions → 422)
- Concurrency: DbUpdateConcurrencyException → 409 Conflict
- Authorization: POST (Manager+), DELETE (Admin), GET/PATCH (Member+)
- RLS: Automatic tenant filtering via TenantDbConnectionInterceptor
- 10 TDD integration tests: CRUD, state transitions, concurrency, role enforcement
- Build: 0 errors (6 BouncyCastle warnings expected)
Task 14 complete. Wave 3: 2/5 tasks done.
2026-03-03 19:19:21 +01:00
|
|
|
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
|
|
|
|
|
});
|
2026-03-05 11:07:19 +01:00
|
|
|
|
feat(tasks): add Task CRUD API with 5-state workflow
- TaskService with CRUD operations + state machine enforcement
- 5 DTOs: TaskListDto, TaskDetailDto, CreateTaskRequest, UpdateTaskRequest
- Minimal API endpoints: GET /api/tasks, GET /api/tasks/{id}, POST, PATCH, DELETE
- State machine: Open→Assigned→InProgress→Review→Done (invalid transitions → 422)
- Concurrency: DbUpdateConcurrencyException → 409 Conflict
- Authorization: POST (Manager+), DELETE (Admin), GET/PATCH (Member+)
- RLS: Automatic tenant filtering via TenantDbConnectionInterceptor
- 10 TDD integration tests: CRUD, state transitions, concurrency, role enforcement
- Build: 0 errors (6 BouncyCastle warnings expected)
Task 14 complete. Wave 3: 2/5 tasks done.
2026-03-03 19:19:21 +01:00
|
|
|
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);
|
2026-03-05 11:07:19 +01:00
|
|
|
|
feat(tasks): add Task CRUD API with 5-state workflow
- TaskService with CRUD operations + state machine enforcement
- 5 DTOs: TaskListDto, TaskDetailDto, CreateTaskRequest, UpdateTaskRequest
- Minimal API endpoints: GET /api/tasks, GET /api/tasks/{id}, POST, PATCH, DELETE
- State machine: Open→Assigned→InProgress→Review→Done (invalid transitions → 422)
- Concurrency: DbUpdateConcurrencyException → 409 Conflict
- Authorization: POST (Manager+), DELETE (Admin), GET/PATCH (Member+)
- RLS: Automatic tenant filtering via TenantDbConnectionInterceptor
- 10 TDD integration tests: CRUD, state transitions, concurrency, role enforcement
- Build: 0 errors (6 BouncyCastle warnings expected)
Task 14 complete. Wave 3: 2/5 tasks done.
2026-03-03 19:19:21 +01:00
|
|
|
var result = await response.Content.ReadFromJsonAsync<TaskListResponse>();
|
|
|
|
|
Assert.NotNull(result);
|
2026-03-06 09:19:32 +01:00
|
|
|
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");
|
feat(tasks): add Task CRUD API with 5-state workflow
- TaskService with CRUD operations + state machine enforcement
- 5 DTOs: TaskListDto, TaskDetailDto, CreateTaskRequest, UpdateTaskRequest
- Minimal API endpoints: GET /api/tasks, GET /api/tasks/{id}, POST, PATCH, DELETE
- State machine: Open→Assigned→InProgress→Review→Done (invalid transitions → 422)
- Concurrency: DbUpdateConcurrencyException → 409 Conflict
- Authorization: POST (Manager+), DELETE (Admin), GET/PATCH (Member+)
- RLS: Automatic tenant filtering via TenantDbConnectionInterceptor
- 10 TDD integration tests: CRUD, state transitions, concurrency, role enforcement
- Build: 0 errors (6 BouncyCastle warnings expected)
Task 14 complete. Wave 3: 2/5 tasks done.
2026-03-03 19:19:21 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[Fact]
|
|
|
|
|
public async Task ListTasks_FilterByStatus_ReturnsFilteredResults()
|
|
|
|
|
{
|
|
|
|
|
// Arrange
|
|
|
|
|
var club1 = Guid.NewGuid();
|
|
|
|
|
var createdBy = Guid.NewGuid();
|
2026-03-05 11:07:19 +01:00
|
|
|
|
feat(tasks): add Task CRUD API with 5-state workflow
- TaskService with CRUD operations + state machine enforcement
- 5 DTOs: TaskListDto, TaskDetailDto, CreateTaskRequest, UpdateTaskRequest
- Minimal API endpoints: GET /api/tasks, GET /api/tasks/{id}, POST, PATCH, DELETE
- State machine: Open→Assigned→InProgress→Review→Done (invalid transitions → 422)
- Concurrency: DbUpdateConcurrencyException → 409 Conflict
- Authorization: POST (Manager+), DELETE (Admin), GET/PATCH (Member+)
- RLS: Automatic tenant filtering via TenantDbConnectionInterceptor
- 10 TDD integration tests: CRUD, state transitions, concurrency, role enforcement
- Build: 0 errors (6 BouncyCastle warnings expected)
Task 14 complete. Wave 3: 2/5 tasks done.
2026-03-03 19:19:21 +01:00
|
|
|
using (var scope = Factory.Services.CreateScope())
|
|
|
|
|
{
|
|
|
|
|
var context = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
2026-03-05 11:07:19 +01:00
|
|
|
|
feat(tasks): add Task CRUD API with 5-state workflow
- TaskService with CRUD operations + state machine enforcement
- 5 DTOs: TaskListDto, TaskDetailDto, CreateTaskRequest, UpdateTaskRequest
- Minimal API endpoints: GET /api/tasks, GET /api/tasks/{id}, POST, PATCH, DELETE
- State machine: Open→Assigned→InProgress→Review→Done (invalid transitions → 422)
- Concurrency: DbUpdateConcurrencyException → 409 Conflict
- Authorization: POST (Manager+), DELETE (Admin), GET/PATCH (Member+)
- RLS: Automatic tenant filtering via TenantDbConnectionInterceptor
- 10 TDD integration tests: CRUD, state transitions, concurrency, role enforcement
- Build: 0 errors (6 BouncyCastle warnings expected)
Task 14 complete. Wave 3: 2/5 tasks done.
2026-03-03 19:19:21 +01:00
|
|
|
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
|
|
|
|
|
});
|
2026-03-05 11:07:19 +01:00
|
|
|
|
feat(tasks): add Task CRUD API with 5-state workflow
- TaskService with CRUD operations + state machine enforcement
- 5 DTOs: TaskListDto, TaskDetailDto, CreateTaskRequest, UpdateTaskRequest
- Minimal API endpoints: GET /api/tasks, GET /api/tasks/{id}, POST, PATCH, DELETE
- State machine: Open→Assigned→InProgress→Review→Done (invalid transitions → 422)
- Concurrency: DbUpdateConcurrencyException → 409 Conflict
- Authorization: POST (Manager+), DELETE (Admin), GET/PATCH (Member+)
- RLS: Automatic tenant filtering via TenantDbConnectionInterceptor
- 10 TDD integration tests: CRUD, state transitions, concurrency, role enforcement
- Build: 0 errors (6 BouncyCastle warnings expected)
Task 14 complete. Wave 3: 2/5 tasks done.
2026-03-03 19:19:21 +01:00
|
|
|
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
|
|
|
|
|
});
|
2026-03-05 11:07:19 +01:00
|
|
|
|
feat(tasks): add Task CRUD API with 5-state workflow
- TaskService with CRUD operations + state machine enforcement
- 5 DTOs: TaskListDto, TaskDetailDto, CreateTaskRequest, UpdateTaskRequest
- Minimal API endpoints: GET /api/tasks, GET /api/tasks/{id}, POST, PATCH, DELETE
- State machine: Open→Assigned→InProgress→Review→Done (invalid transitions → 422)
- Concurrency: DbUpdateConcurrencyException → 409 Conflict
- Authorization: POST (Manager+), DELETE (Admin), GET/PATCH (Member+)
- RLS: Automatic tenant filtering via TenantDbConnectionInterceptor
- 10 TDD integration tests: CRUD, state transitions, concurrency, role enforcement
- Build: 0 errors (6 BouncyCastle warnings expected)
Task 14 complete. Wave 3: 2/5 tasks done.
2026-03-03 19:19:21 +01:00
|
|
|
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);
|
2026-03-05 11:07:19 +01:00
|
|
|
|
feat(tasks): add Task CRUD API with 5-state workflow
- TaskService with CRUD operations + state machine enforcement
- 5 DTOs: TaskListDto, TaskDetailDto, CreateTaskRequest, UpdateTaskRequest
- Minimal API endpoints: GET /api/tasks, GET /api/tasks/{id}, POST, PATCH, DELETE
- State machine: Open→Assigned→InProgress→Review→Done (invalid transitions → 422)
- Concurrency: DbUpdateConcurrencyException → 409 Conflict
- Authorization: POST (Manager+), DELETE (Admin), GET/PATCH (Member+)
- RLS: Automatic tenant filtering via TenantDbConnectionInterceptor
- 10 TDD integration tests: CRUD, state transitions, concurrency, role enforcement
- Build: 0 errors (6 BouncyCastle warnings expected)
Task 14 complete. Wave 3: 2/5 tasks done.
2026-03-03 19:19:21 +01:00
|
|
|
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();
|
2026-03-05 11:07:19 +01:00
|
|
|
|
feat(tasks): add Task CRUD API with 5-state workflow
- TaskService with CRUD operations + state machine enforcement
- 5 DTOs: TaskListDto, TaskDetailDto, CreateTaskRequest, UpdateTaskRequest
- Minimal API endpoints: GET /api/tasks, GET /api/tasks/{id}, POST, PATCH, DELETE
- State machine: Open→Assigned→InProgress→Review→Done (invalid transitions → 422)
- Concurrency: DbUpdateConcurrencyException → 409 Conflict
- Authorization: POST (Manager+), DELETE (Admin), GET/PATCH (Member+)
- RLS: Automatic tenant filtering via TenantDbConnectionInterceptor
- 10 TDD integration tests: CRUD, state transitions, concurrency, role enforcement
- Build: 0 errors (6 BouncyCastle warnings expected)
Task 14 complete. Wave 3: 2/5 tasks done.
2026-03-03 19:19:21 +01:00
|
|
|
using (var scope = Factory.Services.CreateScope())
|
|
|
|
|
{
|
|
|
|
|
var context = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
2026-03-05 11:07:19 +01:00
|
|
|
|
feat(tasks): add Task CRUD API with 5-state workflow
- TaskService with CRUD operations + state machine enforcement
- 5 DTOs: TaskListDto, TaskDetailDto, CreateTaskRequest, UpdateTaskRequest
- Minimal API endpoints: GET /api/tasks, GET /api/tasks/{id}, POST, PATCH, DELETE
- State machine: Open→Assigned→InProgress→Review→Done (invalid transitions → 422)
- Concurrency: DbUpdateConcurrencyException → 409 Conflict
- Authorization: POST (Manager+), DELETE (Admin), GET/PATCH (Member+)
- RLS: Automatic tenant filtering via TenantDbConnectionInterceptor
- 10 TDD integration tests: CRUD, state transitions, concurrency, role enforcement
- Build: 0 errors (6 BouncyCastle warnings expected)
Task 14 complete. Wave 3: 2/5 tasks done.
2026-03-03 19:19:21 +01:00
|
|
|
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
|
|
|
|
|
});
|
2026-03-05 11:07:19 +01:00
|
|
|
|
feat(tasks): add Task CRUD API with 5-state workflow
- TaskService with CRUD operations + state machine enforcement
- 5 DTOs: TaskListDto, TaskDetailDto, CreateTaskRequest, UpdateTaskRequest
- Minimal API endpoints: GET /api/tasks, GET /api/tasks/{id}, POST, PATCH, DELETE
- State machine: Open→Assigned→InProgress→Review→Done (invalid transitions → 422)
- Concurrency: DbUpdateConcurrencyException → 409 Conflict
- Authorization: POST (Manager+), DELETE (Admin), GET/PATCH (Member+)
- RLS: Automatic tenant filtering via TenantDbConnectionInterceptor
- 10 TDD integration tests: CRUD, state transitions, concurrency, role enforcement
- Build: 0 errors (6 BouncyCastle warnings expected)
Task 14 complete. Wave 3: 2/5 tasks done.
2026-03-03 19:19:21 +01:00
|
|
|
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);
|
2026-03-05 11:07:19 +01:00
|
|
|
|
feat(tasks): add Task CRUD API with 5-state workflow
- TaskService with CRUD operations + state machine enforcement
- 5 DTOs: TaskListDto, TaskDetailDto, CreateTaskRequest, UpdateTaskRequest
- Minimal API endpoints: GET /api/tasks, GET /api/tasks/{id}, POST, PATCH, DELETE
- State machine: Open→Assigned→InProgress→Review→Done (invalid transitions → 422)
- Concurrency: DbUpdateConcurrencyException → 409 Conflict
- Authorization: POST (Manager+), DELETE (Admin), GET/PATCH (Member+)
- RLS: Automatic tenant filtering via TenantDbConnectionInterceptor
- 10 TDD integration tests: CRUD, state transitions, concurrency, role enforcement
- Build: 0 errors (6 BouncyCastle warnings expected)
Task 14 complete. Wave 3: 2/5 tasks done.
2026-03-03 19:19:21 +01:00
|
|
|
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();
|
2026-03-05 11:07:19 +01:00
|
|
|
|
feat(tasks): add Task CRUD API with 5-state workflow
- TaskService with CRUD operations + state machine enforcement
- 5 DTOs: TaskListDto, TaskDetailDto, CreateTaskRequest, UpdateTaskRequest
- Minimal API endpoints: GET /api/tasks, GET /api/tasks/{id}, POST, PATCH, DELETE
- State machine: Open→Assigned→InProgress→Review→Done (invalid transitions → 422)
- Concurrency: DbUpdateConcurrencyException → 409 Conflict
- Authorization: POST (Manager+), DELETE (Admin), GET/PATCH (Member+)
- RLS: Automatic tenant filtering via TenantDbConnectionInterceptor
- 10 TDD integration tests: CRUD, state transitions, concurrency, role enforcement
- Build: 0 errors (6 BouncyCastle warnings expected)
Task 14 complete. Wave 3: 2/5 tasks done.
2026-03-03 19:19:21 +01:00
|
|
|
using (var scope = Factory.Services.CreateScope())
|
|
|
|
|
{
|
|
|
|
|
var context = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
2026-03-05 11:07:19 +01:00
|
|
|
|
feat(tasks): add Task CRUD API with 5-state workflow
- TaskService with CRUD operations + state machine enforcement
- 5 DTOs: TaskListDto, TaskDetailDto, CreateTaskRequest, UpdateTaskRequest
- Minimal API endpoints: GET /api/tasks, GET /api/tasks/{id}, POST, PATCH, DELETE
- State machine: Open→Assigned→InProgress→Review→Done (invalid transitions → 422)
- Concurrency: DbUpdateConcurrencyException → 409 Conflict
- Authorization: POST (Manager+), DELETE (Admin), GET/PATCH (Member+)
- RLS: Automatic tenant filtering via TenantDbConnectionInterceptor
- 10 TDD integration tests: CRUD, state transitions, concurrency, role enforcement
- Build: 0 errors (6 BouncyCastle warnings expected)
Task 14 complete. Wave 3: 2/5 tasks done.
2026-03-03 19:19:21 +01:00
|
|
|
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
|
|
|
|
|
});
|
2026-03-05 11:07:19 +01:00
|
|
|
|
feat(tasks): add Task CRUD API with 5-state workflow
- TaskService with CRUD operations + state machine enforcement
- 5 DTOs: TaskListDto, TaskDetailDto, CreateTaskRequest, UpdateTaskRequest
- Minimal API endpoints: GET /api/tasks, GET /api/tasks/{id}, POST, PATCH, DELETE
- State machine: Open→Assigned→InProgress→Review→Done (invalid transitions → 422)
- Concurrency: DbUpdateConcurrencyException → 409 Conflict
- Authorization: POST (Manager+), DELETE (Admin), GET/PATCH (Member+)
- RLS: Automatic tenant filtering via TenantDbConnectionInterceptor
- 10 TDD integration tests: CRUD, state transitions, concurrency, role enforcement
- Build: 0 errors (6 BouncyCastle warnings expected)
Task 14 complete. Wave 3: 2/5 tasks done.
2026-03-03 19:19:21 +01:00
|
|
|
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);
|
2026-03-05 11:07:19 +01:00
|
|
|
|
feat(tasks): add Task CRUD API with 5-state workflow
- TaskService with CRUD operations + state machine enforcement
- 5 DTOs: TaskListDto, TaskDetailDto, CreateTaskRequest, UpdateTaskRequest
- Minimal API endpoints: GET /api/tasks, GET /api/tasks/{id}, POST, PATCH, DELETE
- State machine: Open→Assigned→InProgress→Review→Done (invalid transitions → 422)
- Concurrency: DbUpdateConcurrencyException → 409 Conflict
- Authorization: POST (Manager+), DELETE (Admin), GET/PATCH (Member+)
- RLS: Automatic tenant filtering via TenantDbConnectionInterceptor
- 10 TDD integration tests: CRUD, state transitions, concurrency, role enforcement
- Build: 0 errors (6 BouncyCastle warnings expected)
Task 14 complete. Wave 3: 2/5 tasks done.
2026-03-03 19:19:21 +01:00
|
|
|
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();
|
2026-03-05 11:07:19 +01:00
|
|
|
|
feat(tasks): add Task CRUD API with 5-state workflow
- TaskService with CRUD operations + state machine enforcement
- 5 DTOs: TaskListDto, TaskDetailDto, CreateTaskRequest, UpdateTaskRequest
- Minimal API endpoints: GET /api/tasks, GET /api/tasks/{id}, POST, PATCH, DELETE
- State machine: Open→Assigned→InProgress→Review→Done (invalid transitions → 422)
- Concurrency: DbUpdateConcurrencyException → 409 Conflict
- Authorization: POST (Manager+), DELETE (Admin), GET/PATCH (Member+)
- RLS: Automatic tenant filtering via TenantDbConnectionInterceptor
- 10 TDD integration tests: CRUD, state transitions, concurrency, role enforcement
- Build: 0 errors (6 BouncyCastle warnings expected)
Task 14 complete. Wave 3: 2/5 tasks done.
2026-03-03 19:19:21 +01:00
|
|
|
using (var scope = Factory.Services.CreateScope())
|
|
|
|
|
{
|
|
|
|
|
var context = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
2026-03-05 11:07:19 +01:00
|
|
|
|
feat(tasks): add Task CRUD API with 5-state workflow
- TaskService with CRUD operations + state machine enforcement
- 5 DTOs: TaskListDto, TaskDetailDto, CreateTaskRequest, UpdateTaskRequest
- Minimal API endpoints: GET /api/tasks, GET /api/tasks/{id}, POST, PATCH, DELETE
- State machine: Open→Assigned→InProgress→Review→Done (invalid transitions → 422)
- Concurrency: DbUpdateConcurrencyException → 409 Conflict
- Authorization: POST (Manager+), DELETE (Admin), GET/PATCH (Member+)
- RLS: Automatic tenant filtering via TenantDbConnectionInterceptor
- 10 TDD integration tests: CRUD, state transitions, concurrency, role enforcement
- Build: 0 errors (6 BouncyCastle warnings expected)
Task 14 complete. Wave 3: 2/5 tasks done.
2026-03-03 19:19:21 +01:00
|
|
|
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
|
|
|
|
|
});
|
2026-03-05 11:07:19 +01:00
|
|
|
|
feat(tasks): add Task CRUD API with 5-state workflow
- TaskService with CRUD operations + state machine enforcement
- 5 DTOs: TaskListDto, TaskDetailDto, CreateTaskRequest, UpdateTaskRequest
- Minimal API endpoints: GET /api/tasks, GET /api/tasks/{id}, POST, PATCH, DELETE
- State machine: Open→Assigned→InProgress→Review→Done (invalid transitions → 422)
- Concurrency: DbUpdateConcurrencyException → 409 Conflict
- Authorization: POST (Manager+), DELETE (Admin), GET/PATCH (Member+)
- RLS: Automatic tenant filtering via TenantDbConnectionInterceptor
- 10 TDD integration tests: CRUD, state transitions, concurrency, role enforcement
- Build: 0 errors (6 BouncyCastle warnings expected)
Task 14 complete. Wave 3: 2/5 tasks done.
2026-03-03 19:19:21 +01:00
|
|
|
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();
|
2026-03-05 11:07:19 +01:00
|
|
|
|
feat(tasks): add Task CRUD API with 5-state workflow
- TaskService with CRUD operations + state machine enforcement
- 5 DTOs: TaskListDto, TaskDetailDto, CreateTaskRequest, UpdateTaskRequest
- Minimal API endpoints: GET /api/tasks, GET /api/tasks/{id}, POST, PATCH, DELETE
- State machine: Open→Assigned→InProgress→Review→Done (invalid transitions → 422)
- Concurrency: DbUpdateConcurrencyException → 409 Conflict
- Authorization: POST (Manager+), DELETE (Admin), GET/PATCH (Member+)
- RLS: Automatic tenant filtering via TenantDbConnectionInterceptor
- 10 TDD integration tests: CRUD, state transitions, concurrency, role enforcement
- Build: 0 errors (6 BouncyCastle warnings expected)
Task 14 complete. Wave 3: 2/5 tasks done.
2026-03-03 19:19:21 +01:00
|
|
|
using (var scope = Factory.Services.CreateScope())
|
|
|
|
|
{
|
|
|
|
|
var context = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
2026-03-05 11:07:19 +01:00
|
|
|
|
feat(tasks): add Task CRUD API with 5-state workflow
- TaskService with CRUD operations + state machine enforcement
- 5 DTOs: TaskListDto, TaskDetailDto, CreateTaskRequest, UpdateTaskRequest
- Minimal API endpoints: GET /api/tasks, GET /api/tasks/{id}, POST, PATCH, DELETE
- State machine: Open→Assigned→InProgress→Review→Done (invalid transitions → 422)
- Concurrency: DbUpdateConcurrencyException → 409 Conflict
- Authorization: POST (Manager+), DELETE (Admin), GET/PATCH (Member+)
- RLS: Automatic tenant filtering via TenantDbConnectionInterceptor
- 10 TDD integration tests: CRUD, state transitions, concurrency, role enforcement
- Build: 0 errors (6 BouncyCastle warnings expected)
Task 14 complete. Wave 3: 2/5 tasks done.
2026-03-03 19:19:21 +01:00
|
|
|
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
|
|
|
|
|
});
|
2026-03-05 11:07:19 +01:00
|
|
|
|
feat(tasks): add Task CRUD API with 5-state workflow
- TaskService with CRUD operations + state machine enforcement
- 5 DTOs: TaskListDto, TaskDetailDto, CreateTaskRequest, UpdateTaskRequest
- Minimal API endpoints: GET /api/tasks, GET /api/tasks/{id}, POST, PATCH, DELETE
- State machine: Open→Assigned→InProgress→Review→Done (invalid transitions → 422)
- Concurrency: DbUpdateConcurrencyException → 409 Conflict
- Authorization: POST (Manager+), DELETE (Admin), GET/PATCH (Member+)
- RLS: Automatic tenant filtering via TenantDbConnectionInterceptor
- 10 TDD integration tests: CRUD, state transitions, concurrency, role enforcement
- Build: 0 errors (6 BouncyCastle warnings expected)
Task 14 complete. Wave 3: 2/5 tasks done.
2026-03-03 19:19:21 +01:00
|
|
|
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" };
|
2026-03-05 11:07:19 +01:00
|
|
|
|
feat(tasks): add Task CRUD API with 5-state workflow
- TaskService with CRUD operations + state machine enforcement
- 5 DTOs: TaskListDto, TaskDetailDto, CreateTaskRequest, UpdateTaskRequest
- Minimal API endpoints: GET /api/tasks, GET /api/tasks/{id}, POST, PATCH, DELETE
- State machine: Open→Assigned→InProgress→Review→Done (invalid transitions → 422)
- Concurrency: DbUpdateConcurrencyException → 409 Conflict
- Authorization: POST (Manager+), DELETE (Admin), GET/PATCH (Member+)
- RLS: Automatic tenant filtering via TenantDbConnectionInterceptor
- 10 TDD integration tests: CRUD, state transitions, concurrency, role enforcement
- Build: 0 errors (6 BouncyCastle warnings expected)
Task 14 complete. Wave 3: 2/5 tasks done.
2026-03-03 19:19:21 +01:00
|
|
|
// 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();
|
2026-03-05 11:07:19 +01:00
|
|
|
|
feat(tasks): add Task CRUD API with 5-state workflow
- TaskService with CRUD operations + state machine enforcement
- 5 DTOs: TaskListDto, TaskDetailDto, CreateTaskRequest, UpdateTaskRequest
- Minimal API endpoints: GET /api/tasks, GET /api/tasks/{id}, POST, PATCH, DELETE
- State machine: Open→Assigned→InProgress→Review→Done (invalid transitions → 422)
- Concurrency: DbUpdateConcurrencyException → 409 Conflict
- Authorization: POST (Manager+), DELETE (Admin), GET/PATCH (Member+)
- RLS: Automatic tenant filtering via TenantDbConnectionInterceptor
- 10 TDD integration tests: CRUD, state transitions, concurrency, role enforcement
- Build: 0 errors (6 BouncyCastle warnings expected)
Task 14 complete. Wave 3: 2/5 tasks done.
2026-03-03 19:19:21 +01:00
|
|
|
using (var scope = Factory.Services.CreateScope())
|
|
|
|
|
{
|
|
|
|
|
var context = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
2026-03-05 11:07:19 +01:00
|
|
|
|
feat(tasks): add Task CRUD API with 5-state workflow
- TaskService with CRUD operations + state machine enforcement
- 5 DTOs: TaskListDto, TaskDetailDto, CreateTaskRequest, UpdateTaskRequest
- Minimal API endpoints: GET /api/tasks, GET /api/tasks/{id}, POST, PATCH, DELETE
- State machine: Open→Assigned→InProgress→Review→Done (invalid transitions → 422)
- Concurrency: DbUpdateConcurrencyException → 409 Conflict
- Authorization: POST (Manager+), DELETE (Admin), GET/PATCH (Member+)
- RLS: Automatic tenant filtering via TenantDbConnectionInterceptor
- 10 TDD integration tests: CRUD, state transitions, concurrency, role enforcement
- Build: 0 errors (6 BouncyCastle warnings expected)
Task 14 complete. Wave 3: 2/5 tasks done.
2026-03-03 19:19:21 +01:00
|
|
|
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
|
|
|
|
|
});
|
2026-03-05 11:07:19 +01:00
|
|
|
|
feat(tasks): add Task CRUD API with 5-state workflow
- TaskService with CRUD operations + state machine enforcement
- 5 DTOs: TaskListDto, TaskDetailDto, CreateTaskRequest, UpdateTaskRequest
- Minimal API endpoints: GET /api/tasks, GET /api/tasks/{id}, POST, PATCH, DELETE
- State machine: Open→Assigned→InProgress→Review→Done (invalid transitions → 422)
- Concurrency: DbUpdateConcurrencyException → 409 Conflict
- Authorization: POST (Manager+), DELETE (Admin), GET/PATCH (Member+)
- RLS: Automatic tenant filtering via TenantDbConnectionInterceptor
- 10 TDD integration tests: CRUD, state transitions, concurrency, role enforcement
- Build: 0 errors (6 BouncyCastle warnings expected)
Task 14 complete. Wave 3: 2/5 tasks done.
2026-03-03 19:19:21 +01:00
|
|
|
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);
|
2026-03-05 11:07:19 +01:00
|
|
|
|
feat(tasks): add Task CRUD API with 5-state workflow
- TaskService with CRUD operations + state machine enforcement
- 5 DTOs: TaskListDto, TaskDetailDto, CreateTaskRequest, UpdateTaskRequest
- Minimal API endpoints: GET /api/tasks, GET /api/tasks/{id}, POST, PATCH, DELETE
- State machine: Open→Assigned→InProgress→Review→Done (invalid transitions → 422)
- Concurrency: DbUpdateConcurrencyException → 409 Conflict
- Authorization: POST (Manager+), DELETE (Admin), GET/PATCH (Member+)
- RLS: Automatic tenant filtering via TenantDbConnectionInterceptor
- 10 TDD integration tests: CRUD, state transitions, concurrency, role enforcement
- Build: 0 errors (6 BouncyCastle warnings expected)
Task 14 complete. Wave 3: 2/5 tasks done.
2026-03-03 19:19:21 +01:00
|
|
|
// 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();
|
2026-03-05 11:07:19 +01:00
|
|
|
|
feat(tasks): add Task CRUD API with 5-state workflow
- TaskService with CRUD operations + state machine enforcement
- 5 DTOs: TaskListDto, TaskDetailDto, CreateTaskRequest, UpdateTaskRequest
- Minimal API endpoints: GET /api/tasks, GET /api/tasks/{id}, POST, PATCH, DELETE
- State machine: Open→Assigned→InProgress→Review→Done (invalid transitions → 422)
- Concurrency: DbUpdateConcurrencyException → 409 Conflict
- Authorization: POST (Manager+), DELETE (Admin), GET/PATCH (Member+)
- RLS: Automatic tenant filtering via TenantDbConnectionInterceptor
- 10 TDD integration tests: CRUD, state transitions, concurrency, role enforcement
- Build: 0 errors (6 BouncyCastle warnings expected)
Task 14 complete. Wave 3: 2/5 tasks done.
2026-03-03 19:19:21 +01:00
|
|
|
using (var scope = Factory.Services.CreateScope())
|
|
|
|
|
{
|
|
|
|
|
var context = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
2026-03-05 11:07:19 +01:00
|
|
|
|
feat(tasks): add Task CRUD API with 5-state workflow
- TaskService with CRUD operations + state machine enforcement
- 5 DTOs: TaskListDto, TaskDetailDto, CreateTaskRequest, UpdateTaskRequest
- Minimal API endpoints: GET /api/tasks, GET /api/tasks/{id}, POST, PATCH, DELETE
- State machine: Open→Assigned→InProgress→Review→Done (invalid transitions → 422)
- Concurrency: DbUpdateConcurrencyException → 409 Conflict
- Authorization: POST (Manager+), DELETE (Admin), GET/PATCH (Member+)
- RLS: Automatic tenant filtering via TenantDbConnectionInterceptor
- 10 TDD integration tests: CRUD, state transitions, concurrency, role enforcement
- Build: 0 errors (6 BouncyCastle warnings expected)
Task 14 complete. Wave 3: 2/5 tasks done.
2026-03-03 19:19:21 +01:00
|
|
|
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
|
|
|
|
|
});
|
2026-03-05 11:07:19 +01:00
|
|
|
|
feat(tasks): add Task CRUD API with 5-state workflow
- TaskService with CRUD operations + state machine enforcement
- 5 DTOs: TaskListDto, TaskDetailDto, CreateTaskRequest, UpdateTaskRequest
- Minimal API endpoints: GET /api/tasks, GET /api/tasks/{id}, POST, PATCH, DELETE
- State machine: Open→Assigned→InProgress→Review→Done (invalid transitions → 422)
- Concurrency: DbUpdateConcurrencyException → 409 Conflict
- Authorization: POST (Manager+), DELETE (Admin), GET/PATCH (Member+)
- RLS: Automatic tenant filtering via TenantDbConnectionInterceptor
- 10 TDD integration tests: CRUD, state transitions, concurrency, role enforcement
- Build: 0 errors (6 BouncyCastle warnings expected)
Task 14 complete. Wave 3: 2/5 tasks done.
2026-03-03 19:19:21 +01:00
|
|
|
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);
|