- 从多个文件中移除未使用的 System.Linq.Dynamic.Core 引用 - 从多个文件中移除未使用的 Microsoft.EntityFrameworkCore 引用 - 从 IM 模块中移除未使用的 MongoDB.Driver.Linq 引用 - 清理 Handler 文件中的冗余命名空间引用 - 优化代码结构减少不必要的依赖引入 - 提高代码整洁性和编译效率
482 lines
18 KiB
C#
482 lines
18 KiB
C#
using Workflow.Application.Engine;
|
||
using Workflow.Domain.Entities;
|
||
using Workflow.Domain.Enums;
|
||
using Workflow.Domain.Expressions;
|
||
using TaskStatus = Workflow.Domain.Enums.TaskStatus;
|
||
|
||
namespace BuildFlow.Workflow.Tests.Engine;
|
||
|
||
using FluentAssertions;
|
||
using Microsoft.EntityFrameworkCore;
|
||
using Microsoft.Extensions.DependencyInjection;
|
||
using global::BuildFlow.Shared.Domain.Common;
|
||
using Xunit;
|
||
|
||
/// <summary>
|
||
/// 会签/或签/退回测试(TDD)。
|
||
///
|
||
/// 设计:在 Approval 节点 config 增加字段(不新增 NodeType,向后兼容):
|
||
/// - approvalMode: "single"(默认) | "countersign"(会签-全部通过) | "any"(或签-任一通过)
|
||
/// - assigneeRules: ["user:xxx", "user:yyy", ...] 多受理人
|
||
/// - minApprovals: 会签模式下最少通过数(默认=全部)
|
||
/// </summary>
|
||
public class MultiApprovalTests
|
||
{
|
||
private readonly WorkflowDbContext _dbContext;
|
||
private readonly ProcessEngine _engine;
|
||
|
||
public MultiApprovalTests()
|
||
{
|
||
var options = new DbContextOptionsBuilder<WorkflowDbContext>()
|
||
.UseInMemoryDatabase(Guid.NewGuid().ToString())
|
||
.Options;
|
||
_dbContext = new WorkflowDbContext(options);
|
||
|
||
// 模拟 AuditInterceptor:InMemory 不跑拦截器,此处手动填充审计字段,
|
||
// 使 StartAsync/CompleteTaskAsync 创建的 token/task 能通过非空校验。
|
||
_dbContext.SavingChanges += (sender, _) =>
|
||
{
|
||
if (sender is not DbContext ctx) return;
|
||
foreach (var entry in ctx.ChangeTracker.Entries())
|
||
{
|
||
if (entry.Entity is IAuditable a)
|
||
{
|
||
a.CreatedBy ??= "test";
|
||
a.UpdatedBy ??= "test";
|
||
}
|
||
if (entry.Entity is IHasOperatorIP o)
|
||
{
|
||
o.OperatorIP ??= "127.0.0.1";
|
||
}
|
||
}
|
||
};
|
||
|
||
_engine = new ProcessEngine(_dbContext, new ServiceCollection().BuildServiceProvider(), new ConditionEvaluator());
|
||
}
|
||
|
||
// ============================================================
|
||
// 会签(Countersign):全部通过才推进
|
||
// ============================================================
|
||
|
||
/// <summary>
|
||
/// 会签模式:3 个受理人,第 1 个通过后流程不推进(仍有待办任务)。
|
||
/// </summary>
|
||
[Fact]
|
||
public async Task Countersign_PartialApproval_DoesNotPropagate()
|
||
{
|
||
var user1 = Guid.NewGuid();
|
||
var user2 = Guid.NewGuid();
|
||
var user3 = Guid.NewGuid();
|
||
|
||
var approvalNode = new WorkflowNode
|
||
{
|
||
Id = Guid.NewGuid(),
|
||
NodeType = NodeType.Approval,
|
||
Name = "会签节点",
|
||
CreatedBy = "test",
|
||
UpdatedBy = "test",
|
||
Config = $$"""{ "approvalMode": "countersign", "assigneeRules": ["user:{{user1}}", "user:{{user2}}", "user:{{user3}}"] }""",
|
||
};
|
||
var startNode = CreateNode(NodeType.Start);
|
||
var endNode = CreateNode(NodeType.End);
|
||
var edgeStart = CreateEdge(startNode, approvalNode);
|
||
var edge = CreateEdge(approvalNode, endNode);
|
||
|
||
var (_, instance) = PersistSetup([startNode, approvalNode, endNode], [edgeStart, edge]);
|
||
await _engine.StartAsync(instance);
|
||
|
||
// 应为 3 个受理人各创建 1 个 Pending 任务
|
||
var tasks = await _dbContext.WorkflowTasks
|
||
.Where(t => t.NodeId == approvalNode.Id && t.Status == TaskStatus.Pending)
|
||
.ToListAsync();
|
||
tasks.Should().HaveCount(3);
|
||
|
||
// 第 1 个人通过
|
||
await _engine.CompleteTaskAsync(tasks[0], TaskResult.Approved);
|
||
|
||
// 还有 2 个待办 → 流程不应推进到 End
|
||
var remainingPending = await _dbContext.WorkflowTasks
|
||
.CountAsync(t => t.NodeId == approvalNode.Id && t.Status == TaskStatus.Pending);
|
||
remainingPending.Should().Be(2);
|
||
|
||
var endTokens = await _dbContext.WorkflowTokens
|
||
.CountAsync(t => t.NodeId == endNode.Id);
|
||
endTokens.Should().Be(0, "会签未完成,token 不应到达 End");
|
||
}
|
||
|
||
/// <summary>
|
||
/// 会签模式:全部通过后流程才推进到下游。
|
||
/// </summary>
|
||
[Fact]
|
||
public async Task Countersign_AllApproved_PropagatesDownstream()
|
||
{
|
||
var user1 = Guid.NewGuid();
|
||
var user2 = Guid.NewGuid();
|
||
|
||
var approvalNode = new WorkflowNode
|
||
{
|
||
Id = Guid.NewGuid(),
|
||
NodeType = NodeType.Approval,
|
||
Name = "approval",
|
||
CreatedBy = "test",
|
||
UpdatedBy = "test",
|
||
Config = $$"""{ "approvalMode": "countersign", "assigneeRules": ["user:{{user1}}", "user:{{user2}}"] }""",
|
||
};
|
||
var startNode = CreateNode(NodeType.Start);
|
||
var endNode = CreateNode(NodeType.End);
|
||
var edgeStart = CreateEdge(startNode, approvalNode);
|
||
var edge = CreateEdge(approvalNode, endNode);
|
||
|
||
var (_, instance) = PersistSetup([startNode, approvalNode, endNode], [edgeStart, edge]);
|
||
await _engine.StartAsync(instance);
|
||
|
||
var tasks = await _dbContext.WorkflowTasks
|
||
.Where(t => t.NodeId == approvalNode.Id)
|
||
.ToListAsync();
|
||
|
||
// 全部通过
|
||
await _engine.CompleteTaskAsync(tasks[0], TaskResult.Approved);
|
||
await _engine.CompleteTaskAsync(tasks[1], TaskResult.Approved);
|
||
|
||
// 实例应完成(token 到达 End 被消费)
|
||
instance.Status.Should().Be(InstanceStatus.Completed);
|
||
}
|
||
|
||
// ============================================================
|
||
// 或签(Any):任一通过即推进
|
||
// ============================================================
|
||
|
||
/// <summary>
|
||
/// 或签模式:第 1 个通过后立即推进,其余任务自动关闭。
|
||
/// </summary>
|
||
[Fact]
|
||
public async Task AnyApproval_FirstApproval_PropagatesAndClosesOthers()
|
||
{
|
||
var user1 = Guid.NewGuid();
|
||
var user2 = Guid.NewGuid();
|
||
|
||
var approvalNode = new WorkflowNode
|
||
{
|
||
Id = Guid.NewGuid(),
|
||
NodeType = NodeType.Approval,
|
||
Name = "approval",
|
||
CreatedBy = "test",
|
||
UpdatedBy = "test",
|
||
Config = $$"""{ "approvalMode": "any", "assigneeRules": ["user:{{user1}}", "user:{{user2}}"] }""",
|
||
};
|
||
var startNode = CreateNode(NodeType.Start);
|
||
var endNode = CreateNode(NodeType.End);
|
||
var edgeStart = CreateEdge(startNode, approvalNode);
|
||
var edge = CreateEdge(approvalNode, endNode);
|
||
|
||
var (_, instance) = PersistSetup([startNode, approvalNode, endNode], [edgeStart, edge]);
|
||
await _engine.StartAsync(instance);
|
||
|
||
var tasks = await _dbContext.WorkflowTasks
|
||
.Where(t => t.NodeId == approvalNode.Id && t.Status == TaskStatus.Pending)
|
||
.ToListAsync();
|
||
tasks.Should().HaveCount(2);
|
||
|
||
// 第 1 个人通过
|
||
await _engine.CompleteTaskAsync(tasks[0], TaskResult.Approved);
|
||
|
||
// 流程应已推进 → 实例完成
|
||
instance.Status.Should().Be(InstanceStatus.Completed);
|
||
|
||
// 其余待办任务应自动关闭(不应残留 Pending)
|
||
var remainingPending = await _dbContext.WorkflowTasks
|
||
.CountAsync(t => t.NodeId == approvalNode.Id && t.Status == TaskStatus.Pending);
|
||
remainingPending.Should().Be(0, "或签通过后其余任务应自动关闭");
|
||
}
|
||
|
||
// ============================================================
|
||
// 向后兼容:无 approvalMode 时保持单审批行为
|
||
// ============================================================
|
||
|
||
[Fact]
|
||
public async Task NoApprovalMode_DefaultsToSingleApproval()
|
||
{
|
||
var approvalNode = new WorkflowNode
|
||
{
|
||
Id = Guid.NewGuid(),
|
||
NodeType = NodeType.Approval,
|
||
Name = "approval",
|
||
CreatedBy = "test",
|
||
UpdatedBy = "test",
|
||
Config = """{ "assigneeRule": "role:approver" }""",
|
||
};
|
||
var startNode = CreateNode(NodeType.Start);
|
||
var endNode = CreateNode(NodeType.End);
|
||
var edgeStart = CreateEdge(startNode, approvalNode);
|
||
var edge = CreateEdge(approvalNode, endNode);
|
||
|
||
var (_, instance) = PersistSetup([startNode, approvalNode, endNode], [edgeStart, edge]);
|
||
await _engine.StartAsync(instance);
|
||
|
||
var tasks = await _dbContext.WorkflowTasks
|
||
.CountAsync(t => t.NodeId == approvalNode.Id && t.Status == TaskStatus.Pending);
|
||
tasks.Should().Be(1, "无 approvalMode 默认单审批");
|
||
}
|
||
|
||
// ============================================================
|
||
// 退回(Return):回到上一审批节点
|
||
// ============================================================
|
||
|
||
/// <summary>
|
||
/// 退回:第 2 个审批节点退回时,token 回到第 1 个审批节点重新创建任务。
|
||
/// </summary>
|
||
[Fact]
|
||
public async Task ReturnTask_GoesBackToPreviousApprovalNode()
|
||
{
|
||
var firstApproval = new WorkflowNode
|
||
{
|
||
Id = Guid.NewGuid(),
|
||
NodeType = NodeType.Approval,
|
||
Name = "approval",
|
||
CreatedBy = "test",
|
||
UpdatedBy = "test",
|
||
Config = """{ "assigneeRule": "role:approver1" }""",
|
||
};
|
||
var secondApproval = new WorkflowNode
|
||
{
|
||
Id = Guid.NewGuid(),
|
||
NodeType = NodeType.Approval,
|
||
Name = "approval",
|
||
CreatedBy = "test",
|
||
UpdatedBy = "test",
|
||
Config = """{ "assigneeRule": "role:approver2" }""",
|
||
};
|
||
var endNode = CreateNode(NodeType.End);
|
||
var startNode = CreateNode(NodeType.Start);
|
||
var edgeStart = CreateEdge(startNode, firstApproval);
|
||
var edge1 = CreateEdge(firstApproval, secondApproval);
|
||
var edge2 = CreateEdge(secondApproval, endNode);
|
||
|
||
var (_, instance) = PersistSetup([startNode, firstApproval, secondApproval, endNode], [edgeStart, edge1, edge2]);
|
||
await _engine.StartAsync(instance);
|
||
|
||
// 第 1 个审批通过 → token 到第 2 个审批
|
||
var task1 = await _dbContext.WorkflowTasks.FirstAsync(t => t.NodeId == firstApproval.Id);
|
||
await _engine.CompleteTaskAsync(task1, TaskResult.Approved);
|
||
|
||
// 第 2 个审批任务存在
|
||
var task2 = await _dbContext.WorkflowTasks.FirstAsync(t => t.NodeId == secondApproval.Id);
|
||
task2.Status.Should().Be(TaskStatus.Pending);
|
||
|
||
// 退回:应回到第 1 个审批节点
|
||
await _engine.ReturnTaskAsync(task2);
|
||
|
||
// 第 1 个审批节点应有新的 Pending 任务(重新创建)
|
||
var returnedTask = await _dbContext.WorkflowTasks
|
||
.FirstOrDefaultAsync(t => t.NodeId == firstApproval.Id && t.Status == TaskStatus.Pending);
|
||
returnedTask.Should().NotBeNull("退回应在第 1 个审批节点重新创建任务");
|
||
|
||
// 第 2 个审批任务应不再是 Pending
|
||
await _dbContext.Entry(task2).ReloadAsync();
|
||
task2.Status.Should().NotBe(TaskStatus.Pending);
|
||
}
|
||
|
||
// ============================================================
|
||
// 端到端组合:会签 × 条件路由(消解"条件求值器没弄好"的疑虑)
|
||
// ============================================================
|
||
|
||
/// <summary>
|
||
/// 会签通过后,Condition 节点根据实例变量正确路由到不同分支。
|
||
/// 验证:会签引擎与条件求值器的端到端协作(用户担忧的"流程引擎+条件器没弄好")。
|
||
/// </summary>
|
||
[Fact]
|
||
public async Task CountersignApproved_ThenConditionRoutesBasedOnVariable()
|
||
{
|
||
var user1 = Guid.NewGuid();
|
||
var user2 = Guid.NewGuid();
|
||
|
||
var approvalNode = new WorkflowNode
|
||
{
|
||
Id = Guid.NewGuid(),
|
||
NodeType = NodeType.Approval,
|
||
Name = "会签审批",
|
||
CreatedBy = "test",
|
||
UpdatedBy = "test",
|
||
Config = $$"""{ "approvalMode": "countersign", "assigneeRules": ["user:{{user1}}", "user:{{user2}}"] }""",
|
||
};
|
||
var conditionNode = new WorkflowNode
|
||
{
|
||
Id = Guid.NewGuid(),
|
||
NodeType = NodeType.Condition,
|
||
Name = "金额判断",
|
||
CreatedBy = "test",
|
||
UpdatedBy = "test",
|
||
Config = "{}",
|
||
};
|
||
// 金额 > 1000 走高额分支
|
||
var highAmountEnd = CreateNode(NodeType.End);
|
||
highAmountEnd.Name = "高额终态";
|
||
var normalEnd = CreateNode(NodeType.End);
|
||
normalEnd.Name = "普通终态";
|
||
|
||
var startNode = CreateNode(NodeType.Start);
|
||
var edgeStart = CreateEdge(startNode, approvalNode);
|
||
var edgeToCondition = CreateEdge(approvalNode, conditionNode);
|
||
var edgeHigh = new WorkflowEdge
|
||
{
|
||
Id = Guid.NewGuid(),
|
||
DefinitionId = Guid.Empty, // PersistSetup 会回填
|
||
SourceNodeId = conditionNode.Id,
|
||
TargetNodeId = highAmountEnd.Id,
|
||
EdgeType = EdgeType.Normal,
|
||
Condition = "amount > 1000",
|
||
Order = 1,
|
||
CreatedBy = "test",
|
||
UpdatedBy = "test",
|
||
};
|
||
var edgeNormal = new WorkflowEdge
|
||
{
|
||
Id = Guid.NewGuid(),
|
||
DefinitionId = Guid.Empty,
|
||
SourceNodeId = conditionNode.Id,
|
||
TargetNodeId = normalEnd.Id,
|
||
EdgeType = EdgeType.Normal,
|
||
Condition = "amount <= 1000",
|
||
Order = 2,
|
||
CreatedBy = "test",
|
||
UpdatedBy = "test",
|
||
};
|
||
|
||
var (_, instance) = PersistSetup(
|
||
[startNode, approvalNode, conditionNode, highAmountEnd, normalEnd],
|
||
[edgeStart, edgeToCondition, edgeHigh, edgeNormal]);
|
||
|
||
// 设置实例变量(模拟表单数据回流)
|
||
instance.Variables = """{ "amount": 5000 }""";
|
||
await _dbContext.SaveChangesAsync();
|
||
|
||
await _engine.StartAsync(instance);
|
||
|
||
// 会签全部通过
|
||
var tasks = await _dbContext.WorkflowTasks
|
||
.Where(t => t.NodeId == approvalNode.Id)
|
||
.ToListAsync();
|
||
await _engine.CompleteTaskAsync(tasks[0], TaskResult.Approved);
|
||
await _engine.CompleteTaskAsync(tasks[1], TaskResult.Approved);
|
||
|
||
// 会签通过 → Condition 求值 → 应路由到高额分支(amount=5000 > 1000)
|
||
var highEndToken = await _dbContext.WorkflowTokens
|
||
.AnyAsync(t => t.NodeId == highAmountEnd.Id);
|
||
var normalEndToken = await _dbContext.WorkflowTokens
|
||
.AnyAsync(t => t.NodeId == normalEnd.Id);
|
||
|
||
highEndToken.Should().BeTrue("amount=5000 > 1000 应走高额分支");
|
||
normalEndToken.Should().BeFalse("不应走普通分支");
|
||
}
|
||
|
||
/// <summary>
|
||
/// 或签模式:第 1 个通过即推进,验证其余任务的 Comment 字段不影响判定。
|
||
/// 锁定或签的"竞争"语义——只看状态不看评论。
|
||
/// </summary>
|
||
[Fact]
|
||
public async Task AnyApproval_PartialRejectDoesNotAffectAlreadyPropagated()
|
||
{
|
||
var user1 = Guid.NewGuid();
|
||
var user2 = Guid.NewGuid();
|
||
|
||
var approvalNode = new WorkflowNode
|
||
{
|
||
Id = Guid.NewGuid(),
|
||
NodeType = NodeType.Approval,
|
||
Name = "或签",
|
||
CreatedBy = "test",
|
||
UpdatedBy = "test",
|
||
Config = $$"""{ "approvalMode": "any", "assigneeRules": ["user:{{user1}}", "user:{{user2}}"] }""",
|
||
};
|
||
var endNode = CreateNode(NodeType.End);
|
||
var startNode = CreateNode(NodeType.Start);
|
||
var edgeStart = CreateEdge(startNode, approvalNode);
|
||
var edge = CreateEdge(approvalNode, endNode);
|
||
|
||
var (_, instance) = PersistSetup([startNode, approvalNode, endNode], [edgeStart, edge]);
|
||
await _engine.StartAsync(instance);
|
||
|
||
var tasks = await _dbContext.WorkflowTasks
|
||
.Where(t => t.NodeId == approvalNode.Id && t.Status == TaskStatus.Pending)
|
||
.ToListAsync();
|
||
|
||
// 第 1 个通过 → 立即推进
|
||
await _engine.CompleteTaskAsync(tasks[0], TaskResult.Approved);
|
||
instance.Status.Should().Be(InstanceStatus.Completed);
|
||
|
||
// 第 2 个任务已被自动关闭(不再 Pending)
|
||
await _dbContext.Entry(tasks[1]).ReloadAsync();
|
||
tasks[1].Status.Should().NotBe(TaskStatus.Pending, "或签通过后其余任务应自动关闭");
|
||
}
|
||
|
||
// ============================================================
|
||
// 辅助方法
|
||
// ============================================================
|
||
|
||
private static WorkflowNode CreateNode(NodeType type) => new()
|
||
{
|
||
Id = Guid.NewGuid(),
|
||
NodeType = type,
|
||
Config = "{}",
|
||
Name = type.ToString(),
|
||
CreatedBy = "test",
|
||
UpdatedBy = "test",
|
||
};
|
||
|
||
private static WorkflowEdge CreateEdge(WorkflowNode source, WorkflowNode target) => new()
|
||
{
|
||
Id = Guid.NewGuid(),
|
||
SourceNodeId = source.Id,
|
||
TargetNodeId = target.Id,
|
||
EdgeType = EdgeType.Normal,
|
||
CreatedBy = "test",
|
||
UpdatedBy = "test",
|
||
};
|
||
|
||
/// <summary>在审批节点预创建 Active token(填审计字段,绕过 InMemory 不跑拦截器)。</summary>
|
||
private WorkflowToken SeedApprovalToken(WorkflowInstance instance, WorkflowNode node)
|
||
{
|
||
var token = new WorkflowToken
|
||
{
|
||
Id = Guid.NewGuid(),
|
||
InstanceId = instance.Id,
|
||
NodeId = node.Id,
|
||
Status = TokenStatus.Active,
|
||
CreatedBy = "test",
|
||
UpdatedBy = "test",
|
||
};
|
||
_dbContext.WorkflowTokens.Add(token);
|
||
_dbContext.SaveChanges();
|
||
return token;
|
||
}
|
||
|
||
private (WorkflowDefinition Definition, WorkflowInstance Instance) PersistSetup(
|
||
List<WorkflowNode> nodes, List<WorkflowEdge> edges)
|
||
{
|
||
var definition = new WorkflowDefinition
|
||
{
|
||
Id = Guid.NewGuid(),
|
||
Name = "test-flow",
|
||
Status = DefinitionStatus.Published,
|
||
CreatedBy = "test",
|
||
UpdatedBy = "test",
|
||
};
|
||
foreach (var n in nodes) { n.DefinitionId = definition.Id; _dbContext.WorkflowNodes.Add(n); }
|
||
foreach (var e in edges) { e.DefinitionId = definition.Id; _dbContext.WorkflowEdges.Add(e); }
|
||
_dbContext.WorkflowDefinitions.Add(definition);
|
||
var instance = new WorkflowInstance
|
||
{
|
||
Id = Guid.NewGuid(),
|
||
DefinitionId = definition.Id,
|
||
Title = "test-instance",
|
||
Status = InstanceStatus.Running, // 直接置 Running,绕过 StartAsync
|
||
Variables = "{}",
|
||
CreatedBy = "test",
|
||
UpdatedBy = "test",
|
||
};
|
||
_dbContext.WorkflowInstances.Add(instance);
|
||
_dbContext.SaveChanges();
|
||
return (definition, instance);
|
||
}
|
||
}
|