work-flow/tests/Workflow.Tests/Form/FormDeletionReferenceTests.cs
向宁 9f878286e7 feat: form versioning, notification center, scheduler and webhooks
- Add FormDefinitionVersion with compare/versions endpoints and schema differ
- Add Notification entity, endpoints and application features
- Add Scheduler (timeout) and WebhookDispatcher services
- Add FormDataValidator/FieldPermissionEvaluator/ReactionEvaluator
- Add workflow task mark-read, CC support and SystemUserContext
- Add EF migrations for form versions and notifications
- Add unit tests for form schema, notifications, scheduler and serialization
2026-06-14 15:03:11 +08:00

184 lines
7.4 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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;
/// <summary>
/// 表单设计器与流程设计器耦合关系下的删除边界测试。
///
/// 背景FormDefinition 被软删除后,全局查询过滤器会让 GetFormDefinitionByIdQuery 等读取路径
/// 直接抛 NotFoundExceptionStartWorkflowInstanceCommand 也会抛 BusinessException。
/// 因此删除一张仍被流程定义/节点引用的表单,会在运行期产生孤儿引用。
/// 删除前必须阻断并给出明确错误,而不是让下游崩溃。
/// </summary>
[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<BusinessException>()
.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<BusinessException>()
.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();
}
}