using FluentAssertions; using Microsoft.EntityFrameworkCore; using Workflow.Application.Features.WorkflowTasks.Commands; using Workflow.Application.Features.WorkflowTasks.Queries; using Workflow.Domain.Entities; using Workflow.Domain.Enums; using Workflow.Domain.Exceptions; using Workflow.Infrastructure.Persistence; using Xunit; using TaskStatus = Workflow.Domain.Enums.TaskStatus; namespace Workflow.Tests.Handlers; public class WorkflowTaskHandlerTests { private static WorkflowDbContext CreateDbContext() { var options = new DbContextOptionsBuilder() .UseInMemoryDatabase(Guid.NewGuid().ToString()) .Options; return new WorkflowDbContext(options); } #region ApproveTask [Fact] public async Task ApproveTask_UpdatesTaskStatus() { // Arrange await using var db = CreateDbContext(); var taskId = Guid.NewGuid(); var assigneeId = Guid.NewGuid(); var task = new WorkflowTask { Id = taskId, InstanceId = Guid.NewGuid(), TokenId = Guid.NewGuid(), AssigneeId = assigneeId, Status = TaskStatus.Pending }; db.WorkflowTasks.Add(task); await db.SaveChangesAsync(); var handler = new ApproveTaskCommandHandler(db); var command = new ApproveTaskCommand( TaskId: taskId, UserId: assigneeId, Comment: "Looks good" ); // Act await handler.Handle(command, CancellationToken.None); // Assert var updated = await db.WorkflowTasks.FindAsync(taskId); updated.Should().NotBeNull(); updated!.Status.Should().Be(TaskStatus.Approved); } [Fact] public async Task ApproveTask_SetsCompletedAt() { // Arrange await using var db = CreateDbContext(); var taskId = Guid.NewGuid(); var assigneeId = Guid.NewGuid(); var task = new WorkflowTask { Id = taskId, InstanceId = Guid.NewGuid(), TokenId = Guid.NewGuid(), AssigneeId = assigneeId, Status = TaskStatus.Pending }; db.WorkflowTasks.Add(task); await db.SaveChangesAsync(); var beforeApprove = DateTime.UtcNow; var handler = new ApproveTaskCommandHandler(db); var command = new ApproveTaskCommand( TaskId: taskId, UserId: assigneeId, Comment: "Approved" ); // Act await handler.Handle(command, CancellationToken.None); // Assert var updated = await db.WorkflowTasks.FindAsync(taskId); updated.Should().NotBeNull(); updated!.CompletedAt.Should().NotBeNull(); updated.CompletedAt.Should().BeOnOrAfter(beforeApprove); } [Fact] public async Task ApproveTask_ByNonAssignee_ThrowsUnauthorizedException() { // Arrange await using var db = CreateDbContext(); var taskId = Guid.NewGuid(); var assigneeId = Guid.NewGuid(); var otherUserId = Guid.NewGuid(); var task = new WorkflowTask { Id = taskId, InstanceId = Guid.NewGuid(), TokenId = Guid.NewGuid(), AssigneeId = assigneeId, Status = TaskStatus.Pending }; db.WorkflowTasks.Add(task); await db.SaveChangesAsync(); var handler = new ApproveTaskCommandHandler(db); var command = new ApproveTaskCommand( TaskId: taskId, UserId: otherUserId, Comment: "Impersonation attempt" ); // Act var act = () => handler.Handle(command, CancellationToken.None); // Assert await act.Should().ThrowAsync() .WithMessage("*assignee*"); } #endregion #region RejectTask [Fact] public async Task RejectTask_UpdatesTaskStatusWithComment() { // Arrange await using var db = CreateDbContext(); var taskId = Guid.NewGuid(); var assigneeId = Guid.NewGuid(); var task = new WorkflowTask { Id = taskId, InstanceId = Guid.NewGuid(), TokenId = Guid.NewGuid(), AssigneeId = assigneeId, Status = TaskStatus.Pending }; db.WorkflowTasks.Add(task); await db.SaveChangesAsync(); var handler = new RejectTaskCommandHandler(db); var command = new RejectTaskCommand( TaskId: taskId, UserId: assigneeId, Comment: "Missing required documents" ); // Act await handler.Handle(command, CancellationToken.None); // Assert var updated = await db.WorkflowTasks.FindAsync(taskId); updated.Should().NotBeNull(); updated!.Status.Should().Be(TaskStatus.Rejected); updated.Comment.Should().Be("Missing required documents"); updated.CompletedAt.Should().NotBeNull(); } #endregion #region TransferTask [Fact] public async Task TransferTask_CreatesNewTaskForTargetUser() { // Arrange await using var db = CreateDbContext(); var taskId = Guid.NewGuid(); var fromUserId = Guid.NewGuid(); var toUserId = Guid.NewGuid(); var instanceId = Guid.NewGuid(); var tokenId = Guid.NewGuid(); var task = new WorkflowTask { Id = taskId, InstanceId = instanceId, TokenId = tokenId, AssigneeId = fromUserId, Status = TaskStatus.Pending }; db.WorkflowTasks.Add(task); await db.SaveChangesAsync(); var handler = new TransferTaskCommandHandler(db); var command = new TransferTaskCommand( TaskId: taskId, FromUserId: fromUserId, ToUserId: toUserId ); // Act await handler.Handle(command, CancellationToken.None); // Assert - a new task for the target user should exist var newTask = await db.WorkflowTasks .FirstOrDefaultAsync(t => t.AssigneeId == toUserId && t.Id != taskId); newTask.Should().NotBeNull(); newTask!.Status.Should().Be(TaskStatus.Pending); newTask.InstanceId.Should().Be(instanceId); } [Fact] public async Task TransferTask_ClosesOriginalTask() { // Arrange await using var db = CreateDbContext(); var taskId = Guid.NewGuid(); var fromUserId = Guid.NewGuid(); var toUserId = Guid.NewGuid(); var task = new WorkflowTask { Id = taskId, InstanceId = Guid.NewGuid(), TokenId = Guid.NewGuid(), AssigneeId = fromUserId, Status = TaskStatus.Pending }; db.WorkflowTasks.Add(task); await db.SaveChangesAsync(); var handler = new TransferTaskCommandHandler(db); var command = new TransferTaskCommand( TaskId: taskId, FromUserId: fromUserId, ToUserId: toUserId ); // Act await handler.Handle(command, CancellationToken.None); // Assert var original = await db.WorkflowTasks.FindAsync(taskId); original.Should().NotBeNull(); original!.Status.Should().Be(TaskStatus.Transferred); original.CompletedAt.Should().NotBeNull(); } #endregion #region DelegateTask [Fact] public async Task DelegateTask_SetsTaskToDelegated() { // Arrange await using var db = CreateDbContext(); var taskId = Guid.NewGuid(); var fromUserId = Guid.NewGuid(); var toUserId = Guid.NewGuid(); var instanceId = Guid.NewGuid(); var tokenId = Guid.NewGuid(); var task = new WorkflowTask { Id = taskId, InstanceId = instanceId, TokenId = tokenId, AssigneeId = fromUserId, Status = TaskStatus.Pending }; db.WorkflowTasks.Add(task); await db.SaveChangesAsync(); var handler = new DelegateTaskCommandHandler(db); var command = new DelegateTaskCommand( TaskId: taskId, FromUserId: fromUserId, ToUserId: toUserId ); // Act await handler.Handle(command, CancellationToken.None); // Assert - original task should be marked as delegated var original = await db.WorkflowTasks.FindAsync(taskId); original.Should().NotBeNull(); original!.Status.Should().Be(TaskStatus.Delegated); // A new delegated task should exist for the target user var delegatedTask = await db.WorkflowTasks .FirstOrDefaultAsync(t => t.AssigneeId == toUserId && t.DelegatedFromId == fromUserId); delegatedTask.Should().NotBeNull(); delegatedTask!.Status.Should().Be(TaskStatus.Pending); } #endregion #region GetPendingTasks [Fact] public async Task GetPendingTasks_ReturnsOnlyUserTasks() { // Arrange await using var db = CreateDbContext(); var userId = Guid.NewGuid(); var otherUserId = Guid.NewGuid(); db.WorkflowTasks.Add(new WorkflowTask { Id = Guid.NewGuid(), InstanceId = Guid.NewGuid(), TokenId = Guid.NewGuid(), AssigneeId = userId, Status = TaskStatus.Pending }); db.WorkflowTasks.Add(new WorkflowTask { Id = Guid.NewGuid(), InstanceId = Guid.NewGuid(), TokenId = Guid.NewGuid(), AssigneeId = userId, Status = TaskStatus.Pending }); db.WorkflowTasks.Add(new WorkflowTask { Id = Guid.NewGuid(), InstanceId = Guid.NewGuid(), TokenId = Guid.NewGuid(), AssigneeId = otherUserId, Status = TaskStatus.Pending }); await db.SaveChangesAsync(); var handler = new GetPendingTasksQueryHandler(db); var query = new GetPendingTasksQuery( UserId: userId, PageIndex: 1, PageSize: 10 ); // Act var result = await handler.Handle(query, CancellationToken.None); // Assert result.Should().NotBeNull(); result.Items.Should().HaveCount(2); result.Items.Should().OnlyContain(t => t.AssigneeId == userId); } [Fact] public async Task GetPendingTasks_ExcludesCompletedTasks() { // Arrange await using var db = CreateDbContext(); var userId = Guid.NewGuid(); db.WorkflowTasks.Add(new WorkflowTask { Id = Guid.NewGuid(), InstanceId = Guid.NewGuid(), TokenId = Guid.NewGuid(), AssigneeId = userId, Status = TaskStatus.Pending }); db.WorkflowTasks.Add(new WorkflowTask { Id = Guid.NewGuid(), InstanceId = Guid.NewGuid(), TokenId = Guid.NewGuid(), AssigneeId = userId, Status = TaskStatus.Approved, CompletedAt = DateTime.UtcNow }); db.WorkflowTasks.Add(new WorkflowTask { Id = Guid.NewGuid(), InstanceId = Guid.NewGuid(), TokenId = Guid.NewGuid(), AssigneeId = userId, Status = TaskStatus.Rejected, CompletedAt = DateTime.UtcNow }); await db.SaveChangesAsync(); var handler = new GetPendingTasksQueryHandler(db); var query = new GetPendingTasksQuery( UserId: userId, PageIndex: 1, PageSize: 10 ); // Act var result = await handler.Handle(query, CancellationToken.None); // Assert result.Should().NotBeNull(); result.Items.Should().HaveCount(1); result.Items[0].Status.Should().Be(TaskStatus.Pending); } #endregion #region GetHistoryTasks [Fact] public async Task GetHistoryTasks_ReturnsOnlyProcessedTasks() { // Arrange await using var db = CreateDbContext(); var userId = Guid.NewGuid(); db.WorkflowTasks.Add(new WorkflowTask { Id = Guid.NewGuid(), InstanceId = Guid.NewGuid(), TokenId = Guid.NewGuid(), AssigneeId = userId, Status = TaskStatus.Approved, CompletedAt = DateTime.UtcNow }); db.WorkflowTasks.Add(new WorkflowTask { Id = Guid.NewGuid(), InstanceId = Guid.NewGuid(), TokenId = Guid.NewGuid(), AssigneeId = userId, Status = TaskStatus.Rejected, CompletedAt = DateTime.UtcNow }); db.WorkflowTasks.Add(new WorkflowTask { Id = Guid.NewGuid(), InstanceId = Guid.NewGuid(), TokenId = Guid.NewGuid(), AssigneeId = userId, Status = TaskStatus.Pending }); await db.SaveChangesAsync(); var handler = new GetHistoryTasksQueryHandler(db); var query = new GetHistoryTasksQuery( UserId: userId, PageIndex: 1, PageSize: 10 ); // Act var result = await handler.Handle(query, CancellationToken.None); // Assert result.Should().NotBeNull(); result.Items.Should().HaveCount(2); result.Items.Should().OnlyContain(t => t.Status == TaskStatus.Approved || t.Status == TaskStatus.Rejected); } #endregion }