486 lines
15 KiB
C#
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
|
|
}
|