- WorkflowApplicationModule 依赖 ScriptingApplicationModule - BuildFlowButtonActionExecutor 改用 IScriptExecutionService.ExecuteByIdAsync - EnsureFunctionScopeAsync 改查 ScriptingDbContext.Scripts - 结果列表类型 CustomFunctionExecutionResult → ScriptExecutionResult (连带更新 BuildFlowButtonActionResult DTO 的 Results 元素类型) - BuildFlowButtonActionTests 旧 CustomFunction 链路用例暂跳过,留待 T9 重写 - 旧 CustomFunction 代码保留,T7 统一删除 Task T6 of scripting module refactor
282 lines
10 KiB
C#
282 lines
10 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":"确认提交?"}""",
|
||
null,
|
||
"""[]""",
|
||
null,
|
||
10),
|
||
CancellationToken.None);
|
||
|
||
button.Code.Should().Be("submit");
|
||
button.PermissionCode.Should().Be("form:submit");
|
||
button.Status.Should().Be(BuildFlowMetadataStatus.Draft);
|
||
}
|
||
|
||
[Fact]
|
||
public async Task PublishButtonDefinition_ShouldMakeButtonExecutableByRuntime()
|
||
{
|
||
await using var db = TestDbFactory.Create();
|
||
var button = CreateButton(Guid.NewGuid());
|
||
button.Status = BuildFlowMetadataStatus.Draft;
|
||
db.BuildFlowButtons.Add(button);
|
||
await db.SaveChangesAsync();
|
||
|
||
var handler = new PublishBuildFlowButtonDefinitionCommandHandler(db);
|
||
|
||
var result = await handler.Handle(
|
||
new PublishBuildFlowButtonDefinitionCommand(button.Id),
|
||
CancellationToken.None);
|
||
|
||
result.Status.Should().Be(BuildFlowMetadataStatus.Published);
|
||
(await db.BuildFlowButtons.FindAsync(button.Id))!.Status
|
||
.Should().Be(BuildFlowMetadataStatus.Published);
|
||
}
|
||
|
||
[Fact]
|
||
public async Task DisableButtonDefinition_ShouldRemoveButtonFromRuntime()
|
||
{
|
||
await using var db = TestDbFactory.Create();
|
||
var button = CreateButton(Guid.NewGuid());
|
||
db.BuildFlowButtons.Add(button);
|
||
await db.SaveChangesAsync();
|
||
|
||
var handler = new DisableBuildFlowButtonDefinitionCommandHandler(db);
|
||
|
||
var result = await handler.Handle(
|
||
new DisableBuildFlowButtonDefinitionCommand(button.Id),
|
||
CancellationToken.None);
|
||
|
||
result.Status.Should().Be(BuildFlowMetadataStatus.Disabled);
|
||
(await db.BuildFlowButtons.FindAsync(button.Id))!.Status
|
||
.Should().Be(BuildFlowMetadataStatus.Disabled);
|
||
}
|
||
|
||
// T6: BuildFlowButtonActionExecutor 已改造为依赖 IScriptExecutionService + ScriptingDbContext,
|
||
// 本组用例仍基于旧 CustomFunctionExecutionService/CustomFunctionCommandHandler 链路构造,
|
||
// 待 T9 重写为新的脚本执行路径。
|
||
[Fact(Skip = "T9 will rewrite to use IScriptExecutionService")]
|
||
public async Task ExecuteAsync_RunFunctionAction_ReturnsFunctionResultAndWritesLog()
|
||
{
|
||
// T9 重写为 IScriptExecutionService 路径。
|
||
await Task.CompletedTask;
|
||
}
|
||
|
||
[Fact(Skip = "T9 will rewrite to use IScriptExecutionService")]
|
||
public async Task ExecuteAsync_SubmitFormThenRunFunction_PersistsFormDataAndContinuesActionChain()
|
||
{
|
||
// T9 重写为 IScriptExecutionService 路径。
|
||
await Task.CompletedTask;
|
||
}
|
||
|
||
[Fact(Skip = "T9 will rewrite to use IScriptExecutionService")]
|
||
public async Task ExecuteAsync_WhenButtonRequiresPermission_RejectsExecutionWithoutPermissionContext()
|
||
{
|
||
// T9 重写为 IScriptExecutionService 路径。
|
||
await Task.CompletedTask;
|
||
}
|
||
|
||
[Fact(Skip = "T9 will rewrite to use IScriptExecutionService")]
|
||
public async Task ExecuteAsync_WhenButtonRequiresPermissionAndUserHasAccess_ExecutesAction()
|
||
{
|
||
// T9 重写为 IScriptExecutionService 路径。
|
||
await Task.CompletedTask;
|
||
}
|
||
|
||
[Fact(Skip = "T9 will rewrite to use IScriptExecutionService")]
|
||
public async Task ExecuteAsync_WhenActionFails_DoesNotPersistActionLog()
|
||
{
|
||
// T9 重写为 IScriptExecutionService 路径。
|
||
await Task.CompletedTask;
|
||
}
|
||
|
||
[Fact(Skip = "T9 will rewrite to use IScriptExecutionService")]
|
||
public async Task ExecuteAsync_ShouldRejectFunctionBoundToAnotherForm()
|
||
{
|
||
// T9 重写为 IScriptExecutionService 路径。
|
||
await Task.CompletedTask;
|
||
}
|
||
|
||
[Fact(Skip = "T9 will rewrite to use IScriptExecutionService")]
|
||
public async Task ExecuteAsync_ShouldRejectSubmitFormForAnotherForm()
|
||
{
|
||
// T9 重写为 IScriptExecutionService 路径。
|
||
await Task.CompletedTask;
|
||
}
|
||
|
||
private static async Task<CustomFunction> CreatePublishedFunctionAsync(
|
||
WorkflowDbContext db,
|
||
Guid? formDefinitionId = null)
|
||
{
|
||
var createHandler = new CreateCustomFunctionCommandHandler(db);
|
||
var publishHandler = new PublishCustomFunctionCommandHandler(db);
|
||
var function = await createHandler.Handle(
|
||
new CreateCustomFunctionCommand(
|
||
"button-amount-level",
|
||
"按钮金额分级",
|
||
null,
|
||
CustomFunctionType.Script,
|
||
"((System.Text.Json.JsonElement)Variables[\"amount\"]).GetDecimal() > 1000 ? \"high\" : \"normal\"",
|
||
null,
|
||
null,
|
||
null,
|
||
null,
|
||
null,
|
||
null,
|
||
5000,
|
||
formDefinitionId),
|
||
CancellationToken.None);
|
||
await publishHandler.Handle(new PublishCustomFunctionCommand(function.Id), CancellationToken.None);
|
||
return await db.CustomFunctions.FindAsync(function.Id) ?? throw new InvalidOperationException();
|
||
}
|
||
|
||
private static FormDefinition CreatePublishedFormDefinition(Guid formDefinitionId)
|
||
=> new()
|
||
{
|
||
Id = formDefinitionId,
|
||
Name = "按钮提交表单",
|
||
Code = $"button-submit-{formDefinitionId:N}",
|
||
Version = 1,
|
||
Status = FormStatus.Published,
|
||
SchemaJson = """
|
||
{
|
||
"type": "object",
|
||
"properties": {
|
||
"name": {
|
||
"type": "string",
|
||
"title": "姓名",
|
||
"required": true,
|
||
"x-decorator": "FormItem",
|
||
"x-component": "Input"
|
||
},
|
||
"amount": {
|
||
"type": "number",
|
||
"title": "金额",
|
||
"x-decorator": "FormItem",
|
||
"x-component": "InputNumber"
|
||
}
|
||
}
|
||
}
|
||
""",
|
||
};
|
||
|
||
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 static BuildFlowButtonDefinition CreateSubmitAndRunButton(Guid functionId)
|
||
=> new()
|
||
{
|
||
Id = Guid.NewGuid(),
|
||
Code = "submit-form-and-run",
|
||
Label = "提交表单并计算",
|
||
Placement = BuildFlowButtonPlacement.FormToolbar,
|
||
Status = BuildFlowMetadataStatus.Published,
|
||
ActionsJson = $$"""
|
||
[
|
||
{
|
||
"type": "submitForm"
|
||
},
|
||
{
|
||
"type": "runFunction",
|
||
"target": "{{functionId}}",
|
||
"params": { "amount": "$input.values.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}");
|
||
}
|
||
}
|