xiangning baaff8db63 feat(workflow): 添加自定义函数表单绑定及权限控制功能
- 新增自定义函数与表单定义绑定关系字段 FormDefinitionId
- 实现按钮调用函数时的表单作用域权限校验机制
- 实现按钮提交表单时的作用域权限检查功能
- 添加自定义函数列表按表单定义筛选查询能力
- 新增 RelationPicker 关联选择表单组件支持
- 完善菜单权限体系支持受限但可见菜单配置
- 添加文件消息大小限制验证(最大200MB)
- 优化表单聚合变量同步异常处理机制
- 增加BuildFlow管理后台入口菜单种子数据
- 补充相关单元测试和架构约束测试用例
2026-07-05 09:30:45 +08:00

295 lines
9.5 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 BuildFlow.Platform.Application.Menus.Commands;
using BuildFlow.Platform.Application.Menus.Handlers;
using BuildFlow.Platform.Application.Menus.Queries;
using BuildFlow.Platform.Domain.Entities;
using BuildFlow.Platform.Domain.Enums;
using BuildFlow.Platform.Infrastructure.Persistence;
using BuildFlow.Shared.Domain.Exceptions;
using FluentAssertions;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Xunit;
namespace BuildFlow.Platform.Tests.Menus;
/// <summary>
/// 菜单管理测试。约束菜单主键使用 int64,并保证拖拽排序按 long Id 落库。
/// </summary>
public class MenuSortTests
{
[Fact]
public void MenuIdentifier_ShouldUseInt64()
{
typeof(Menu).GetProperty(nameof(Menu.Id))!.PropertyType.Should().Be(typeof(long));
typeof(Menu).GetProperty(nameof(Menu.ParentId))!.PropertyType.Should().Be(typeof(long?));
}
[Fact]
public async Task SortMenusCommand_ShouldUpdateMenusByInt64Ids()
{
await using var db = BuildInMemoryDb();
db.Menus.AddRange(
BuildMenu(1, "root", null, 0),
BuildMenu(2, "child-a", 1, 0),
BuildMenu(3, "child-b", 1, 1));
await db.SaveChangesAsync();
var handler = new SortMenusCommandHandler(db);
var affected = await handler.Handle(
new SortMenusCommand(
[
new MenuSortItem(2, null, 1),
new MenuSortItem(3, null, 0),
]),
CancellationToken.None);
affected.Should().Be(2);
var childA = await db.Menus.SingleAsync(m => m.Id == 2);
var childB = await db.Menus.SingleAsync(m => m.Id == 3);
childA.ParentId.Should().BeNull();
childA.Order.Should().Be(1);
childB.ParentId.Should().BeNull();
childB.Order.Should().Be(0);
}
[Fact]
public async Task SortMenusCommand_ShouldRejectMovingMenuUnderDescendant()
{
await using var db = BuildInMemoryDb();
db.Menus.AddRange(
BuildMenu(1, "root", null, 0),
BuildMenu(2, "child", 1, 0),
BuildMenu(3, "grand-child", 2, 0));
await db.SaveChangesAsync();
var handler = new SortMenusCommandHandler(db);
var act = () => handler.Handle(
new SortMenusCommand([new MenuSortItem(1, 3, 0)]),
CancellationToken.None);
await act.Should().ThrowAsync<BusinessException>()
.WithMessage("不能将菜单移动到自己的子菜单下面");
}
[Fact]
public async Task GetAllMenusQuery_ShouldExposeBuildFlowFormRuntimeMetadata()
{
await using var db = BuildInMemoryDb();
var userId = Guid.NewGuid();
var roleId = Guid.NewGuid();
var formId = Guid.NewGuid();
db.Users.Add(new User
{
Id = userId,
Username = "admin",
Email = "admin@buildflow.local",
PasswordHash = "hash",
CreatedBy = "test",
UpdatedBy = "test",
OperatorIP = "127.0.0.1",
});
db.Roles.Add(new Role
{
Id = roleId,
Name = "Admin",
CreatedBy = "test",
UpdatedBy = "test",
OperatorIP = "127.0.0.1",
});
db.UserRoles.Add(new UserRole
{
UserId = userId,
RoleId = roleId,
CreatedBy = "test",
UpdatedBy = "test",
});
db.Menus.Add(BuildMenu(
100,
"expense.request",
null,
0,
renderType: MenuRenderType.BuildFlowForm,
formDefinitionId: formId,
runtimeViewMode: MenuRuntimeViewMode.List));
await db.SaveChangesAsync();
var handler = new GetAllMenusQueryHandler(db);
var routes = await handler.Handle(new GetAllMenusQuery(userId.ToString()), CancellationToken.None);
var route = routes.Should().ContainSingle().Subject;
route.Component.Should().Be("/buildflow/runtime/index");
route.Meta.RenderType.Should().Be("BuildFlowForm");
route.Meta.FormDefinitionId.Should().Be(formId);
route.Meta.RuntimeViewMode.Should().Be("List");
}
[Fact]
public async Task GetAllMenusQuery_ShouldHideRestrictedMenuWithoutVisibleForbidden()
{
await using var db = BuildInMemoryDb();
var userId = Guid.NewGuid();
var roleId = Guid.NewGuid();
db.Users.Add(new User
{
Id = userId,
Username = "viewer",
Email = "viewer@buildflow.local",
PasswordHash = "hash",
CreatedBy = "test",
UpdatedBy = "test",
OperatorIP = "127.0.0.1",
});
db.Roles.Add(new Role
{
Id = roleId,
Name = "Viewer",
CreatedBy = "test",
UpdatedBy = "test",
OperatorIP = "127.0.0.1",
});
db.UserRoles.Add(new UserRole
{
UserId = userId,
RoleId = roleId,
CreatedBy = "test",
UpdatedBy = "test",
});
db.Menus.Add(BuildMenu(200, "finance.expense", null, 0, authority: "Admin"));
await db.SaveChangesAsync();
var handler = new GetAllMenusQueryHandler(db);
var routes = await handler.Handle(new GetAllMenusQuery(userId.ToString()), CancellationToken.None);
routes.Should().BeEmpty();
}
[Fact]
public async Task GetAllMenusQuery_ShouldReturnForbiddenVisibleMenuOnlyForUnauthorizedUser()
{
await using var db = BuildInMemoryDb();
var viewerId = Guid.NewGuid();
var adminId = Guid.NewGuid();
var viewerRoleId = Guid.NewGuid();
var adminRoleId = Guid.NewGuid();
db.Users.AddRange(
new User
{
Id = viewerId,
Username = "viewer",
Email = "viewer@buildflow.local",
PasswordHash = "hash",
CreatedBy = "test",
UpdatedBy = "test",
OperatorIP = "127.0.0.1",
},
new User
{
Id = adminId,
Username = "admin",
Email = "admin@buildflow.local",
PasswordHash = "hash",
CreatedBy = "test",
UpdatedBy = "test",
OperatorIP = "127.0.0.1",
});
db.Roles.AddRange(
new Role
{
Id = viewerRoleId,
Name = "Viewer",
CreatedBy = "test",
UpdatedBy = "test",
OperatorIP = "127.0.0.1",
},
new Role
{
Id = adminRoleId,
Name = "Admin",
CreatedBy = "test",
UpdatedBy = "test",
OperatorIP = "127.0.0.1",
});
db.UserRoles.AddRange(
new UserRole
{
UserId = viewerId,
RoleId = viewerRoleId,
CreatedBy = "test",
UpdatedBy = "test",
},
new UserRole
{
UserId = adminId,
RoleId = adminRoleId,
CreatedBy = "test",
UpdatedBy = "test",
});
db.Menus.Add(BuildMenu(
200,
"finance.expense",
null,
0,
authority: "Admin",
menuVisibleWithForbidden: true));
await db.SaveChangesAsync();
var handler = new GetAllMenusQueryHandler(db);
var viewerRoutes = await handler.Handle(new GetAllMenusQuery(viewerId.ToString()), CancellationToken.None);
var adminRoutes = await handler.Handle(new GetAllMenusQuery(adminId.ToString()), CancellationToken.None);
var viewerRoute = viewerRoutes.Should().ContainSingle().Subject;
viewerRoute.Meta.Authority.Should().Equal("Admin");
viewerRoute.Meta.MenuVisibleWithForbidden.Should().BeTrue();
var adminRoute = adminRoutes.Should().ContainSingle().Subject;
adminRoute.Meta.Authority.Should().Equal("Admin");
adminRoute.Meta.MenuVisibleWithForbidden.Should().BeNull();
}
private static PlatformDbContext BuildInMemoryDb()
{
var services = new ServiceCollection();
services.AddDbContext<PlatformDbContext>(o => o.UseInMemoryDatabase($"platform-menu-{Guid.NewGuid()}"));
return services.BuildServiceProvider().GetRequiredService<PlatformDbContext>();
}
private static Menu BuildMenu(
long id,
string code,
long? parentId,
int order,
MenuRenderType renderType = MenuRenderType.Route,
Guid? formDefinitionId = null,
MenuRuntimeViewMode runtimeViewMode = MenuRuntimeViewMode.List,
string? authority = null,
bool menuVisibleWithForbidden = false)
=> new()
{
Id = id,
Code = code,
Name = code.Replace(".", "-", StringComparison.Ordinal),
Path = $"/{code}",
ParentId = parentId,
Title = code,
Order = order,
RenderType = renderType,
FormDefinitionId = formDefinitionId,
RuntimeViewMode = runtimeViewMode,
Authority = authority,
MenuVisibleWithForbidden = menuVisibleWithForbidden,
CreatedBy = "test",
CreatedAt = DateTime.UtcNow,
UpdatedBy = "test",
UpdatedAt = DateTime.UtcNow,
OperatorIP = "127.0.0.1",
};
}