- 在菜单系统中新增 RenderType、FormDefinitionId 和 RuntimeViewMode 字段 - 实现自定义表单菜单类型的组件映射和验证逻辑 - 为分片上传相关端点添加 file:upload 权限控制 - 为桶和文件夹操作添加 bucket:write 和 file:write 权限控制 - 为文件共享功能添加 file-share:write 权限控制 - 为文件读取操作添加 file:read 权限控制 - 扩展 AI 模型提供商枚举,支持更多模型供应商 - 在 AI 模型创建和更新时添加枚举值验证 - 重构工作流按钮权限检查逻辑,使用统一的权限码查询方式 - 为 JSON 格式文档添加提取器支持 - 增强表单数据派生服务,支持自定义表单关联解析 - 为表单数据添加按表单定义 ID 的索引优化 - 添加聊天消息搜索和上下文获取的集成测试 - 修正删除菜单端点的 ID 类型从 Guid 改为 long
221 lines
8.3 KiB
C#
221 lines
8.3 KiB
C#
using BuildFlow.Platform.Application.Auth.Queries;
|
||
using FluentAssertions;
|
||
using BuildFlow.Shared.Domain.Common;
|
||
using MediatR;
|
||
using Microsoft.EntityFrameworkCore;
|
||
using Workflow.Application.BuildFlow.Buttons.Commands;
|
||
using Workflow.Application.BuildFlow.Buttons.Handlers;
|
||
using Workflow.Application.BuildFlow.Buttons.Services;
|
||
using Workflow.Application.BuildFlow.CustomFunctions.Commands;
|
||
using Workflow.Application.BuildFlow.CustomFunctions.Handlers;
|
||
using Workflow.Application.BuildFlow.CustomFunctions.Services;
|
||
using Workflow.Domain.Entities;
|
||
using Workflow.Domain.Enums;
|
||
using Xunit;
|
||
|
||
namespace BuildFlow.Workflow.Tests.BuildFlow;
|
||
|
||
/// <summary>
|
||
/// 按钮动作链测试,约束按钮配置驱动后端动作执行,而不是前端写死行为。
|
||
/// </summary>
|
||
public class BuildFlowButtonActionTests
|
||
{
|
||
[Fact]
|
||
public async Task CreateButtonDefinition_PersistsActionMetadata()
|
||
{
|
||
await using var db = TestDbFactory.Create();
|
||
var handler = new CreateBuildFlowButtonDefinitionCommandHandler(db);
|
||
|
||
var button = await handler.Handle(
|
||
new CreateBuildFlowButtonDefinitionCommand(
|
||
null,
|
||
null,
|
||
null,
|
||
null,
|
||
"submit",
|
||
"提交",
|
||
"send",
|
||
BuildFlowButtonPlacement.FormToolbar,
|
||
"form:submit",
|
||
null,
|
||
null,
|
||
"""{"title":"确认提交?"}""",
|
||
"""[]""",
|
||
10),
|
||
CancellationToken.None);
|
||
|
||
button.Code.Should().Be("submit");
|
||
button.PermissionCode.Should().Be("form:submit");
|
||
button.Status.Should().Be(BuildFlowMetadataStatus.Draft);
|
||
}
|
||
|
||
[Fact]
|
||
public async Task ExecuteAsync_RunFunctionAction_ReturnsFunctionResultAndWritesLog()
|
||
{
|
||
await using var db = TestDbFactory.Create();
|
||
var function = await CreatePublishedFunctionAsync(db);
|
||
var button = CreateButton(function.Id);
|
||
db.BuildFlowButtons.Add(button);
|
||
await db.SaveChangesAsync();
|
||
|
||
var executor = new BuildFlowButtonActionExecutor(
|
||
db,
|
||
new CustomFunctionExecutionService(db));
|
||
|
||
var result = await executor.ExecuteAsync(
|
||
button.Id,
|
||
"""{"amount":1200}""",
|
||
CancellationToken.None);
|
||
|
||
result.Success.Should().BeTrue();
|
||
result.Results.Should().ContainSingle().Which.ResultJson.Should().Be("\"high\"");
|
||
(await db.BuildFlowActionExecutionLogs.CountAsync()).Should().Be(1);
|
||
}
|
||
|
||
[Fact]
|
||
public async Task ExecuteAsync_WhenButtonRequiresPermission_RejectsExecutionWithoutPermissionContext()
|
||
{
|
||
await using var db = TestDbFactory.Create();
|
||
var function = await CreatePublishedFunctionAsync(db);
|
||
var button = CreateButton(function.Id);
|
||
button.PermissionCode = "form:submit";
|
||
db.BuildFlowButtons.Add(button);
|
||
await db.SaveChangesAsync();
|
||
|
||
var executor = new BuildFlowButtonActionExecutor(
|
||
db,
|
||
new CustomFunctionExecutionService(db));
|
||
|
||
var act = () => executor.ExecuteAsync(
|
||
button.Id,
|
||
"""{"amount":1200}""",
|
||
CancellationToken.None);
|
||
|
||
await act.Should().ThrowAsync<UnauthorizedException>()
|
||
.WithMessage("*form:submit*");
|
||
(await db.BuildFlowActionExecutionLogs.CountAsync()).Should().Be(0);
|
||
}
|
||
|
||
[Fact]
|
||
public async Task ExecuteAsync_WhenButtonRequiresPermissionAndUserHasAccess_ExecutesAction()
|
||
{
|
||
await using var db = TestDbFactory.Create();
|
||
var function = await CreatePublishedFunctionAsync(db);
|
||
var button = CreateButton(function.Id);
|
||
button.PermissionCode = "form:submit";
|
||
db.BuildFlowButtons.Add(button);
|
||
await db.SaveChangesAsync();
|
||
|
||
var sender = new TestSender(["form:submit"]);
|
||
var executor = new BuildFlowButtonActionExecutor(
|
||
db,
|
||
new CustomFunctionExecutionService(db),
|
||
sender,
|
||
new TestCurrentUserContext("user-1"));
|
||
|
||
var result = await executor.ExecuteAsync(
|
||
button.Id,
|
||
"""{"amount":1200}""",
|
||
CancellationToken.None);
|
||
|
||
result.Success.Should().BeTrue();
|
||
result.Results.Should().ContainSingle().Which.ResultJson.Should().Be("\"high\"");
|
||
sender.RequestedUserId.Should().Be("user-1");
|
||
(await db.BuildFlowActionExecutionLogs.CountAsync()).Should().Be(1);
|
||
}
|
||
|
||
[Fact]
|
||
public async Task ExecuteAsync_WhenActionFails_DoesNotPersistActionLog()
|
||
{
|
||
await using var db = TestDbFactory.Create();
|
||
var button = CreateButton(Guid.NewGuid());
|
||
db.BuildFlowButtons.Add(button);
|
||
await db.SaveChangesAsync();
|
||
|
||
var executor = new BuildFlowButtonActionExecutor(
|
||
db,
|
||
new CustomFunctionExecutionService(db));
|
||
|
||
var act = () => executor.ExecuteAsync(button.Id, "{}", CancellationToken.None);
|
||
|
||
await act.Should().ThrowAsync<BusinessException>();
|
||
(await db.BuildFlowActionExecutionLogs.CountAsync()).Should().Be(0);
|
||
}
|
||
|
||
private static async Task<CustomFunction> CreatePublishedFunctionAsync(WorkflowDbContext db)
|
||
{
|
||
var createHandler = new CreateCustomFunctionCommandHandler(db);
|
||
var publishHandler = new PublishCustomFunctionCommandHandler(db);
|
||
var function = await createHandler.Handle(
|
||
new CreateCustomFunctionCommand(
|
||
"button-amount-level",
|
||
"按钮金额分级",
|
||
null,
|
||
"((System.Text.Json.JsonElement)Variables[\"amount\"]).GetDecimal() > 1000 ? \"high\" : \"normal\"",
|
||
null,
|
||
null,
|
||
5000),
|
||
CancellationToken.None);
|
||
await publishHandler.Handle(new PublishCustomFunctionCommand(function.Id), CancellationToken.None);
|
||
return await db.CustomFunctions.FindAsync(function.Id) ?? throw new InvalidOperationException();
|
||
}
|
||
|
||
private static BuildFlowButtonDefinition CreateButton(Guid functionId)
|
||
=> new()
|
||
{
|
||
Id = Guid.NewGuid(),
|
||
Code = "submit-and-run",
|
||
Label = "提交并计算",
|
||
Placement = BuildFlowButtonPlacement.FormToolbar,
|
||
Status = BuildFlowMetadataStatus.Published,
|
||
ActionsJson = $$"""
|
||
[
|
||
{
|
||
"type": "runFunction",
|
||
"target": "{{functionId}}",
|
||
"params": { "amount": "$input.amount" },
|
||
"runPolicy": "stopOnFailure"
|
||
}
|
||
]
|
||
""",
|
||
};
|
||
|
||
private sealed class TestCurrentUserContext(string userId) : ICurrentUserContext
|
||
{
|
||
public string UserId { get; } = userId;
|
||
|
||
public string IPAddress => "127.0.0.1";
|
||
}
|
||
|
||
private sealed class TestSender(IReadOnlyCollection<string> accessCodes) : ISender
|
||
{
|
||
public string? RequestedUserId { get; private set; }
|
||
|
||
public Task<TResponse> Send<TResponse>(IRequest<TResponse> request, CancellationToken cancellationToken = default)
|
||
{
|
||
if (request is GetAccessCodesQuery query && typeof(TResponse) == typeof(List<string>))
|
||
{
|
||
RequestedUserId = query.UserId;
|
||
return Task.FromResult((TResponse)(object)accessCodes.ToList());
|
||
}
|
||
|
||
throw new NotSupportedException($"Unexpected request {request.GetType().Name}");
|
||
}
|
||
|
||
public Task Send<TRequest>(TRequest request, CancellationToken cancellationToken = default)
|
||
where TRequest : IRequest
|
||
=> throw new NotSupportedException($"Unexpected request {typeof(TRequest).Name}");
|
||
|
||
public Task<object?> Send(object request, CancellationToken cancellationToken = default)
|
||
=> throw new NotSupportedException($"Unexpected request {request.GetType().Name}");
|
||
|
||
public IAsyncEnumerable<TResponse> CreateStream<TResponse>(
|
||
IStreamRequest<TResponse> request,
|
||
CancellationToken cancellationToken = default)
|
||
=> throw new NotSupportedException($"Unexpected stream request {request.GetType().Name}");
|
||
|
||
public IAsyncEnumerable<object?> CreateStream(object request, CancellationToken cancellationToken = default)
|
||
=> throw new NotSupportedException($"Unexpected stream request {request.GetType().Name}");
|
||
}
|
||
}
|