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