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;
///
/// 按钮动作链测试,约束按钮配置驱动后端动作执行,而不是前端写死行为。
///
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 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 accessCodes) : ISender
{
public string? RequestedUserId { get; private set; }
public Task Send(IRequest request, CancellationToken cancellationToken = default)
{
if (request is GetAccessCodesQuery query && typeof(TResponse) == typeof(List))
{
RequestedUserId = query.UserId;
return Task.FromResult((TResponse)(object)accessCodes.ToList());
}
throw new NotSupportedException($"Unexpected request {request.GetType().Name}");
}
public Task Send(TRequest request, CancellationToken cancellationToken = default)
where TRequest : IRequest
=> throw new NotSupportedException($"Unexpected request {typeof(TRequest).Name}");
public Task