using FluentAssertions; using Microsoft.EntityFrameworkCore; using Workflow.Application.Features.WorkflowDefinitions.Commands; using Workflow.Domain.Entities; using Workflow.Domain.Enums; using Workflow.Domain.Exceptions; using Workflow.Infrastructure.Persistence; using Xunit; namespace Workflow.Tests.Handlers; public class EdgeCommandHandlerTests { private static WorkflowDbContext CreateDbContext() { var options = new DbContextOptionsBuilder() .UseInMemoryDatabase(Guid.NewGuid().ToString()) .Options; return new WorkflowDbContext(options); } private static async Task<(WorkflowDefinition Definition, WorkflowNode SourceNode, WorkflowNode TargetNode)> SeedDefinitionWithNodes(WorkflowDbContext db) { var definition = new WorkflowDefinition { Id = Guid.NewGuid(), Name = "Test Workflow", Code = $"test-wf-{Guid.NewGuid():N}", Status = DefinitionStatus.Draft, Version = 1 }; db.WorkflowDefinitions.Add(definition); var sourceNode = new WorkflowNode { Id = Guid.NewGuid(), DefinitionId = definition.Id, NodeType = NodeType.Start, Name = "Start", PositionX = 0, PositionY = 0 }; var targetNode = new WorkflowNode { Id = Guid.NewGuid(), DefinitionId = definition.Id, NodeType = NodeType.End, Name = "End", PositionX = 300, PositionY = 0 }; db.WorkflowNodes.AddRange(sourceNode, targetNode); await db.SaveChangesAsync(); return (definition, sourceNode, targetNode); } #region CreateEdge [Fact] public async Task CreateEdge_ReturnsEdgeDto() { // Arrange await using var db = CreateDbContext(); var (definition, sourceNode, targetNode) = await SeedDefinitionWithNodes(db); var handler = new CreateEdgeCommandHandler(db); var command = new CreateEdgeCommand( DefinitionId: definition.Id, SourceNodeId: sourceNode.Id, TargetNodeId: targetNode.Id, EdgeType: EdgeType.Normal, Label: "Normal Flow", Condition: null, Order: 1 ); // Act var result = await handler.Handle(command, CancellationToken.None); // Assert result.Should().NotBeNull(); result.SourceNodeId.Should().Be(sourceNode.Id); result.TargetNodeId.Should().Be(targetNode.Id); result.EdgeType.Should().Be(EdgeType.Normal); result.Label.Should().Be("Normal Flow"); result.Condition.Should().BeNull(); result.Order.Should().Be(1); result.Id.Should().NotBeEmpty(); var entity = await db.WorkflowEdges.FindAsync(result.Id); entity.Should().NotBeNull(); entity!.DefinitionId.Should().Be(definition.Id); } [Fact] public async Task CreateEdge_WithMissingSource_ThrowsNotFoundException() { // Arrange await using var db = CreateDbContext(); var (definition, _, targetNode) = await SeedDefinitionWithNodes(db); var handler = new CreateEdgeCommandHandler(db); var command = new CreateEdgeCommand( DefinitionId: definition.Id, SourceNodeId: Guid.NewGuid(), // Non-existent source TargetNodeId: targetNode.Id, EdgeType: EdgeType.Normal, Label: null, Condition: null, Order: 0 ); // Act var act = () => handler.Handle(command, CancellationToken.None); // Assert await act.Should().ThrowAsync(); } [Fact] public async Task CreateEdge_WithNodeOutsideDefinition_ThrowsBusinessException() { // Arrange await using var db = CreateDbContext(); var (definition, sourceNode, _) = await SeedDefinitionWithNodes(db); var otherDefinition = new WorkflowDefinition { Id = Guid.NewGuid(), Name = "Other Workflow", Code = $"other-wf-{Guid.NewGuid():N}", Status = DefinitionStatus.Draft, Version = 1 }; var targetNodeInOtherDefinition = new WorkflowNode { Id = Guid.NewGuid(), DefinitionId = otherDefinition.Id, NodeType = NodeType.End, Name = "Other End", PositionX = 100, PositionY = 100 }; db.WorkflowDefinitions.Add(otherDefinition); db.WorkflowNodes.Add(targetNodeInOtherDefinition); await db.SaveChangesAsync(); var handler = new CreateEdgeCommandHandler(db); var command = new CreateEdgeCommand( DefinitionId: definition.Id, SourceNodeId: sourceNode.Id, TargetNodeId: targetNodeInOtherDefinition.Id, EdgeType: EdgeType.Normal, Label: null, Condition: null, Order: 0 ); // Act var act = () => handler.Handle(command, CancellationToken.None); // Assert await act.Should().ThrowAsync() .WithMessage("*same workflow definition*"); } #endregion #region UpdateEdge [Fact] public async Task UpdateEdge_UpdatesLabelAndCondition() { // Arrange await using var db = CreateDbContext(); var (definition, sourceNode, targetNode) = await SeedDefinitionWithNodes(db); var edgeId = Guid.NewGuid(); db.WorkflowEdges.Add(new WorkflowEdge { Id = edgeId, DefinitionId = definition.Id, SourceNodeId = sourceNode.Id, TargetNodeId = targetNode.Id, EdgeType = EdgeType.Normal, Label = "Old Label", Condition = null, Order = 0 }); await db.SaveChangesAsync(); var handler = new UpdateEdgeCommandHandler(db); var command = new UpdateEdgeCommand( EdgeId: edgeId, EdgeType: EdgeType.Approved, Label: "Approved Path", Condition: "amount > 5000", Order: 2 ); // Act var result = await handler.Handle(command, CancellationToken.None); // Assert result.Should().NotBeNull(); result.Id.Should().Be(edgeId); result.Label.Should().Be("Approved Path"); result.Condition.Should().Be("amount > 5000"); result.EdgeType.Should().Be(EdgeType.Approved); result.Order.Should().Be(2); result.SourceNodeId.Should().Be(sourceNode.Id); result.TargetNodeId.Should().Be(targetNode.Id); var entity = await db.WorkflowEdges.FindAsync(edgeId); entity.Should().NotBeNull(); entity!.Label.Should().Be("Approved Path"); entity.Condition.Should().Be("amount > 5000"); } [Fact] public async Task UpdateEdge_WithMissingEdge_ThrowsNotFoundException() { // Arrange await using var db = CreateDbContext(); var handler = new UpdateEdgeCommandHandler(db); var command = new UpdateEdgeCommand( EdgeId: Guid.NewGuid(), EdgeType: EdgeType.Normal, Label: "Ghost", Condition: null, Order: 0 ); // Act var act = () => handler.Handle(command, CancellationToken.None); // Assert await act.Should().ThrowAsync(); } #endregion #region DeleteEdge [Fact] public async Task DeleteEdge_RemovesEdge() { // Arrange await using var db = CreateDbContext(); var (definition, sourceNode, targetNode) = await SeedDefinitionWithNodes(db); var edgeId = Guid.NewGuid(); db.WorkflowEdges.Add(new WorkflowEdge { Id = edgeId, DefinitionId = definition.Id, SourceNodeId = sourceNode.Id, TargetNodeId = targetNode.Id, EdgeType = EdgeType.Normal, Label = "To Delete", Condition = null, Order = 0 }); await db.SaveChangesAsync(); var handler = new DeleteEdgeCommandHandler(db); var command = new DeleteEdgeCommand(EdgeId: edgeId); // Act await handler.Handle(command, CancellationToken.None); // Assert var edges = await db.WorkflowEdges.ToListAsync(); edges.Should().BeEmpty(); } #endregion }