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.Infrastructure.Persistence; using BuildFlow.Shared.Domain.Errors; using BuildFlow.Shared.Domain.Exceptions; using FluentAssertions; using Microsoft.EntityFrameworkCore; using Xunit; namespace BuildFlow.Platform.Tests.Menus; /// /// [SOP-PLATFORM] Menus 模块 5 个 Handler 覆盖测试。 /// 覆盖: Create / Update / Delete / GetTree / GetById。 /// 飞书矩阵: platform/Menus 子域(5 Handler 零覆盖→全补)。 /// public class MenuHandlerTests { private static PlatformDbContext CreateDb([System.Runtime.CompilerServices.CallerMemberName] string name = "") => PlatformTestDb.CreateDbContext(name); private static async Task SeedMenu( PlatformDbContext db, string code, string title, string? name = null, long? parentId = null, int order = 1) { var menu = PlatformTestDb.WithAudit(new MenuEntity { Name = name ?? code.Replace(".", ""), Code = code, Title = title, Path = "/" + code.Replace(".", "/"), Order = order, ParentId = parentId, }); db.Menus.Add(menu); await db.SaveChangesAsync(); return menu; } #region CreateMenu [Fact] public async Task CreateMenu_Valid_ReturnsDto() { var db = CreateDb(); var result = await new CreateMenuCommandHandler(db).Handle(new CreateMenuCommand { Code = "sys.a", Title = "A", Path = "/sys/a", }, CancellationToken.None); result.Code.Should().Be("sys.a"); (await db.Menus.CountAsync()).Should().Be(1); } [Fact] public async Task CreateMenu_DerivesNameFromCode_WhenNameOmitted() { var db = CreateDb(); var result = await new CreateMenuCommandHandler(db).Handle(new CreateMenuCommand { Code = "system.user", Title = "用户", Path = "/system/user", }, CancellationToken.None); result.Name.Should().Be("SystemUser"); } [Fact] public async Task CreateMenu_DuplicateCode_ThrowsBusiness() { var db = CreateDb(); await SeedMenu(db, "sys.a", "A"); var act = async () => await new CreateMenuCommandHandler(db).Handle(new CreateMenuCommand { Code = "sys.a", Title = "A2", Path = "/sys/a2", }, CancellationToken.None); var ex = await act.Should().ThrowAsync(); ex.Which.Code.Should().Be(ErrorCode.AlreadyExists); } [Fact] public async Task CreateMenu_ParentNotExists_ThrowsNotFound() { var db = CreateDb(); var act = async () => await new CreateMenuCommandHandler(db).Handle(new CreateMenuCommand { Code = "sys.b", Title = "B", Path = "/sys/b", ParentId = 9999, }, CancellationToken.None); var ex = await act.Should().ThrowAsync(); ex.Which.Code.Should().Be(ErrorCode.NotFound); } #endregion #region UpdateMenu [Fact] public async Task UpdateMenu_Existing_UpdatesTitle() { var db = CreateDb(); var menu = await SeedMenu(db, "sys.a", "A"); var result = await new UpdateMenuCommandHandler(db).Handle(new UpdateMenuCommand { Id = menu.Id, Title = "新标题", }, CancellationToken.None); result.Title.Should().Be("新标题"); } [Fact] public async Task UpdateMenu_NonExistent_ThrowsNotFound() { var db = CreateDb(); var act = async () => await new UpdateMenuCommandHandler(db).Handle( new UpdateMenuCommand { Id = 9999, Title = "x" }, CancellationToken.None); var ex = await act.Should().ThrowAsync(); ex.Which.Code.Should().Be(ErrorCode.NotFound); } [Fact] public async Task UpdateMenu_ParentPointsToSelf_ThrowsCircular() { var db = CreateDb(); var menu = await SeedMenu(db, "sys.a", "A"); var act = async () => await new UpdateMenuCommandHandler(db).Handle( new UpdateMenuCommand { Id = menu.Id, ParentId = menu.Id }, CancellationToken.None); var ex = await act.Should().ThrowAsync(); ex.Which.Code.Should().Be(ErrorCode.CircularReference); } [Fact] public async Task UpdateMenu_ParentNotExists_ThrowsNotFound() { var db = CreateDb(); var menu = await SeedMenu(db, "sys.a", "A"); var act = async () => await new UpdateMenuCommandHandler(db).Handle( new UpdateMenuCommand { Id = menu.Id, ParentId = 8888 }, CancellationToken.None); var ex = await act.Should().ThrowAsync(); ex.Which.Code.Should().Be(ErrorCode.NotFound); } [Fact] public async Task UpdateMenu_DuplicateName_ThrowsBusiness() { var db = CreateDb(); await SeedMenu(db, "sys.a", "A", name: "Same"); var other = await SeedMenu(db, "sys.b", "B"); var act = async () => await new UpdateMenuCommandHandler(db).Handle( new UpdateMenuCommand { Id = other.Id, Name = "Same" }, CancellationToken.None); var ex = await act.Should().ThrowAsync(); ex.Which.Code.Should().Be(ErrorCode.AlreadyExists); } #endregion #region DeleteMenu [Fact] public async Task DeleteMenu_Leaf_Removes() { var db = CreateDb(); var menu = await SeedMenu(db, "sys.a", "A"); await new DeleteMenuCommandHandler(db).Handle(new DeleteMenuCommand(menu.Id), CancellationToken.None); (await db.Menus.CountAsync()).Should().Be(0); } [Fact] public async Task DeleteMenu_HasChildren_ThrowsBusiness() { var db = CreateDb(); var parent = await SeedMenu(db, "sys", "Sys", order: 1); await SeedMenu(db, "sys.a", "A", parentId: parent.Id, order: 2); var act = async () => await new DeleteMenuCommandHandler(db) .Handle(new DeleteMenuCommand(parent.Id), CancellationToken.None); var ex = await act.Should().ThrowAsync(); ex.Which.Code.Should().Be(ErrorCode.HasChildren); } [Fact] public async Task DeleteMenu_NonExistent_ThrowsNotFound() { var db = CreateDb(); var act = async () => await new DeleteMenuCommandHandler(db) .Handle(new DeleteMenuCommand(7777), CancellationToken.None); var ex = await act.Should().ThrowAsync(); ex.Which.Code.Should().Be(ErrorCode.NotFound); } #endregion #region GetMenuTree [Fact] public async Task GetMenuTree_BuildsHierarchy() { var db = CreateDb(); var parent = await SeedMenu(db, "sys", "Sys", order: 1); await SeedMenu(db, "sys.a", "A", parentId: parent.Id, order: 2); var tree = await new GetMenuTreeQueryHandler(db).Handle(new GetMenuTreeQuery(), CancellationToken.None); tree.Should().HaveCount(1); tree[0].MenuEntity.Code.Should().Be("sys"); tree[0].Children.Should().HaveCount(1); tree[0].Children[0].MenuEntity.Code.Should().Be("sys.a"); } [Fact] public async Task GetMenuTree_Empty_ReturnsEmpty() { var db = CreateDb(); var tree = await new GetMenuTreeQueryHandler(db).Handle(new GetMenuTreeQuery(), CancellationToken.None); tree.Should().BeEmpty(); } #endregion #region GetMenuById [Fact] public async Task GetMenuById_Existing_ReturnsDto() { var db = CreateDb(); var menu = await SeedMenu(db, "sys.a", "A"); var result = await new GetMenuByIdQueryHandler(db) .Handle(new GetMenuByIdQuery(menu.Id), CancellationToken.None); result.Code.Should().Be("sys.a"); } [Fact] public async Task GetMenuById_NonExistent_ThrowsNotFound() { var db = CreateDb(); var act = async () => await new GetMenuByIdQueryHandler(db) .Handle(new GetMenuByIdQuery(12345), CancellationToken.None); var ex = await act.Should().ThrowAsync(); ex.Which.Code.Should().Be(ErrorCode.NotFound); } #endregion }