build-flow.api/tests/BuildFlow.Workflow.Tests/BuildFlow/BuildFlowButtonActionTests.cs
xiangning 1aeaf7b1a6 feat(platform): 实现菜单管理功能并完善权限控制
- 将菜单实体的 ID 类型从 Guid 改为 long,并配置为自增主键
- 添加菜单创建、删除、树形结构构建等完整的 CRUD 操作支持
- 为文件上传相关端点添加 file:upload 权限验证
- 为文件操作端点添加 file:read 和 file:write 权限控制
- 为通知功能添加 notification:read 权限限制
- 实现 BuildFlow 按钮执行的权限检查机制
- 在 IM 模块中集成 Redis 未读计数器优化性能
- 更新用户主页路径从 /workspace 到 /dashboard/workspace
- 添加 SignalR Hub 授权和 WebSocket 连接安全保护
- 新增平台测试项目并完善按钮权限测试用例
2026-07-04 17:21:07 +08:00

203 lines
7.3 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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;
/// <summary>
/// 按钮动作链测试,约束按钮配置驱动后端动作执行,而不是前端写死行为。
/// </summary>
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<UnauthorizedException>()
.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<BusinessException>();
(await db.BuildFlowActionExecutionLogs.CountAsync()).Should().Be(0);
}
private static async Task<CustomFunction> 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<bool> HasPermissionAsync(string userId, string permissionCode, CancellationToken ct)
{
CheckedUserId = userId;
CheckedPermissionCode = permissionCode;
return Task.FromResult(allowed);
}
}
}