using FluentAssertions;
using Microsoft.EntityFrameworkCore;
using Workflow.Application.Form.FormDefinition.Commands;
using Workflow.Domain.Entities;
using Workflow.Domain.Enums;
using Workflow.Domain.Exceptions;
using Workflow.Infrastructure.Persistence;
using Xunit;
namespace Workflow.Tests.Form;
///
/// 表单设计器与流程设计器耦合关系下的删除边界测试。
///
/// 背景:FormDefinition 被软删除后,全局查询过滤器会让 GetFormDefinitionByIdQuery 等读取路径
/// 直接抛 NotFoundException;StartWorkflowInstanceCommand 也会抛 BusinessException。
/// 因此删除一张仍被流程定义/节点引用的表单,会在运行期产生孤儿引用。
/// 删除前必须阻断并给出明确错误,而不是让下游崩溃。
///
[Collection("FormTests")]
public class FormDeletionReferenceTests
{
private readonly FormTestFixture _fixture;
public FormDeletionReferenceTests(FormTestFixtureClassFixture fixture)
{
_fixture = fixture;
}
private const string ValidSchema = """
{
"type": "object",
"properties": {
"name": { "type": "string", "title": "姓名", "x-decorator": "FormItem", "x-component": "Input" }
}
}
""";
private static FormDefinition NewForm(Guid id, string code = "REF_FORM") => new()
{
Id = id,
Name = "引用测试表单",
Code = code,
Version = 1,
Status = FormStatus.Published,
SchemaJson = ValidSchema,
};
// ----------------------------------------------------------------
// 已被流程定义引用:删除必须被阻断
// ----------------------------------------------------------------
[Fact]
public async Task DeleteFormDefinition_ReferencedByWorkflowDefinition_ThrowsBusinessException()
{
var formId = Guid.NewGuid();
await using var db = await _fixture.CreateDbContextWithSeedAsync(
testName: nameof(DeleteFormDefinition_ReferencedByWorkflowDefinition_ThrowsBusinessException),
seedAction: ctx =>
{
ctx.FormDefinitions.Add(NewForm(formId));
ctx.WorkflowDefinitions.Add(new WorkflowDefinition
{
Id = Guid.NewGuid(),
Name = "引用该表单的流程",
Code = "WF_REF_FORM_1",
Version = 1,
Status = DefinitionStatus.Draft,
IsEnabled = true,
FormDefinitionId = formId,
});
});
var handler = new DeleteFormDefinitionCommandHandler(db);
var act = () => handler.Handle(new DeleteFormDefinitionCommand(formId), CancellationToken.None);
await act.Should().ThrowAsync()
.WithMessage("*流程*");
// 删除被阻断,表单保持可见
var stillThere = await db.FormDefinitions.FindAsync(formId);
stillThere.Should().NotBeNull();
stillThere!.IsDeleted.Should().BeFalse();
}
// ----------------------------------------------------------------
// 已被流程节点引用:删除必须被阻断
// ----------------------------------------------------------------
[Fact]
public async Task DeleteFormDefinition_ReferencedByWorkflowNode_ThrowsBusinessException()
{
var formId = Guid.NewGuid();
var definitionId = Guid.NewGuid();
await using var db = await _fixture.CreateDbContextWithSeedAsync(
testName: nameof(DeleteFormDefinition_ReferencedByWorkflowNode_ThrowsBusinessException),
seedAction: ctx =>
{
ctx.FormDefinitions.Add(NewForm(formId));
ctx.WorkflowDefinitions.Add(new WorkflowDefinition
{
Id = definitionId,
Name = "节点引用表单的流程",
Code = "WF_REF_FORM_2",
Version = 1,
Status = DefinitionStatus.Draft,
IsEnabled = true,
});
ctx.WorkflowNodes.Add(new WorkflowNode
{
Id = Guid.NewGuid(),
DefinitionId = definitionId,
NodeType = NodeType.Approval,
Name = "审批节点",
FormDefinitionId = formId,
});
});
var handler = new DeleteFormDefinitionCommandHandler(db);
var act = () => handler.Handle(new DeleteFormDefinitionCommand(formId), CancellationToken.None);
await act.Should().ThrowAsync()
.WithMessage("*流程*");
var stillThere = await db.FormDefinitions.FindAsync(formId);
stillThere.Should().NotBeNull();
stillThere!.IsDeleted.Should().BeFalse();
}
// ----------------------------------------------------------------
// 仅被已软删除的流程引用:可正常删除(孤儿引用不应永久锁死表单)
// ----------------------------------------------------------------
[Fact]
public async Task DeleteFormDefinition_OnlyReferencedBySoftDeletedDefinition_AllowsDeletion()
{
var formId = Guid.NewGuid();
await using var db = await _fixture.CreateDbContextWithSeedAsync(
testName: nameof(DeleteFormDefinition_OnlyReferencedBySoftDeletedDefinition_AllowsDeletion),
seedAction: ctx =>
{
ctx.FormDefinitions.Add(NewForm(formId));
ctx.WorkflowDefinitions.Add(new WorkflowDefinition
{
Id = Guid.NewGuid(),
Name = "已删除的流程",
Code = "WF_DELETED",
Version = 1,
Status = DefinitionStatus.Draft,
IsEnabled = true,
FormDefinitionId = formId,
IsDeleted = true,
});
});
var handler = new DeleteFormDefinitionCommandHandler(db);
await handler.Handle(new DeleteFormDefinitionCommand(formId), CancellationToken.None);
var normalQuery = await db.FormDefinitions.FirstOrDefaultAsync(f => f.Id == formId);
normalQuery.Should().BeNull();
var deleted = await db.FormDefinitions.IgnoreQueryFilters().FirstAsync(f => f.Id == formId);
deleted.IsDeleted.Should().BeTrue();
}
// ----------------------------------------------------------------
// 无任何引用:可正常删除(回归现有行为,确保不误伤)
// ----------------------------------------------------------------
[Fact]
public async Task DeleteFormDefinition_NotReferenced_Succeeds()
{
var formId = Guid.NewGuid();
await using var db = await _fixture.CreateDbContextWithSeedAsync(
testName: nameof(DeleteFormDefinition_NotReferenced_Succeeds),
seedAction: ctx => ctx.FormDefinitions.Add(NewForm(formId, code: "UNREF")));
var handler = new DeleteFormDefinitionCommandHandler(db);
await handler.Handle(new DeleteFormDefinitionCommand(formId), CancellationToken.None);
var normalQuery = await db.FormDefinitions.FirstOrDefaultAsync(f => f.Id == formId);
normalQuery.Should().BeNull();
var deleted = await db.FormDefinitions.IgnoreQueryFilters().FirstAsync(f => f.Id == formId);
deleted.IsDeleted.Should().BeTrue();
}
}