work-flow/tests/Workflow.Tests/Handlers/WorkflowTaskHandlerTests.cs

486 lines
15 KiB
C#

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<WorkflowDbContext>()
.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<UnauthorizedException>()
.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
}