using FluentAssertions; using Microsoft.EntityFrameworkCore; using Workflow.Application.Engine; 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); } private static async Task<(WorkflowDbContext db, Guid instanceId, Guid tokenId, Guid nodeId)> SeedTaskWithWorkflowAsync( Guid taskId, Guid assigneeId, Guid? tokenId = null, Guid? nodeId = null) { var db = CreateDbContext(); var definitionId = Guid.NewGuid(); var instId = Guid.NewGuid(); var tokId = tokenId ?? Guid.NewGuid(); var ndId = nodeId ?? Guid.NewGuid(); var endNodeId = Guid.NewGuid(); db.WorkflowDefinitions.Add(new WorkflowDefinition { Id = definitionId, Name = "Test", Code = "test", Status = DefinitionStatus.Published, IsEnabled = true }); db.WorkflowInstances.Add(new WorkflowInstance { Id = instId, DefinitionId = definitionId, Status = InstanceStatus.Running }); db.WorkflowTokens.Add(new WorkflowToken { Id = tokId, InstanceId = instId, NodeId = ndId, Status = TokenStatus.Active }); db.WorkflowNodes.Add(new WorkflowNode { Id = ndId, DefinitionId = definitionId, NodeType = NodeType.Approval, Name = "Approve" }); db.WorkflowNodes.Add(new WorkflowNode { Id = endNodeId, DefinitionId = definitionId, NodeType = NodeType.End, Name = "End" }); db.WorkflowEdges.Add(new WorkflowEdge { Id = Guid.NewGuid(), DefinitionId = definitionId, SourceNodeId = ndId, TargetNodeId = endNodeId, EdgeType = EdgeType.Approved }); db.WorkflowEdges.Add(new WorkflowEdge { Id = Guid.NewGuid(), DefinitionId = definitionId, SourceNodeId = ndId, TargetNodeId = endNodeId, EdgeType = EdgeType.Rejected }); db.WorkflowTasks.Add(new WorkflowTask { Id = taskId, InstanceId = instId, TokenId = tokId, NodeId = ndId, AssigneeId = assigneeId, Status = TaskStatus.Pending }); await db.SaveChangesAsync(); return (db, instId, tokId, ndId); } #region ApproveTask [Fact] public async Task ApproveTask_UpdatesTaskStatus() { // Arrange var taskId = Guid.NewGuid(); var assigneeId = Guid.NewGuid(); var (db, _, _, _) = await SeedTaskWithWorkflowAsync(taskId, assigneeId); var handler = new ApproveTaskCommandHandler(db, new ProcessEngine(db, null!, new())); 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 var taskId = Guid.NewGuid(); var assigneeId = Guid.NewGuid(); var (db, _, _, _) = await SeedTaskWithWorkflowAsync(taskId, assigneeId); var beforeApprove = DateTime.UtcNow; var handler = new ApproveTaskCommandHandler(db, new ProcessEngine(db, null!, new())); 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, new ProcessEngine(db, null!, new())); 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 var taskId = Guid.NewGuid(); var assigneeId = Guid.NewGuid(); var (db, _, _, _) = await SeedTaskWithWorkflowAsync(taskId, assigneeId); var handler = new RejectTaskCommandHandler(db, new ProcessEngine(db, null!, new())); 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 }