work-flow/tests/Workflow.Tests/Handlers/WorkflowInstanceHandlerTests.cs
向宁 9f878286e7 feat: form versioning, notification center, scheduler and webhooks
- Add FormDefinitionVersion with compare/versions endpoints and schema differ
- Add Notification entity, endpoints and application features
- Add Scheduler (timeout) and WebhookDispatcher services
- Add FormDataValidator/FieldPermissionEvaluator/ReactionEvaluator
- Add workflow task mark-read, CC support and SystemUserContext
- Add EF migrations for form versions and notifications
- Add unit tests for form schema, notifications, scheduler and serialization
2026-06-14 15:03:11 +08:00

844 lines
28 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using FluentAssertions;
using Microsoft.EntityFrameworkCore;
using Workflow.Application.Engine;
using Workflow.Application.Features.WorkflowInstances.Commands;
using Workflow.Application.Features.WorkflowInstances.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 WorkflowInstanceHandlerTests
{
private static WorkflowDbContext CreateDbContext()
{
var options = new DbContextOptionsBuilder<WorkflowDbContext>()
.UseInMemoryDatabase(Guid.NewGuid().ToString())
.Options;
return new WorkflowDbContext(options);
}
private static async Task<WorkflowDbContext> SeedPublishedDefinitionAsync(
string code = "leave-approval",
string name = "Leave Approval",
Guid? startNodeId = null)
{
var db = CreateDbContext();
var definitionId = Guid.NewGuid();
var nodeId = startNodeId ?? Guid.NewGuid();
var approvalNodeId = Guid.NewGuid();
var definition = new WorkflowDefinition
{
Id = definitionId,
Name = name,
Code = code,
Status = DefinitionStatus.Published,
Version = 2,
IsEnabled = true
};
db.WorkflowDefinitions.Add(definition);
var startNode = new WorkflowNode
{
Id = nodeId,
DefinitionId = definitionId,
NodeType = NodeType.Start,
Name = "Start",
PositionX = 100,
PositionY = 100
};
db.WorkflowNodes.Add(startNode);
var approvalNode = new WorkflowNode
{
Id = approvalNodeId,
DefinitionId = definitionId,
NodeType = NodeType.Approval,
Name = "Approve",
Config = """{"assigneeRule": "user:00000000-0000-0000-0000-000000000001"}""",
PositionX = 300,
PositionY = 100
};
db.WorkflowNodes.Add(approvalNode);
db.WorkflowEdges.Add(new WorkflowEdge
{
Id = Guid.NewGuid(),
DefinitionId = definitionId,
SourceNodeId = nodeId,
TargetNodeId = approvalNodeId,
EdgeType = EdgeType.Normal
});
await db.SaveChangesAsync();
return db;
}
#region StartWorkflowInstance
[Fact]
public async Task StartInstance_CreatesInstanceWithRunningStatus()
{
// Arrange
await using var db = await SeedPublishedDefinitionAsync("running-test");
var handler = new StartWorkflowInstanceCommandHandler(db, new ProcessEngine(db, null!, new()));
var command = new StartWorkflowInstanceCommand(
DefinitionCode: "running-test",
Title: "Test Instance",
Variables: null,
FormDataJson: null
);
// Act
var instanceId = await handler.Handle(command, CancellationToken.None);
// Assert
instanceId.Should().NotBe(Guid.Empty);
var instance = await db.WorkflowInstances.FindAsync(instanceId);
instance.Should().NotBeNull();
instance!.Status.Should().Be(InstanceStatus.Running);
instance.Title.Should().Be("Test Instance");
}
[Fact]
public async Task StartInstance_CreatesInitialTokenAtStartNode()
{
// Arrange
var startNodeId = Guid.NewGuid();
await using var db = await SeedPublishedDefinitionAsync("token-test", startNodeId: startNodeId);
var handler = new StartWorkflowInstanceCommandHandler(db, new ProcessEngine(db, null!, new()));
var command = new StartWorkflowInstanceCommand(
DefinitionCode: "token-test",
Title: "Token Test",
Variables: null,
FormDataJson: null
);
// Act
var instanceId = await handler.Handle(command, CancellationToken.None);
// Assert — Start→Approval: Start token consumed, Approval token active
var tokens = await db.WorkflowTokens
.Where(t => t.InstanceId == instanceId)
.ToListAsync();
tokens.Should().HaveCount(2);
tokens.Should().Contain(t => t.NodeId == startNodeId && t.Status == TokenStatus.Consumed);
tokens.Should().Contain(t => t.Status == TokenStatus.Active);
}
[Fact]
public async Task StartInstance_WithInvalidDefinitionCode_ThrowsNotFoundException()
{
// Arrange
await using var db = CreateDbContext();
var handler = new StartWorkflowInstanceCommandHandler(db, new ProcessEngine(db, null!, new()));
var command = new StartWorkflowInstanceCommand(
DefinitionCode: "nonexistent-code",
Title: "Should Fail",
Variables: null,
FormDataJson: null
);
// Act
var act = () => handler.Handle(command, CancellationToken.None);
// Assert
await act.Should().ThrowAsync<NotFoundException>()
.WithMessage("*nonexistent-code*");
}
[Fact]
public async Task StartInstance_WithDisabledDefinition_ThrowsBusinessException()
{
// Arrange
await using var db = CreateDbContext();
var definition = new WorkflowDefinition
{
Id = Guid.NewGuid(),
Name = "Disabled Workflow",
Code = "disabled-wf",
Status = DefinitionStatus.Published,
Version = 2,
IsEnabled = false
};
db.WorkflowDefinitions.Add(definition);
await db.SaveChangesAsync();
var handler = new StartWorkflowInstanceCommandHandler(db, new ProcessEngine(db, null!, new()));
var command = new StartWorkflowInstanceCommand(
DefinitionCode: "disabled-wf",
Title: "Should Fail",
Variables: null,
FormDataJson: null
);
// Act
var act = () => handler.Handle(command, CancellationToken.None);
// Assert
await act.Should().ThrowAsync<BusinessException>()
.WithMessage("*disabled*");
}
[Fact]
public async Task StartInstance_WithFormDataJson_MergesFormDataIntoVariables()
{
// Arrange
var formId = Guid.NewGuid();
await using var db = await SeedPublishedDefinitionAsync("form-vars-test");
var definition = await db.WorkflowDefinitions.FirstAsync(d => d.Code == "form-vars-test");
definition.FormDefinitionId = formId;
db.FormDefinitions.Add(new FormDefinition
{
Id = formId,
Name = "Expense Form",
Code = "expense-form",
Status = FormStatus.Published,
Version = 1,
SchemaJson = """{"type":"object","properties":{}}"""
});
await db.SaveChangesAsync();
var handler = new StartWorkflowInstanceCommandHandler(db, new ProcessEngine(db, null!, new()));
var command = new StartWorkflowInstanceCommand(
DefinitionCode: "form-vars-test",
Title: "Expense",
Variables: null,
FormDataJson: """{"amount":6500,"category":"travel"}"""
);
// Act
var instanceId = await handler.Handle(command, CancellationToken.None);
// Assert
var instance = await db.WorkflowInstances.FindAsync(instanceId);
instance.Should().NotBeNull();
instance!.Variables.Should().NotBeNull();
instance.Variables.Should().Contain("\"amount\":6500");
instance.Variables.Should().Contain("\"category\":\"travel\"");
var formData = await db.FormData.SingleAsync(f => f.InstanceId == instanceId);
formData.FormDefinitionId.Should().Be(formId);
formData.DataJson.Should().Be("""{"amount":6500,"category":"travel"}""");
}
/// <summary>
/// 边界:流程定义绑定的启动表单已被软删除时启动实例,必须给出准确错误
/// (表单已被删除),而非误导性的「表单定义 ... 不存在」。
/// 且不得创建实例、不得保存运行时记录Token 等)。
/// </summary>
[Fact]
public async Task StartInstance_WithSoftDeletedStartForm_ThrowsAccurateErrorAndPersistsNothing()
{
// Arrange
var formId = Guid.NewGuid();
await using var db = await SeedPublishedDefinitionAsync("deleted-start-form-test");
var definition = await db.WorkflowDefinitions.FirstAsync(d => d.Code == "deleted-start-form-test");
definition.FormDefinitionId = formId;
db.FormDefinitions.Add(new FormDefinition
{
Id = formId,
Name = "Deleted Start Form",
Code = "deleted-start-form",
Status = FormStatus.Published,
Version = 1,
SchemaJson = """{"type":"object","properties":{}}""",
IsDeleted = true,
});
await db.SaveChangesAsync();
var handler = new StartWorkflowInstanceCommandHandler(db, new ProcessEngine(db, null!, new()));
var command = new StartWorkflowInstanceCommand(
DefinitionCode: "deleted-start-form-test",
Title: "X",
Variables: null,
FormDataJson: """{"amount":100}"""
);
// Act
var act = () => handler.Handle(command, CancellationToken.None);
// Assert准确错误而非误导性的「不存在」
await act.Should().ThrowAsync<BusinessException>()
.WithMessage("*表单*删除*");
// 不留任何运行时记录
(await db.WorkflowInstances.CountAsync()).Should().Be(0);
(await db.FormData.CountAsync()).Should().Be(0);
(await db.WorkflowTokens.CountAsync()).Should().Be(0);
}
/// <summary>
/// 边界:流程定义绑定的启动表单确实不存在(既未存在也未删除),必须给出准确错误(不存在)。
/// 与软删除场景区分。
/// </summary>
[Fact]
public async Task StartInstance_WithTrulyNonExistentStartForm_ThrowsAccurateError()
{
var formId = Guid.NewGuid();
await using var db = await SeedPublishedDefinitionAsync("nonexistent-start-form-test");
var definition = await db.WorkflowDefinitions.FirstAsync(d => d.Code == "nonexistent-start-form-test");
definition.FormDefinitionId = formId;
await db.SaveChangesAsync();
var handler = new StartWorkflowInstanceCommandHandler(db, new ProcessEngine(db, null!, new()));
var command = new StartWorkflowInstanceCommand(
DefinitionCode: "nonexistent-start-form-test",
Title: "X",
Variables: null,
FormDataJson: """{"amount":100}"""
);
var act = () => handler.Handle(command, CancellationToken.None);
await act.Should().ThrowAsync<BusinessException>()
.WithMessage("*不存在*");
}
/// <summary>
/// 边界:流程定义绑定的启动表单状态为 Disabled 时启动实例,必须严格阻断。
/// 产品决策Disabled = 严格阻断(新启动一律拒绝)。且不得创建任何运行时记录。
/// </summary>
[Fact]
public async Task StartInstance_WithDisabledStartForm_ThrowsBusinessExceptionAndPersistsNothing()
{
var formId = Guid.NewGuid();
await using var db = await SeedPublishedDefinitionAsync("disabled-start-form-test");
var definition = await db.WorkflowDefinitions.FirstAsync(d => d.Code == "disabled-start-form-test");
definition.FormDefinitionId = formId;
db.FormDefinitions.Add(new FormDefinition
{
Id = formId,
Name = "Disabled Start Form",
Code = "disabled-start-form",
Status = FormStatus.Disabled,
Version = 1,
SchemaJson = """{"type":"object","properties":{}}""",
});
await db.SaveChangesAsync();
var handler = new StartWorkflowInstanceCommandHandler(db, new ProcessEngine(db, null!, new()));
var command = new StartWorkflowInstanceCommand(
DefinitionCode: "disabled-start-form-test",
Title: "X",
Variables: null,
FormDataJson: """{"amount":100}"""
);
var act = () => handler.Handle(command, CancellationToken.None);
await act.Should().ThrowAsync<BusinessException>()
.WithMessage("*表单*停用*");
(await db.WorkflowInstances.CountAsync()).Should().Be(0);
(await db.FormData.CountAsync()).Should().Be(0);
(await db.WorkflowTokens.CountAsync()).Should().Be(0);
}
[Fact]
public async Task StartInstance_WithFormDataAndVariables_MergesBothWithExplicitVariablesTakingPrecedence()
{
// Arrange
var formId = Guid.NewGuid();
await using var db = await SeedPublishedDefinitionAsync("merge-vars-test");
var definition = await db.WorkflowDefinitions.FirstAsync(d => d.Code == "merge-vars-test");
definition.FormDefinitionId = formId;
db.FormDefinitions.Add(new FormDefinition
{
Id = formId,
Name = "Leave Form",
Code = "leave-form",
Status = FormStatus.Published,
Version = 1,
SchemaJson = """{"type":"object","properties":{}}"""
});
await db.SaveChangesAsync();
var handler = new StartWorkflowInstanceCommandHandler(db, new ProcessEngine(db, null!, new()));
var command = new StartWorkflowInstanceCommand(
DefinitionCode: "merge-vars-test",
Title: "Leave",
Variables: """{"days":5,"urgent":true}""",
FormDataJson: """{"days":2,"leaveType":"annual"}"""
);
// Act
var instanceId = await handler.Handle(command, CancellationToken.None);
// Assert
var instance = await db.WorkflowInstances.FindAsync(instanceId);
instance.Should().NotBeNull();
instance!.Variables.Should().NotBeNull();
instance.Variables.Should().Contain("\"days\":5");
instance.Variables.Should().Contain("\"urgent\":true");
instance.Variables.Should().Contain("\"leaveType\":\"annual\"");
}
[Fact]
public async Task StartWorkflowInstance_WithInvalidFormData_ThrowsBusinessExceptionAndDoesNotPersistRuntimeRecords()
{
// Arrange
var formId = Guid.NewGuid();
await using var db = await SeedPublishedDefinitionAsync("invalid-start-form-test");
var definition = await db.WorkflowDefinitions.FirstAsync(d => d.Code == "invalid-start-form-test");
definition.FormDefinitionId = formId;
db.FormDefinitions.Add(new FormDefinition
{
Id = formId,
Name = "启动表单",
Code = "start-form",
Status = FormStatus.Published,
Version = 1,
SchemaJson = """
{
"type": "object",
"properties": {
"name": {
"type": "string",
"title": "姓名",
"required": true,
"x-component": "Input"
}
}
}
"""
});
await db.SaveChangesAsync();
var handler = new StartWorkflowInstanceCommandHandler(db, new ProcessEngine(db, null!, new()));
var command = new StartWorkflowInstanceCommand(
DefinitionCode: "invalid-start-form-test",
Title: "Invalid Start Form",
Variables: null,
FormDataJson: """{"name":""}"""
);
// Act
var act = () => handler.Handle(command, CancellationToken.None);
// Assert
await act.Should().ThrowAsync<BusinessException>()
.WithMessage("*表单数据校验失败*姓名*");
db.FormData.Should().BeEmpty();
db.WorkflowInstances.Should().BeEmpty();
db.WorkflowTokens.Should().BeEmpty();
}
[Fact]
public async Task StartWorkflowInstance_WithNestedFormData_AcceptsValidAndRejectsMissingNestedRequired()
{
// 嵌套表单FormGrid 容器下的 startDate/endDate 字段 Path 形如 "dateRange.startDate"
// 提交数据是嵌套对象 {dateRange:{startDate:...}}。FormDataValidator 必须按点分路径取值,
// 否则带容器的表单无法通过发起流程的校验(回归 bug扁平 TryGetValue 找不到嵌套值)。
var formId = Guid.NewGuid();
await using var db = await SeedPublishedDefinitionAsync("nested-start-form-test");
var definition = await db.WorkflowDefinitions.FirstAsync(d => d.Code == "nested-start-form-test");
definition.FormDefinitionId = formId;
db.FormDefinitions.Add(new FormDefinition
{
Id = formId,
Name = "嵌套启动表单",
Code = "nested-start-form",
Status = FormStatus.Published,
Version = 1,
SchemaJson = """
{
"type": "object",
"properties": {
"leaveType": {
"type": "string",
"title": "请假类型",
"required": true,
"x-component": "Select"
},
"dateRange": {
"type": "void",
"title": "日期范围",
"x-component": "FormGrid",
"properties": {
"startDate": {
"type": "string",
"title": "开始日期",
"required": true,
"x-component": "DatePicker"
},
"endDate": {
"type": "string",
"title": "结束日期",
"required": true,
"x-component": "DatePicker"
}
}
}
}
}
"""
});
await db.SaveChangesAsync();
var handler = new StartWorkflowInstanceCommandHandler(db, new ProcessEngine(db, null!, new()));
// 合法嵌套数据:必须成功发起(这是回归 bug 修复的核心断言)
var instanceId = await handler.Handle(
new StartWorkflowInstanceCommand(
"nested-start-form-test",
"嵌套合法发起",
null,
"""{"leaveType":"annual","dateRange":{"startDate":"2026-06-15","endDate":"2026-06-16"}}"""),
CancellationToken.None);
instanceId.Should().NotBeEmpty();
db.FormData.Should().ContainSingle(f => f.InstanceId == instanceId);
// 嵌套必填缺失:必须被拒
var act = () => handler.Handle(
new StartWorkflowInstanceCommand(
"nested-start-form-test",
"嵌套缺失",
null,
"""{"leaveType":"annual","dateRange":{}}"""),
CancellationToken.None);
await act.Should().ThrowAsync<BusinessException>()
.WithMessage("*开始日期*");
}
#endregion
#region WithdrawWorkflowInstance
[Fact]
public async Task WithdrawInstance_TerminatesInstance_WhenInitiatorAndNoTasksProcessed()
{
// Arrange
await using var db = CreateDbContext();
var instanceId = Guid.NewGuid();
var initiatorId = Guid.NewGuid();
var instance = new WorkflowInstance
{
Id = instanceId,
DefinitionId = Guid.NewGuid(),
Title = "Test",
Status = InstanceStatus.Running,
InitiatorId = initiatorId
};
db.WorkflowInstances.Add(instance);
var token1 = new WorkflowToken
{
Id = Guid.NewGuid(),
InstanceId = instanceId,
NodeId = Guid.NewGuid(),
Status = TokenStatus.Active
};
var token2 = new WorkflowToken
{
Id = Guid.NewGuid(),
InstanceId = instanceId,
NodeId = Guid.NewGuid(),
Status = TokenStatus.Active
};
db.WorkflowTokens.AddRange(token1, token2);
// No tasks have been processed (task exists but is pending, not completed)
var pendingTask = new WorkflowTask
{
Id = Guid.NewGuid(),
InstanceId = instanceId,
TokenId = token1.Id,
AssigneeId = Guid.NewGuid(),
Status = TaskStatus.Pending
};
db.WorkflowTasks.Add(pendingTask);
await db.SaveChangesAsync();
var handler = new WithdrawWorkflowInstanceCommandHandler(db);
var command = new WithdrawWorkflowInstanceCommand(
InstanceId: instanceId,
UserId: initiatorId
);
// Act
await handler.Handle(command, CancellationToken.None);
// Assert
var updatedInstance = await db.WorkflowInstances.FindAsync(instanceId);
updatedInstance.Should().NotBeNull();
updatedInstance!.Status.Should().Be(InstanceStatus.Terminated);
var tokens = await db.WorkflowTokens
.Where(t => t.InstanceId == instanceId)
.ToListAsync();
tokens.Should().OnlyContain(t => t.Status == TokenStatus.Terminated);
}
[Fact]
public async Task WithdrawInstance_Throws_WhenNotInitiator()
{
// Arrange
await using var db = CreateDbContext();
var instanceId = Guid.NewGuid();
var initiatorId = Guid.NewGuid();
var otherUserId = Guid.NewGuid();
var instance = new WorkflowInstance
{
Id = instanceId,
DefinitionId = Guid.NewGuid(),
Title = "Test",
Status = InstanceStatus.Running,
InitiatorId = initiatorId
};
db.WorkflowInstances.Add(instance);
await db.SaveChangesAsync();
var handler = new WithdrawWorkflowInstanceCommandHandler(db);
var command = new WithdrawWorkflowInstanceCommand(
InstanceId: instanceId,
UserId: otherUserId
);
// Act
var act = () => handler.Handle(command, CancellationToken.None);
// Assert
await act.Should().ThrowAsync<InvalidStateTransitionException>()
.WithMessage("*initiator*");
}
[Fact]
public async Task WithdrawInstance_Throws_WhenTaskAlreadyProcessed()
{
// Arrange
await using var db = CreateDbContext();
var instanceId = Guid.NewGuid();
var initiatorId = Guid.NewGuid();
var tokenId = Guid.NewGuid();
var instance = new WorkflowInstance
{
Id = instanceId,
DefinitionId = Guid.NewGuid(),
Title = "Test",
Status = InstanceStatus.Running,
InitiatorId = initiatorId
};
db.WorkflowInstances.Add(instance);
var token = new WorkflowToken
{
Id = tokenId,
InstanceId = instanceId,
NodeId = Guid.NewGuid(),
Status = TokenStatus.Active
};
db.WorkflowTokens.Add(token);
// A task that has already been completed (processed)
var completedTask = new WorkflowTask
{
Id = Guid.NewGuid(),
InstanceId = instanceId,
TokenId = tokenId,
AssigneeId = Guid.NewGuid(),
Status = TaskStatus.Approved
};
db.WorkflowTasks.Add(completedTask);
await db.SaveChangesAsync();
var handler = new WithdrawWorkflowInstanceCommandHandler(db);
var command = new WithdrawWorkflowInstanceCommand(
InstanceId: instanceId,
UserId: initiatorId
);
// Act
var act = () => handler.Handle(command, CancellationToken.None);
// Assert
await act.Should().ThrowAsync<InvalidStateTransitionException>()
.WithMessage("*processed*");
}
#endregion
#region SuspendWorkflowInstance
[Fact]
public async Task SuspendInstance_ChangesToSuspended()
{
// Arrange
await using var db = CreateDbContext();
var instanceId = Guid.NewGuid();
var instance = new WorkflowInstance
{
Id = instanceId,
DefinitionId = Guid.NewGuid(),
Title = "To Suspend",
Status = InstanceStatus.Running,
InitiatorId = Guid.NewGuid()
};
db.WorkflowInstances.Add(instance);
await db.SaveChangesAsync();
var handler = new SuspendWorkflowInstanceCommandHandler(db);
var command = new SuspendWorkflowInstanceCommand(InstanceId: instanceId);
// Act
await handler.Handle(command, CancellationToken.None);
// Assert
var updated = await db.WorkflowInstances.FindAsync(instanceId);
updated.Should().NotBeNull();
updated!.Status.Should().Be(InstanceStatus.Suspended);
}
#endregion
#region ResumeWorkflowInstance
[Fact]
public async Task ResumeInstance_ChangesBackToRunning()
{
// Arrange
await using var db = CreateDbContext();
var instanceId = Guid.NewGuid();
var instance = new WorkflowInstance
{
Id = instanceId,
DefinitionId = Guid.NewGuid(),
Title = "To Resume",
Status = InstanceStatus.Suspended,
InitiatorId = Guid.NewGuid()
};
db.WorkflowInstances.Add(instance);
await db.SaveChangesAsync();
var handler = new ResumeWorkflowInstanceCommandHandler(db);
var command = new ResumeWorkflowInstanceCommand(InstanceId: instanceId);
// Act
await handler.Handle(command, CancellationToken.None);
// Assert
var updated = await db.WorkflowInstances.FindAsync(instanceId);
updated.Should().NotBeNull();
updated!.Status.Should().Be(InstanceStatus.Running);
}
#endregion
#region GetWorkflowInstanceList
[Fact]
public async Task GetInstanceList_ReturnsPaged()
{
// Arrange
await using var db = CreateDbContext();
var definitionId = Guid.NewGuid();
for (int i = 1; i <= 12; i++)
{
db.WorkflowInstances.Add(new WorkflowInstance
{
Id = Guid.NewGuid(),
DefinitionId = definitionId,
Title = $"Instance {i}",
Status = InstanceStatus.Running,
InitiatorId = Guid.NewGuid()
});
}
await db.SaveChangesAsync();
var handler = new GetWorkflowInstanceListQueryHandler(db);
var query = new GetWorkflowInstanceListQuery(
PageIndex: 1,
PageSize: 5,
Status: null,
InitiatorId: null
);
// Act
var result = await handler.Handle(query, CancellationToken.None);
// Assert
result.Should().NotBeNull();
result.Items.Should().HaveCount(5);
result.Total.Should().Be(12);
result.PageIndex.Should().Be(1);
result.PageSize.Should().Be(5);
}
#endregion
#region GetWorkflowInstanceById
[Fact]
public async Task GetInstanceById_IncludesTokensAndTasks()
{
// Arrange
await using var db = CreateDbContext();
var instanceId = Guid.NewGuid();
var tokenId = Guid.NewGuid();
var nodeId = Guid.NewGuid();
var instance = new WorkflowInstance
{
Id = instanceId,
DefinitionId = Guid.NewGuid(),
Title = "Detail Test",
Status = InstanceStatus.Running,
InitiatorId = Guid.NewGuid()
};
db.WorkflowInstances.Add(instance);
var token = new WorkflowToken
{
Id = tokenId,
InstanceId = instanceId,
NodeId = nodeId,
Status = TokenStatus.Active
};
db.WorkflowTokens.Add(token);
var task = new WorkflowTask
{
Id = Guid.NewGuid(),
InstanceId = instanceId,
TokenId = tokenId,
AssigneeId = Guid.NewGuid(),
Status = TaskStatus.Pending
};
db.WorkflowTasks.Add(task);
await db.SaveChangesAsync();
var handler = new GetWorkflowInstanceByIdQueryHandler(db);
var query = new GetWorkflowInstanceByIdQuery(Id: instanceId);
// Act
var result = await handler.Handle(query, CancellationToken.None);
// Assert
result.Should().NotBeNull();
result.Id.Should().Be(instanceId);
result.Tokens.Should().NotBeNull().And.HaveCount(1);
result.Tasks.Should().NotBeNull().And.HaveCount(1);
result.Tokens[0].NodeId.Should().Be(nodeId);
result.Tasks[0].Status.Should().Be(TaskStatus.Pending);
}
#endregion
}