feat: add Node CRUD commands with tests

This commit is contained in:
向宁 2026-05-21 14:42:51 +08:00
parent 19399378c1
commit f18a48379f
4 changed files with 328 additions and 0 deletions

View File

@ -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
);
}
}

View File

@ -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;
}
}

View File

@ -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
);
}
}

View 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
}