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 Send(object request, CancellationToken cancellationToken = default) => throw new NotSupportedException($"Unexpected request {request.GetType().Name}"); public IAsyncEnumerable CreateStream( IStreamRequest request, CancellationToken cancellationToken = default) => throw new NotSupportedException($"Unexpected stream request {request.GetType().Name}"); public IAsyncEnumerable CreateStream(object request, CancellationToken cancellationToken = default) => throw new NotSupportedException($"Unexpected stream request {request.GetType().Name}"); } }