- gRPC auth service for token validation - Value comparator system (string, numeric, boolean, datetime, collection) - Condition evaluator with strategy chain - Form definition and data improvements - Workflow instance/task endpoints updated - Seed data and EF design-time factory - Test coverage for comparators and handlers
468 lines
13 KiB
C#
468 lines
13 KiB
C#
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<WorkflowDbContext>()
|
|
.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<UnauthorizedException>()
|
|
.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
|
|
}
|