xiangning 3b13099378 refactor(domain): 重构领域实体结构并更新枚举命名
- 将通用 Entities 命名空间拆分为具体业务实体子目录
- 更新所有实体类型引用为具体的 Entity 后缀类型
- 将 ModelType、ModelProvider、GroupMemberRole 等枚举重命名为 Enum 后缀
- 重构 AiModel 相关配置和映射逻辑适配新的实体结构
- 更新测试代码中的实体类型引用和枚举使用
- 调整应用层处理器中的实体创建和查询逻辑
- 修改 appsettings.json 中的基础设施主机配置为动态占位符模式
2026-07-19 14:02:32 +08:00

303 lines
10 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.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.Menu;
using BuildFlow.Platform.Domain.Entities.Notification;
using BuildFlow.Platform.Domain.Entities.Permission;
using BuildFlow.Platform.Domain.Entities.RefreshToken;
using BuildFlow.Platform.Domain.Entities.Role;
using BuildFlow.Platform.Domain.Entities.RolePermission;
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,
CreatedBy = "test",
CreatedAt = DateTime.UtcNow,
UpdatedBy = "test",
UpdatedAt = DateTime.UtcNow,
OperatorIP = "127.0.0.1",
};
}