build-flow.api/tests/BuildFlow.Workflow.Tests/BuildFlow/BuildFlowButtonActionTests.cs
xiangning 51fe387913 refactor(scripting): 按钮动作链改造指向 IScriptExecutionService
- WorkflowApplicationModule 依赖 ScriptingApplicationModule
- BuildFlowButtonActionExecutor 改用 IScriptExecutionService.ExecuteByIdAsync
- EnsureFunctionScopeAsync 改查 ScriptingDbContext.Scripts
- 结果列表类型 CustomFunctionExecutionResult → ScriptExecutionResult
  (连带更新 BuildFlowButtonActionResult DTO 的 Results 元素类型)
- BuildFlowButtonActionTests 旧 CustomFunction 链路用例暂跳过,留待 T9 重写
- 旧 CustomFunction 代码保留,T7 统一删除

Task T6 of scripting module refactor
2026-07-07 09:42:20 +08:00

282 lines
10 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 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}");
}
}