- 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
184 lines
7.4 KiB
C#
184 lines
7.4 KiB
C#
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 等读取路径
|
||
/// 直接抛 NotFoundException;StartWorkflowInstanceCommand 也会抛 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();
|
||
}
|
||
}
|