feat: add Node CRUD commands with tests
This commit is contained in:
parent
19399378c1
commit
f18a48379f
@ -0,0 +1,51 @@
|
|||||||
|
using MediatR;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
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 CreateNodeCommand(
|
||||||
|
Guid DefinitionId,
|
||||||
|
NodeType NodeType,
|
||||||
|
string Name,
|
||||||
|
string? Config,
|
||||||
|
int PositionX,
|
||||||
|
int PositionY
|
||||||
|
) : IRequest<WorkflowNodeDto>;
|
||||||
|
|
||||||
|
public class CreateNodeCommandHandler(WorkflowDbContext db)
|
||||||
|
: IRequestHandler<CreateNodeCommand, WorkflowNodeDto>
|
||||||
|
{
|
||||||
|
public async Task<WorkflowNodeDto> Handle(CreateNodeCommand request, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var definition = await db.WorkflowDefinitions.FindAsync([request.DefinitionId], cancellationToken)
|
||||||
|
?? throw new NotFoundException($"Workflow definition '{request.DefinitionId}' not found.");
|
||||||
|
|
||||||
|
var entity = new WorkflowNode
|
||||||
|
{
|
||||||
|
Id = Guid.NewGuid(),
|
||||||
|
DefinitionId = request.DefinitionId,
|
||||||
|
NodeType = request.NodeType,
|
||||||
|
Name = request.Name,
|
||||||
|
Config = request.Config,
|
||||||
|
PositionX = request.PositionX,
|
||||||
|
PositionY = request.PositionY
|
||||||
|
};
|
||||||
|
|
||||||
|
db.WorkflowNodes.Add(entity);
|
||||||
|
await db.SaveChangesAsync(cancellationToken);
|
||||||
|
|
||||||
|
return new WorkflowNodeDto(
|
||||||
|
entity.Id,
|
||||||
|
entity.NodeType,
|
||||||
|
entity.Name,
|
||||||
|
entity.Config,
|
||||||
|
entity.PositionX,
|
||||||
|
entity.PositionY
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,22 @@
|
|||||||
|
using MediatR;
|
||||||
|
using Workflow.Domain.Exceptions;
|
||||||
|
using Workflow.Infrastructure.Persistence;
|
||||||
|
|
||||||
|
namespace Workflow.Application.Features.WorkflowDefinitions.Commands;
|
||||||
|
|
||||||
|
public record DeleteNodeCommand(Guid NodeId) : IRequest<Unit>;
|
||||||
|
|
||||||
|
public class DeleteNodeCommandHandler(WorkflowDbContext db)
|
||||||
|
: IRequestHandler<DeleteNodeCommand, Unit>
|
||||||
|
{
|
||||||
|
public async Task<Unit> Handle(DeleteNodeCommand request, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var entity = await db.WorkflowNodes.FindAsync([request.NodeId], cancellationToken)
|
||||||
|
?? throw new NotFoundException($"Workflow node '{request.NodeId}' not found.");
|
||||||
|
|
||||||
|
db.WorkflowNodes.Remove(entity);
|
||||||
|
await db.SaveChangesAsync(cancellationToken);
|
||||||
|
|
||||||
|
return Unit.Value;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,40 @@
|
|||||||
|
using MediatR;
|
||||||
|
using Workflow.Application.Features.WorkflowDefinitions.DTOs;
|
||||||
|
using Workflow.Domain.Exceptions;
|
||||||
|
using Workflow.Infrastructure.Persistence;
|
||||||
|
|
||||||
|
namespace Workflow.Application.Features.WorkflowDefinitions.Commands;
|
||||||
|
|
||||||
|
public record UpdateNodeCommand(
|
||||||
|
Guid NodeId,
|
||||||
|
string Name,
|
||||||
|
string? Config,
|
||||||
|
int PositionX,
|
||||||
|
int PositionY
|
||||||
|
) : IRequest<WorkflowNodeDto>;
|
||||||
|
|
||||||
|
public class UpdateNodeCommandHandler(WorkflowDbContext db)
|
||||||
|
: IRequestHandler<UpdateNodeCommand, WorkflowNodeDto>
|
||||||
|
{
|
||||||
|
public async Task<WorkflowNodeDto> Handle(UpdateNodeCommand request, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var entity = await db.WorkflowNodes.FindAsync([request.NodeId], cancellationToken)
|
||||||
|
?? throw new NotFoundException($"Workflow node '{request.NodeId}' not found.");
|
||||||
|
|
||||||
|
entity.Name = request.Name;
|
||||||
|
entity.Config = request.Config;
|
||||||
|
entity.PositionX = request.PositionX;
|
||||||
|
entity.PositionY = request.PositionY;
|
||||||
|
|
||||||
|
await db.SaveChangesAsync(cancellationToken);
|
||||||
|
|
||||||
|
return new WorkflowNodeDto(
|
||||||
|
entity.Id,
|
||||||
|
entity.NodeType,
|
||||||
|
entity.Name,
|
||||||
|
entity.Config,
|
||||||
|
entity.PositionX,
|
||||||
|
entity.PositionY
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
215
tests/Workflow.Tests/Handlers/NodeCommandHandlerTests.cs
Normal file
215
tests/Workflow.Tests/Handlers/NodeCommandHandlerTests.cs
Normal file
@ -0,0 +1,215 @@
|
|||||||
|
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 NodeCommandHandlerTests
|
||||||
|
{
|
||||||
|
private static WorkflowDbContext CreateDbContext()
|
||||||
|
{
|
||||||
|
var options = new DbContextOptionsBuilder<WorkflowDbContext>()
|
||||||
|
.UseInMemoryDatabase(Guid.NewGuid().ToString())
|
||||||
|
.Options;
|
||||||
|
return new WorkflowDbContext(options);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task<WorkflowDefinition> SeedDefinition(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);
|
||||||
|
await db.SaveChangesAsync();
|
||||||
|
return definition;
|
||||||
|
}
|
||||||
|
|
||||||
|
#region CreateNode
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task CreateNode_ReturnsNodeDto()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
await using var db = CreateDbContext();
|
||||||
|
var definition = await SeedDefinition(db);
|
||||||
|
|
||||||
|
var handler = new CreateNodeCommandHandler(db);
|
||||||
|
var command = new CreateNodeCommand(
|
||||||
|
DefinitionId: definition.Id,
|
||||||
|
NodeType: NodeType.Start,
|
||||||
|
Name: "Start Node",
|
||||||
|
Config: null,
|
||||||
|
PositionX: 100,
|
||||||
|
PositionY: 200
|
||||||
|
);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await handler.Handle(command, CancellationToken.None);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
result.Should().NotBeNull();
|
||||||
|
result.Name.Should().Be("Start Node");
|
||||||
|
result.NodeType.Should().Be(NodeType.Start);
|
||||||
|
result.PositionX.Should().Be(100);
|
||||||
|
result.PositionY.Should().Be(200);
|
||||||
|
result.Config.Should().BeNull();
|
||||||
|
result.Id.Should().NotBeEmpty();
|
||||||
|
|
||||||
|
var entity = await db.WorkflowNodes.FindAsync(result.Id);
|
||||||
|
entity.Should().NotBeNull();
|
||||||
|
entity!.DefinitionId.Should().Be(definition.Id);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task CreateNode_WithMissingDefinition_ThrowsNotFoundException()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
await using var db = CreateDbContext();
|
||||||
|
var handler = new CreateNodeCommandHandler(db);
|
||||||
|
var command = new CreateNodeCommand(
|
||||||
|
DefinitionId: Guid.NewGuid(),
|
||||||
|
NodeType: NodeType.Approval,
|
||||||
|
Name: "Orphan Node",
|
||||||
|
Config: null,
|
||||||
|
PositionX: 0,
|
||||||
|
PositionY: 0
|
||||||
|
);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var act = () => handler.Handle(command, CancellationToken.None);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
await act.Should().ThrowAsync<NotFoundException>();
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region UpdateNode
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task UpdateNode_UpdatesNameAndPosition()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
await using var db = CreateDbContext();
|
||||||
|
var definition = await SeedDefinition(db);
|
||||||
|
|
||||||
|
var nodeId = Guid.NewGuid();
|
||||||
|
db.WorkflowNodes.Add(new WorkflowNode
|
||||||
|
{
|
||||||
|
Id = nodeId,
|
||||||
|
DefinitionId = definition.Id,
|
||||||
|
NodeType = NodeType.Approval,
|
||||||
|
Name = "Old Name",
|
||||||
|
PositionX = 50,
|
||||||
|
PositionY = 50
|
||||||
|
});
|
||||||
|
await db.SaveChangesAsync();
|
||||||
|
|
||||||
|
var handler = new UpdateNodeCommandHandler(db);
|
||||||
|
var command = new UpdateNodeCommand(
|
||||||
|
NodeId: nodeId,
|
||||||
|
Name: "Updated Name",
|
||||||
|
Config: "{\"timeout\": 300}",
|
||||||
|
PositionX: 200,
|
||||||
|
PositionY: 300
|
||||||
|
);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await handler.Handle(command, CancellationToken.None);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
result.Should().NotBeNull();
|
||||||
|
result.Name.Should().Be("Updated Name");
|
||||||
|
result.Config.Should().Be("{\"timeout\": 300}");
|
||||||
|
result.PositionX.Should().Be(200);
|
||||||
|
result.PositionY.Should().Be(300);
|
||||||
|
result.Id.Should().Be(nodeId);
|
||||||
|
|
||||||
|
var entity = await db.WorkflowNodes.FindAsync(nodeId);
|
||||||
|
entity.Should().NotBeNull();
|
||||||
|
entity!.Name.Should().Be("Updated Name");
|
||||||
|
entity.PositionX.Should().Be(200);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task UpdateNode_WithMissingNode_ThrowsNotFoundException()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
await using var db = CreateDbContext();
|
||||||
|
var handler = new UpdateNodeCommandHandler(db);
|
||||||
|
var command = new UpdateNodeCommand(
|
||||||
|
NodeId: Guid.NewGuid(),
|
||||||
|
Name: "Ghost",
|
||||||
|
Config: null,
|
||||||
|
PositionX: 0,
|
||||||
|
PositionY: 0
|
||||||
|
);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var act = () => handler.Handle(command, CancellationToken.None);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
await act.Should().ThrowAsync<NotFoundException>();
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region DeleteNode
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task DeleteNode_RemovesNode()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
await using var db = CreateDbContext();
|
||||||
|
var definition = await SeedDefinition(db);
|
||||||
|
|
||||||
|
var nodeId = Guid.NewGuid();
|
||||||
|
db.WorkflowNodes.Add(new WorkflowNode
|
||||||
|
{
|
||||||
|
Id = nodeId,
|
||||||
|
DefinitionId = definition.Id,
|
||||||
|
NodeType = NodeType.End,
|
||||||
|
Name = "To Delete",
|
||||||
|
PositionX = 0,
|
||||||
|
PositionY = 0
|
||||||
|
});
|
||||||
|
await db.SaveChangesAsync();
|
||||||
|
|
||||||
|
var handler = new DeleteNodeCommandHandler(db);
|
||||||
|
var command = new DeleteNodeCommand(NodeId: nodeId);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await handler.Handle(command, CancellationToken.None);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
var nodes = await db.WorkflowNodes.ToListAsync();
|
||||||
|
nodes.Should().BeEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task DeleteNode_WithMissingNode_ThrowsNotFoundException()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
await using var db = CreateDbContext();
|
||||||
|
var handler = new DeleteNodeCommandHandler(db);
|
||||||
|
var command = new DeleteNodeCommand(NodeId: Guid.NewGuid());
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var act = () => handler.Handle(command, CancellationToken.None);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
await act.Should().ThrowAsync<NotFoundException>();
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user