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;
///
/// 菜单管理测试。约束菜单主键使用 int64,并保证拖拽排序按 long Id 落库。
///
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()
.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(o => o.UseInMemoryDatabase($"platform-menu-{Guid.NewGuid()}"));
return services.BuildServiceProvider().GetRequiredService();
}
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",
};
}