using FluentAssertions;
using BuildFlow.Shared.Domain.Common;
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 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 permissionChecker = new TestButtonPermissionChecker(true);
var executor = new BuildFlowButtonActionExecutor(
db,
new CustomFunctionExecutionService(db),
new TestCurrentUserContext("user-1"),
permissionChecker);
var result = await executor.ExecuteAsync(
button.Id,
"""{"amount":1200}""",
CancellationToken.None);
result.Success.Should().BeTrue();
result.Results.Should().ContainSingle().Which.ResultJson.Should().Be("\"high\"");
permissionChecker.CheckedUserId.Should().Be("user-1");
permissionChecker.CheckedPermissionCode.Should().Be("form:submit");
(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 TestButtonPermissionChecker(bool allowed) : IBuildFlowButtonPermissionChecker
{
public string? CheckedUserId { get; private set; }
public string? CheckedPermissionCode { get; private set; }
public Task HasPermissionAsync(string userId, string permissionCode, CancellationToken ct)
{
CheckedUserId = userId;
CheckedPermissionCode = permissionCode;
return Task.FromResult(allowed);
}
}
}