- 将AI模型相关端点、命令、处理器移动到模块内部统一管理 - 优化数据库迁移脚本,添加对不同环境表状态的兼容处理逻辑 - 为异常处理添加错误码枚举参数,统一错误处理方式 - 重构多处业务逻辑中的异常抛出方式,增强代码一致性 - 移除独立的组件清单分组和项目定义,简化前端物料面板数据结构 - 修复测试代码中实体引用路径错误问题 - 添加新的监控模块测试项目到解决方案文件中
279 lines
9.7 KiB
C#
279 lines
9.7 KiB
C#
using BuildFlow.Platform.Domain.Entities.Menu;
|
||
using BuildFlow.Platform.Application.Menus.Commands;
|
||
using BuildFlow.Platform.Application.Menus.Handlers;
|
||
using BuildFlow.Platform.Application.Menus.Queries;
|
||
using BuildFlow.Platform.Domain.Entities.Role;
|
||
using BuildFlow.Platform.Domain.Entities.User;
|
||
using BuildFlow.Platform.Domain.Entities.UserRole;
|
||
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(MenuEntity).GetProperty(nameof(MenuEntity.Id))!.PropertyType.Should().Be(typeof(long));
|
||
typeof(MenuEntity).GetProperty(nameof(MenuEntity.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 UserEntity
|
||
{
|
||
Id = userId,
|
||
Username = "admin",
|
||
Email = "admin@buildflow.local",
|
||
PasswordHash = "hash",
|
||
CreatedBy = "test",
|
||
UpdatedBy = "test",
|
||
OperatorIP = "127.0.0.1",
|
||
});
|
||
db.Roles.Add(new RoleEntity
|
||
{
|
||
Id = roleId,
|
||
Name = "Admin",
|
||
CreatedBy = "test",
|
||
UpdatedBy = "test",
|
||
OperatorIP = "127.0.0.1",
|
||
});
|
||
db.UserRoles.Add(new UserRoleEntity
|
||
{
|
||
UserId = userId,
|
||
RoleId = roleId,
|
||
CreatedBy = "test",
|
||
UpdatedBy = "test",
|
||
});
|
||
db.Menus.Add(BuildMenu(
|
||
100,
|
||
"expense.request",
|
||
null,
|
||
0,
|
||
renderType: MenuRenderTypeEnum.BuildFlowForm,
|
||
formDefinitionId: formId,
|
||
runtimeViewMode: MenuRuntimeViewModeEnum.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 UserEntity
|
||
{
|
||
Id = userId,
|
||
Username = "viewer",
|
||
Email = "viewer@buildflow.local",
|
||
PasswordHash = "hash",
|
||
CreatedBy = "test",
|
||
UpdatedBy = "test",
|
||
OperatorIP = "127.0.0.1",
|
||
});
|
||
db.Roles.Add(new RoleEntity
|
||
{
|
||
Id = roleId,
|
||
Name = "Viewer",
|
||
CreatedBy = "test",
|
||
UpdatedBy = "test",
|
||
OperatorIP = "127.0.0.1",
|
||
});
|
||
db.UserRoles.Add(new UserRoleEntity
|
||
{
|
||
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 UserEntity
|
||
{
|
||
Id = viewerId,
|
||
Username = "viewer",
|
||
Email = "viewer@buildflow.local",
|
||
PasswordHash = "hash",
|
||
CreatedBy = "test",
|
||
UpdatedBy = "test",
|
||
OperatorIP = "127.0.0.1",
|
||
},
|
||
new UserEntity
|
||
{
|
||
Id = adminId,
|
||
Username = "admin",
|
||
Email = "admin@buildflow.local",
|
||
PasswordHash = "hash",
|
||
CreatedBy = "test",
|
||
UpdatedBy = "test",
|
||
OperatorIP = "127.0.0.1",
|
||
});
|
||
db.Roles.AddRange(
|
||
new RoleEntity
|
||
{
|
||
Id = viewerRoleId,
|
||
Name = "Viewer",
|
||
CreatedBy = "test",
|
||
UpdatedBy = "test",
|
||
OperatorIP = "127.0.0.1",
|
||
},
|
||
new RoleEntity
|
||
{
|
||
Id = adminRoleId,
|
||
Name = "Admin",
|
||
CreatedBy = "test",
|
||
UpdatedBy = "test",
|
||
OperatorIP = "127.0.0.1",
|
||
});
|
||
db.UserRoles.AddRange(
|
||
new UserRoleEntity
|
||
{
|
||
UserId = viewerId,
|
||
RoleId = viewerRoleId,
|
||
CreatedBy = "test",
|
||
UpdatedBy = "test",
|
||
},
|
||
new UserRoleEntity
|
||
{
|
||
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 MenuEntity BuildMenu(
|
||
long id,
|
||
string code,
|
||
long? parentId,
|
||
int order,
|
||
MenuRenderTypeEnum renderType = MenuRenderTypeEnum.Route,
|
||
Guid? formDefinitionId = null,
|
||
MenuRuntimeViewModeEnum runtimeViewMode = MenuRuntimeViewModeEnum.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,
|
||
CreatedAt = DateTime.UtcNow,
|
||
UpdatedAt = DateTime.UtcNow,
|
||
CreatedBy = "test",
|
||
UpdatedBy = "test",
|
||
OperatorIP = "127.0.0.1",
|
||
};
|
||
}
|