work-flow/tests/Workflow.Tests/Handlers/WorkflowDefinitionHandlerTests.cs

359 lines
11 KiB
C#

using FluentAssertions;
using Microsoft.EntityFrameworkCore;
using Workflow.Application.Features.WorkflowDefinitions.Commands;
using Workflow.Application.Features.WorkflowDefinitions.Queries;
using Workflow.Domain.Entities;
using Workflow.Domain.Enums;
using Workflow.Domain.Exceptions;
using Workflow.Infrastructure.Persistence;
using Xunit;
namespace Workflow.Tests.Handlers;
public class WorkflowDefinitionHandlerTests
{
private static WorkflowDbContext CreateDbContext()
{
var options = new DbContextOptionsBuilder<WorkflowDbContext>()
.UseInMemoryDatabase(Guid.NewGuid().ToString())
.Options;
return new WorkflowDbContext(options);
}
#region CreateWorkflowDefinition
[Fact]
public async Task CreateDefinition_SavesToDatabase()
{
// Arrange
await using var db = CreateDbContext();
var handler = new CreateWorkflowDefinitionCommandHandler(db);
var command = new CreateWorkflowDefinitionCommand(
Name: "Leave Approval",
Code: "leave-approval",
Description: "Standard leave approval workflow",
FormDefinitionId: null
);
// Act
var result = await handler.Handle(command, CancellationToken.None);
// Assert
result.Should().NotBeNull();
result.Name.Should().Be("Leave Approval");
result.Code.Should().Be("leave-approval");
result.Description.Should().Be("Standard leave approval workflow");
result.Status.Should().Be(DefinitionStatus.Draft);
result.Version.Should().Be(1);
var entity = await db.WorkflowDefinitions.FirstOrDefaultAsync(d => d.Code == "leave-approval");
entity.Should().NotBeNull();
entity!.Name.Should().Be("Leave Approval");
}
[Fact]
public async Task CreateDefinition_WithDuplicateCode_ThrowsBusinessException()
{
// Arrange
await using var db = CreateDbContext();
var existing = new WorkflowDefinition
{
Id = Guid.NewGuid(),
Name = "Existing",
Code = "duplicate-code",
Status = DefinitionStatus.Draft,
Version = 1
};
db.WorkflowDefinitions.Add(existing);
await db.SaveChangesAsync();
var handler = new CreateWorkflowDefinitionCommandHandler(db);
var command = new CreateWorkflowDefinitionCommand(
Name: "New Workflow",
Code: "duplicate-code",
Description: "Should fail",
FormDefinitionId: null
);
// Act
var act = () => handler.Handle(command, CancellationToken.None);
// Assert
await act.Should().ThrowAsync<BusinessException>()
.WithMessage("*duplicate-code*");
}
#endregion
#region UpdateWorkflowDefinition
[Fact]
public async Task UpdateDefinition_UpdatesFields()
{
// Arrange
await using var db = CreateDbContext();
var definitionId = Guid.NewGuid();
var existing = new WorkflowDefinition
{
Id = definitionId,
Name = "Old Name",
Code = "test-workflow",
Description = "Old description",
Status = DefinitionStatus.Draft,
Version = 1
};
db.WorkflowDefinitions.Add(existing);
await db.SaveChangesAsync();
var handler = new UpdateWorkflowDefinitionCommandHandler(db);
var command = new UpdateWorkflowDefinitionCommand(
Id: definitionId,
Name: "Updated Name",
Description: "Updated description",
DefinitionJson: "{\"nodes\":[],\"edges\":[]}",
FormDefinitionId: null
);
// Act
var result = await handler.Handle(command, CancellationToken.None);
// Assert
result.Should().NotBeNull();
result.Name.Should().Be("Updated Name");
result.Description.Should().Be("Updated description");
result.DefinitionJson.Should().Be("{\"nodes\":[],\"edges\":[]}");
var entity = await db.WorkflowDefinitions.FindAsync(definitionId);
entity.Should().NotBeNull();
entity!.Name.Should().Be("Updated Name");
}
#endregion
#region DeleteWorkflowDefinition
[Fact]
public async Task DeleteDefinition_SoftDeletes()
{
// Arrange
await using var db = CreateDbContext();
var definitionId = Guid.NewGuid();
var existing = new WorkflowDefinition
{
Id = definitionId,
Name = "To Delete",
Code = "to-delete",
Status = DefinitionStatus.Draft,
Version = 1
};
db.WorkflowDefinitions.Add(existing);
await db.SaveChangesAsync();
var handler = new DeleteWorkflowDefinitionCommandHandler(db);
var command = new DeleteWorkflowDefinitionCommand(Id: definitionId);
// Act
await handler.Handle(command, CancellationToken.None);
// Assert - soft delete means the entity still exists but IsDeleted = true
var entity = await db.WorkflowDefinitions
.IgnoreQueryFilters()
.FirstOrDefaultAsync(d => d.Id == definitionId);
entity.Should().NotBeNull();
entity!.IsDeleted.Should().BeTrue();
// Normal query should not return the deleted entity
// Note: InMemory provider does not support global query filters,
// so FindAsync still returns the entity. Verify via filtered query instead.
var activeEntity = await db.WorkflowDefinitions
.FirstOrDefaultAsync(d => d.Id == definitionId);
activeEntity?.IsDeleted.Should().BeTrue();
}
#endregion
#region PublishWorkflowDefinition
[Fact]
public async Task PublishDefinition_ChangesStatusAndIncrementsVersion()
{
// Arrange
await using var db = CreateDbContext();
var definitionId = Guid.NewGuid();
var existing = new WorkflowDefinition
{
Id = definitionId,
Name = "Publishable Workflow",
Code = "publishable",
Status = DefinitionStatus.Draft,
Version = 1
};
db.WorkflowDefinitions.Add(existing);
await db.SaveChangesAsync();
var handler = new PublishWorkflowDefinitionCommandHandler(db);
var command = new PublishWorkflowDefinitionCommand(Id: definitionId);
// Act
await handler.Handle(command, CancellationToken.None);
// Assert
var entity = await db.WorkflowDefinitions.FindAsync(definitionId);
entity.Should().NotBeNull();
entity!.Status.Should().Be(DefinitionStatus.Published);
entity.Version.Should().Be(2);
}
#endregion
#region GetWorkflowDefinitionList
[Fact]
public async Task GetDefinitionList_ReturnsPaged()
{
// Arrange
await using var db = CreateDbContext();
for (int i = 1; i <= 15; i++)
{
db.WorkflowDefinitions.Add(new WorkflowDefinition
{
Id = Guid.NewGuid(),
Name = $"Workflow {i}",
Code = $"workflow-{i}",
Status = DefinitionStatus.Draft,
Version = 1
});
}
await db.SaveChangesAsync();
var handler = new GetWorkflowDefinitionListQueryHandler(db);
var query = new GetWorkflowDefinitionListQuery(
PageIndex: 1,
PageSize: 10,
Status: null
);
// Act
var result = await handler.Handle(query, CancellationToken.None);
// Assert
result.Should().NotBeNull();
result.Items.Should().HaveCount(10);
result.Total.Should().Be(15);
result.PageIndex.Should().Be(1);
result.PageSize.Should().Be(10);
}
[Fact]
public async Task GetDefinitionList_FiltersByStatus()
{
// Arrange
await using var db = CreateDbContext();
db.WorkflowDefinitions.Add(new WorkflowDefinition
{
Id = Guid.NewGuid(),
Name = "Draft Workflow",
Code = "draft-wf",
Status = DefinitionStatus.Draft,
Version = 1
});
db.WorkflowDefinitions.Add(new WorkflowDefinition
{
Id = Guid.NewGuid(),
Name = "Published Workflow",
Code = "published-wf",
Status = DefinitionStatus.Published,
Version = 2
});
await db.SaveChangesAsync();
var handler = new GetWorkflowDefinitionListQueryHandler(db);
var query = new GetWorkflowDefinitionListQuery(
PageIndex: 1,
PageSize: 10,
Status: DefinitionStatus.Published
);
// Act
var result = await handler.Handle(query, CancellationToken.None);
// Assert
result.Should().NotBeNull();
result.Items.Should().HaveCount(1);
result.Items[0].Status.Should().Be(DefinitionStatus.Published);
result.Total.Should().Be(1);
}
#endregion
#region GetWorkflowDefinitionById
[Fact]
public async Task GetDefinitionById_IncludesNodesAndEdges()
{
// Arrange
await using var db = CreateDbContext();
var definitionId = Guid.NewGuid();
var definition = new WorkflowDefinition
{
Id = definitionId,
Name = "Workflow With Graph",
Code = "graph-wf",
Status = DefinitionStatus.Published,
Version = 1
};
db.WorkflowDefinitions.Add(definition);
var startNode = new WorkflowNode
{
Id = Guid.NewGuid(),
DefinitionId = definitionId,
NodeType = NodeType.Start,
Name = "Start",
PositionX = 100,
PositionY = 100
};
var endNode = new WorkflowNode
{
Id = Guid.NewGuid(),
DefinitionId = definitionId,
NodeType = NodeType.End,
Name = "End",
PositionX = 500,
PositionY = 100
};
db.WorkflowNodes.AddRange(startNode, endNode);
var edge = new WorkflowEdge
{
Id = Guid.NewGuid(),
DefinitionId = definitionId,
SourceNodeId = startNode.Id,
TargetNodeId = endNode.Id,
Label = "to end"
};
db.WorkflowEdges.Add(edge);
await db.SaveChangesAsync();
var handler = new GetWorkflowDefinitionByIdQueryHandler(db);
var query = new GetWorkflowDefinitionByIdQuery(Id: definitionId);
// Act
var result = await handler.Handle(query, CancellationToken.None);
// Assert
result.Should().NotBeNull();
result.Id.Should().Be(definitionId);
result.Nodes.Should().NotBeNull().And.HaveCount(2);
result.Edges.Should().NotBeNull().And.HaveCount(1);
result.Nodes.Should().ContainSingle(n => n.NodeType == NodeType.Start);
result.Nodes.Should().ContainSingle(n => n.NodeType == NodeType.End);
result.Edges[0].SourceNodeId.Should().Be(startNode.Id);
result.Edges[0].TargetNodeId.Should().Be(endNode.Id);
}
#endregion
}