From 1d5fd7449e35c1ea2ca347198165b445a17a8ed1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=90=91=E5=AE=81?= <1772105645@qq.com> Date: Thu, 21 May 2026 14:46:32 +0800 Subject: [PATCH] feat: add Edge CRUD commands with tests --- .../Commands/CreateEdgeCommand.cs | 56 +++++ .../Commands/DeleteEdgeCommand.cs | 22 ++ .../Commands/UpdateEdgeCommand.cs | 42 ++++ .../Handlers/EdgeCommandHandlerTests.cs | 232 ++++++++++++++++++ 4 files changed, 352 insertions(+) create mode 100644 src/Workflow.Application/Features/WorkflowDefinitions/Commands/CreateEdgeCommand.cs create mode 100644 src/Workflow.Application/Features/WorkflowDefinitions/Commands/DeleteEdgeCommand.cs create mode 100644 src/Workflow.Application/Features/WorkflowDefinitions/Commands/UpdateEdgeCommand.cs create mode 100644 tests/Workflow.Tests/Handlers/EdgeCommandHandlerTests.cs diff --git a/src/Workflow.Application/Features/WorkflowDefinitions/Commands/CreateEdgeCommand.cs b/src/Workflow.Application/Features/WorkflowDefinitions/Commands/CreateEdgeCommand.cs new file mode 100644 index 0000000..ec7c788 --- /dev/null +++ b/src/Workflow.Application/Features/WorkflowDefinitions/Commands/CreateEdgeCommand.cs @@ -0,0 +1,56 @@ +using MediatR; +using Workflow.Application.Features.WorkflowDefinitions.DTOs; +using Workflow.Domain.Entities; +using Workflow.Domain.Enums; +using Workflow.Domain.Exceptions; +using Workflow.Infrastructure.Persistence; + +namespace Workflow.Application.Features.WorkflowDefinitions.Commands; + +public record CreateEdgeCommand( + Guid DefinitionId, + Guid SourceNodeId, + Guid TargetNodeId, + EdgeType EdgeType, + string? Label, + string? Condition, + int Order +) : IRequest; + +public class CreateEdgeCommandHandler(WorkflowDbContext db) + : IRequestHandler +{ + public async Task Handle(CreateEdgeCommand request, CancellationToken cancellationToken) + { + var sourceNode = await db.WorkflowNodes.FindAsync([request.SourceNodeId], cancellationToken) + ?? throw new NotFoundException($"Source node '{request.SourceNodeId}' not found."); + + var targetNode = await db.WorkflowNodes.FindAsync([request.TargetNodeId], cancellationToken) + ?? throw new NotFoundException($"Target node '{request.TargetNodeId}' not found."); + + var entity = new WorkflowEdge + { + Id = Guid.NewGuid(), + DefinitionId = request.DefinitionId, + SourceNodeId = request.SourceNodeId, + TargetNodeId = request.TargetNodeId, + EdgeType = request.EdgeType, + Label = request.Label, + Condition = request.Condition, + Order = request.Order + }; + + db.WorkflowEdges.Add(entity); + await db.SaveChangesAsync(cancellationToken); + + return new WorkflowEdgeDto( + entity.Id, + entity.SourceNodeId, + entity.TargetNodeId, + entity.EdgeType, + entity.Label, + entity.Condition, + entity.Order + ); + } +} diff --git a/src/Workflow.Application/Features/WorkflowDefinitions/Commands/DeleteEdgeCommand.cs b/src/Workflow.Application/Features/WorkflowDefinitions/Commands/DeleteEdgeCommand.cs new file mode 100644 index 0000000..edd544c --- /dev/null +++ b/src/Workflow.Application/Features/WorkflowDefinitions/Commands/DeleteEdgeCommand.cs @@ -0,0 +1,22 @@ +using MediatR; +using Workflow.Domain.Exceptions; +using Workflow.Infrastructure.Persistence; + +namespace Workflow.Application.Features.WorkflowDefinitions.Commands; + +public record DeleteEdgeCommand(Guid EdgeId) : IRequest; + +public class DeleteEdgeCommandHandler(WorkflowDbContext db) + : IRequestHandler +{ + public async Task Handle(DeleteEdgeCommand request, CancellationToken cancellationToken) + { + var entity = await db.WorkflowEdges.FindAsync([request.EdgeId], cancellationToken) + ?? throw new NotFoundException($"Workflow edge '{request.EdgeId}' not found."); + + db.WorkflowEdges.Remove(entity); + await db.SaveChangesAsync(cancellationToken); + + return Unit.Value; + } +} diff --git a/src/Workflow.Application/Features/WorkflowDefinitions/Commands/UpdateEdgeCommand.cs b/src/Workflow.Application/Features/WorkflowDefinitions/Commands/UpdateEdgeCommand.cs new file mode 100644 index 0000000..3322076 --- /dev/null +++ b/src/Workflow.Application/Features/WorkflowDefinitions/Commands/UpdateEdgeCommand.cs @@ -0,0 +1,42 @@ +using MediatR; +using Workflow.Application.Features.WorkflowDefinitions.DTOs; +using Workflow.Domain.Enums; +using Workflow.Domain.Exceptions; +using Workflow.Infrastructure.Persistence; + +namespace Workflow.Application.Features.WorkflowDefinitions.Commands; + +public record UpdateEdgeCommand( + Guid EdgeId, + EdgeType EdgeType, + string? Label, + string? Condition, + int Order +) : IRequest; + +public class UpdateEdgeCommandHandler(WorkflowDbContext db) + : IRequestHandler +{ + public async Task Handle(UpdateEdgeCommand request, CancellationToken cancellationToken) + { + var entity = await db.WorkflowEdges.FindAsync([request.EdgeId], cancellationToken) + ?? throw new NotFoundException($"Workflow edge '{request.EdgeId}' not found."); + + entity.EdgeType = request.EdgeType; + entity.Label = request.Label; + entity.Condition = request.Condition; + entity.Order = request.Order; + + await db.SaveChangesAsync(cancellationToken); + + return new WorkflowEdgeDto( + entity.Id, + entity.SourceNodeId, + entity.TargetNodeId, + entity.EdgeType, + entity.Label, + entity.Condition, + entity.Order + ); + } +} diff --git a/tests/Workflow.Tests/Handlers/EdgeCommandHandlerTests.cs b/tests/Workflow.Tests/Handlers/EdgeCommandHandlerTests.cs new file mode 100644 index 0000000..ec5c203 --- /dev/null +++ b/tests/Workflow.Tests/Handlers/EdgeCommandHandlerTests.cs @@ -0,0 +1,232 @@ +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(); + } + + #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 +}