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":"确认提交?"}""", """[]""", 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); } [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() .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(); (await db.BuildFlowActionExecutionLogs.CountAsync()).Should().Be(0); } private static async Task 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 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}"); } }