work-flow/tests/Workflow.Tests/Handlers/WorkflowTaskHandlerTests.cs
向宁 fc4ecbbacc feat: add gRPC auth, condition comparators, seed data, EF migrations
- 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
2026-05-20 20:28:35 +08:00

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
}