xiangning a978e7f868 feat(workflow): 添加自定义函数类型和按钮三阶段动作链功能
- 引入 CustomFunctionType 枚举,支持 Script 和 Http 两种函数类型
- 在自定义函数中添加 Http 相关字段(HttpMethod、HttpUrl、HttpHeaders、HttpBodyTemplate)
- 实现 HTTP 函数执行器,支持变量替换和 SSRF 防护
- 将按钮动作链拆分为 before、main、after 三个阶段
- 新增前置和后置动作 JSON 字段存储
- 实现批量执行多个函数的 executeFunctions 动作类型
- 更新数据库迁移以支持新的字段结构
- 调整相关 DTO、命令和处理器以兼容新功能
2026-07-06 18:03:22 +08:00

236 lines
9.4 KiB
C#

using FluentAssertions;
using Microsoft.EntityFrameworkCore;
using Workflow.Application.BuildFlow.Applications.Commands;
using Workflow.Application.BuildFlow.Applications.Handlers;
using Workflow.Application.BuildFlow.Applications.Queries;
using Workflow.Application.BuildFlow.Buttons.Commands;
using Workflow.Application.BuildFlow.Buttons.Handlers;
using Workflow.Application.BuildFlow.Buttons.Queries;
using Workflow.Application.BuildFlow.CustomFunctions.Commands;
using Workflow.Application.BuildFlow.CustomFunctions.Handlers;
using Workflow.Application.BuildFlow.CustomFunctions.Queries;
using Workflow.Application.BuildFlow.Pages.Commands;
using Workflow.Application.BuildFlow.Pages.Handlers;
using Workflow.Application.BuildFlow.Pages.Queries;
using Workflow.Domain.Enums;
using Xunit;
namespace BuildFlow.Workflow.Tests.BuildFlow;
/// <summary>
/// BuildFlow 应用元数据测试,约束应用和页面定义作为低代码平台顶层归属存在。
/// </summary>
public class BuildFlowApplicationTests
{
[Fact]
public async Task CreateApplicationAndPage_PersistsMetadata()
{
await using var db = TestDbFactory.Create();
var appHandler = new CreateBuildFlowApplicationCommandHandler(db);
var pageHandler = new CreateBuildFlowPageDefinitionCommandHandler(db);
var app = await appHandler.Handle(
new CreateBuildFlowApplicationCommand("leave", "请假应用", "员工请假流程"),
CancellationToken.None);
var page = await pageHandler.Handle(
new CreateBuildFlowPageDefinitionCommand(
app.Id,
"leave-form",
"请假申请页",
BuildFlowPageType.Form,
"""{"type":"form","formCode":"leave-request"}"""),
CancellationToken.None);
app.Code.Should().Be("leave");
app.Status.Should().Be(BuildFlowMetadataStatus.Draft);
page.ApplicationId.Should().Be(app.Id);
page.SchemaJson.Should().Contain("leave-request");
}
[Fact]
public async Task GetApplicationList_FiltersByStatus()
{
await using var db = TestDbFactory.Create();
var appHandler = new CreateBuildFlowApplicationCommandHandler(db);
var draft = await appHandler.Handle(
new CreateBuildFlowApplicationCommand("draft-app", "草稿应用", null),
CancellationToken.None);
await appHandler.Handle(
new CreateBuildFlowApplicationCommand("published-app", "已发布应用", null),
CancellationToken.None);
var published = await db.BuildFlowApplications.SingleAsync(x => x.Code == "published-app");
published.Status = BuildFlowMetadataStatus.Published;
await db.SaveChangesAsync();
var handler = new GetBuildFlowApplicationListQueryHandler(db);
var result = await handler.Handle(
new GetBuildFlowApplicationListQuery(1, 20, BuildFlowMetadataStatus.Draft, null),
CancellationToken.None);
result.Total.Should().Be(1);
result.Items.Should().ContainSingle(x => x.Id == draft.Id && x.Code == "draft-app");
}
[Fact]
public async Task GetPageList_FiltersByApplication()
{
await using var db = TestDbFactory.Create();
var appHandler = new CreateBuildFlowApplicationCommandHandler(db);
var pageHandler = new CreateBuildFlowPageDefinitionCommandHandler(db);
var leaveApp = await appHandler.Handle(new CreateBuildFlowApplicationCommand("leave", "请假应用", null), CancellationToken.None);
var purchaseApp = await appHandler.Handle(new CreateBuildFlowApplicationCommand("purchase", "采购应用", null), CancellationToken.None);
await pageHandler.Handle(
new CreateBuildFlowPageDefinitionCommand(leaveApp.Id, "leave-list", "请假列表", BuildFlowPageType.List, "{}"),
CancellationToken.None);
await pageHandler.Handle(
new CreateBuildFlowPageDefinitionCommand(purchaseApp.Id, "purchase-list", "采购列表", BuildFlowPageType.List, "{}"),
CancellationToken.None);
var handler = new GetBuildFlowPageDefinitionListQueryHandler(db);
var result = await handler.Handle(
new GetBuildFlowPageDefinitionListQuery(1, 20, leaveApp.Id, null, null),
CancellationToken.None);
result.Total.Should().Be(1);
result.Items.Should().ContainSingle(x => x.ApplicationId == leaveApp.Id && x.Code == "leave-list");
}
[Fact]
public async Task GetButtonList_FiltersByFormAndPlacement()
{
await using var db = TestDbFactory.Create();
var formDefinitionId = Guid.NewGuid();
var buttonHandler = new CreateBuildFlowButtonDefinitionCommandHandler(db);
await buttonHandler.Handle(
new CreateBuildFlowButtonDefinitionCommand(
null,
null,
formDefinitionId,
null,
"submit",
"提交",
"lucide:send",
BuildFlowButtonPlacement.FormToolbar,
"buildflow:button:submit",
null,
null,
null,
null,
"[]",
null,
10),
CancellationToken.None);
await buttonHandler.Handle(
new CreateBuildFlowButtonDefinitionCommand(
null,
null,
Guid.NewGuid(),
null,
"approve",
"审批",
"lucide:check",
BuildFlowButtonPlacement.RowAction,
"buildflow:button:approve",
null,
null,
null,
null,
"[]",
null,
20),
CancellationToken.None);
var handler = new GetBuildFlowButtonDefinitionListQueryHandler(db);
var result = await handler.Handle(
new GetBuildFlowButtonDefinitionListQuery(
1,
20,
null,
null,
formDefinitionId,
null,
BuildFlowButtonPlacement.FormToolbar,
null),
CancellationToken.None);
result.Total.Should().Be(1);
result.Items.Should().ContainSingle(x => x.Code == "submit" && x.FormDefinitionId == formDefinitionId);
}
[Fact]
public async Task GetCustomFunctionList_FiltersPublishedFunctions()
{
await using var db = TestDbFactory.Create();
var createHandler = new CreateCustomFunctionCommandHandler(db);
var publishHandler = new PublishCustomFunctionCommandHandler(db);
var draft = await createHandler.Handle(
new CreateCustomFunctionCommand("draftRule", "草稿规则", null, CustomFunctionType.Script, "1", null, null, null, null, null, null, 1000),
CancellationToken.None);
var published = await createHandler.Handle(
new CreateCustomFunctionCommand("publishedRule", "已发布规则", null, CustomFunctionType.Script, "2", null, null, null, null, null, null, 1000),
CancellationToken.None);
await publishHandler.Handle(new PublishCustomFunctionCommand(published.Id), CancellationToken.None);
var handler = new GetCustomFunctionListQueryHandler(db);
var result = await handler.Handle(
new GetCustomFunctionListQuery(1, 20, BuildFlowMetadataStatus.Published, null),
CancellationToken.None);
result.Total.Should().Be(1);
result.Items.Should().ContainSingle(x => x.Id == published.Id && x.Name == "publishedRule");
result.Items.Should().NotContain(x => x.Id == draft.Id);
}
[Fact]
public async Task GetCustomFunctionList_FiltersByFormDefinition()
{
await using var db = TestDbFactory.Create();
var expenseFormId = Guid.NewGuid();
var purchaseFormId = Guid.NewGuid();
var createHandler = new CreateCustomFunctionCommandHandler(db);
var expenseFunction = await createHandler.Handle(
new CreateCustomFunctionCommand(
"expenseAmountCalc",
"费用金额计算",
null,
CustomFunctionType.Script,
"return input.amount;",
null,
null,
null,
null,
null,
null,
1000,
expenseFormId),
CancellationToken.None);
await createHandler.Handle(
new CreateCustomFunctionCommand(
"purchaseAmountCalc",
"采购金额计算",
null,
CustomFunctionType.Script,
"return input.amount;",
null,
null,
null,
null,
null,
null,
1000,
purchaseFormId),
CancellationToken.None);
var handler = new GetCustomFunctionListQueryHandler(db);
var result = await handler.Handle(
new GetCustomFunctionListQuery(1, 20, null, null, expenseFormId),
CancellationToken.None);
result.Total.Should().Be(1);
result.Items.Should().ContainSingle(x =>
x.Id == expenseFunction.Id &&
x.FormDefinitionId == expenseFormId &&
x.Name == "expenseAmountCalc");
}
}