using FluentAssertions; using Microsoft.EntityFrameworkCore; 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; namespace Workflow.Tests.Handlers; public class WorkflowInstanceHandlerTests { private static WorkflowDbContext CreateDbContext() { var options = new DbContextOptionsBuilder() .UseInMemoryDatabase(Guid.NewGuid().ToString()) .Options; return new WorkflowDbContext(options); } private static async Task 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 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); 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); var command = new StartWorkflowInstanceCommand( DefinitionCode: "running-test", Title: "Test Instance", Variables: 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); var command = new StartWorkflowInstanceCommand( DefinitionCode: "token-test", Title: "Token Test", Variables: null ); // Act var instanceId = await handler.Handle(command, CancellationToken.None); // Assert var tokens = await db.WorkflowTokens .Where(t => t.InstanceId == instanceId) .ToListAsync(); tokens.Should().HaveCount(1); tokens[0].NodeId.Should().Be(startNodeId); tokens[0].Status.Should().Be(TokenStatus.Active); } [Fact] public async Task StartInstance_WithInvalidDefinitionCode_ThrowsNotFoundException() { // Arrange await using var db = CreateDbContext(); var handler = new StartWorkflowInstanceCommandHandler(db); var command = new StartWorkflowInstanceCommand( DefinitionCode: "nonexistent-code", Title: "Should Fail", Variables: null ); // Act var act = () => handler.Handle(command, CancellationToken.None); // Assert await act.Should().ThrowAsync() .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); var command = new StartWorkflowInstanceCommand( DefinitionCode: "disabled-wf", Title: "Should Fail", Variables: null ); // Act var act = () => handler.Handle(command, CancellationToken.None); // Assert await act.Should().ThrowAsync() .WithMessage("*disabled*"); } #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() .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() .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 }