- 添加HNSW向量索引提升检索性能 - 实现Hybrid检索模式支持向量和关键词双路召回 - 采用RRF算法融合多路召回结果 - 支持ParentChild分块模式的上下文回填 - 优化向量写入采用批量SQL操作提升性能 - 添加RAG PostgreSQL连接字符串配置 - 更新部署脚本增加PGVector集成测试 - 修正表名引用从document_chunks到b_document_chunks - 重构RagRetrievalService实现完整的检索流程 - 增加重排策略支持关键词和LLM重排 - 修改服务器部署地址配置
252 lines
8.4 KiB
C#
252 lines
8.4 KiB
C#
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;
|
|
|
|
/// <summary>
|
|
/// [SOP-PLATFORM] Menus 模块 5 个 Handler 覆盖测试。
|
|
/// 覆盖: Create / Update / Delete / GetTree / GetById。
|
|
/// 飞书矩阵: platform/Menus 子域(5 Handler 零覆盖→全补)。
|
|
/// </summary>
|
|
public class MenuHandlerTests
|
|
{
|
|
private static PlatformDbContext CreateDb([System.Runtime.CompilerServices.CallerMemberName] string name = "")
|
|
=> PlatformTestDb.CreateDbContext(name);
|
|
|
|
private static async Task<MenuEntity> 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<BusinessException>();
|
|
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<NotFoundException>();
|
|
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<NotFoundException>();
|
|
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<BusinessException>();
|
|
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<NotFoundException>();
|
|
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<BusinessException>();
|
|
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<BusinessException>();
|
|
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<NotFoundException>();
|
|
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<NotFoundException>();
|
|
ex.Which.Code.Should().Be(ErrorCode.NotFound);
|
|
}
|
|
|
|
#endregion
|
|
}
|