- 将菜单实体的 ID 类型从 Guid 改为 long,并配置为自增主键 - 添加菜单创建、删除、树形结构构建等完整的 CRUD 操作支持 - 为文件上传相关端点添加 file:upload 权限验证 - 为文件操作端点添加 file:read 和 file:write 权限控制 - 为通知功能添加 notification:read 权限限制 - 实现 BuildFlow 按钮执行的权限检查机制 - 在 IM 模块中集成 Redis 未读计数器优化性能 - 更新用户主页路径从 /workspace 到 /dashboard/workspace - 添加 SignalR Hub 授权和 WebSocket 连接安全保护 - 新增平台测试项目并完善按钮权限测试用例
203 lines
7.3 KiB
C#
203 lines
7.3 KiB
C#
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);
|
||
}
|
||
}
|
||
}
|