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