feat: add Edge CRUD commands with tests
This commit is contained in:
parent
f18a48379f
commit
1d5fd7449e
@ -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<WorkflowEdgeDto>;
|
||||||
|
|
||||||
|
public class CreateEdgeCommandHandler(WorkflowDbContext db)
|
||||||
|
: IRequestHandler<CreateEdgeCommand, WorkflowEdgeDto>
|
||||||
|
{
|
||||||
|
public async Task<WorkflowEdgeDto> 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
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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<Unit>;
|
||||||
|
|
||||||
|
public class DeleteEdgeCommandHandler(WorkflowDbContext db)
|
||||||
|
: IRequestHandler<DeleteEdgeCommand, Unit>
|
||||||
|
{
|
||||||
|
public async Task<Unit> 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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<WorkflowEdgeDto>;
|
||||||
|
|
||||||
|
public class UpdateEdgeCommandHandler(WorkflowDbContext db)
|
||||||
|
: IRequestHandler<UpdateEdgeCommand, WorkflowEdgeDto>
|
||||||
|
{
|
||||||
|
public async Task<WorkflowEdgeDto> 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
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
232
tests/Workflow.Tests/Handlers/EdgeCommandHandlerTests.cs
Normal file
232
tests/Workflow.Tests/Handlers/EdgeCommandHandlerTests.cs
Normal file
@ -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<WorkflowDbContext>()
|
||||||
|
.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<NotFoundException>();
|
||||||
|
}
|
||||||
|
|
||||||
|
#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<NotFoundException>();
|
||||||
|
}
|
||||||
|
|
||||||
|
#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
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user