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