build-flow.api/tests/BuildFlow.Workflow.Tests/Handlers/WorkflowDefinitionHandlerTests.cs
xiangning f932781040 feat(rag): 集成 Microsoft Extensions AI 框架并重构对话代理服务
- 引入 Microsoft.Extensions.AI.Abstractions 依赖包
- 重构 ChatAgentService 实现,支持多轮对话历史
- 更新 IAIChatAgent 接口,统一使用 ChatMessage 消息格式
- 优化流式输出处理逻辑,提升性能表现

feat(im): 增强即时通讯会话功能支持置顶备注免打扰

- 扩展会话 DTO,新增 IsPinned、IsMuted、OtherUserRemark 属性
- 扩展 ConversationParticipant 实体,新增置顶和备注相关字段
- 实现会话置顶功能,支持按置顶时间和最后消息时间排序
- 添加会话备注功能,私聊场景支持自定义备注名称
- 完善免打扰功能,支持用户级别静音设置

feat(file): 实现文件预览服务支持多种鉴权方式

- 新增文件预览端点,支持流式返回文件内容
- 实现三种鉴权方式:授权头、JWT令牌、内部签名
- 集成 KKFileView 预览服务,支持 Office 文档在线预览
- 优化文件访问安全控制,避免直接暴露 S3 链接

feat(rag): 增强 RAG 会话管理支持知识库关联

- 扩展会话创建功能,支持关联知识库
- 新增会话更新功能,支持重命名和切换知识库
- 优化会话 DTO 结构,支持知识库 ID 关联
- 实现知识库检索增强对话功能

feat(workflow): 废弃旧工作流定义接口迁移到图形化存储

- 标记旧的节点边操作端点为废弃状态
- 实现工作流定义图形化存储迁移
- 统一使用 JSON 格式作为单一事实源
- 优化工作流定义查询性能表现

chore(deps): 更新项目依赖包版本解决版本冲突问题

- 添加 Microsoft.Extensions.AI.Abstractions 依赖
- 显式声明 OpenAI 2.11.0 版本解决 CS1705 冲突
- 更新相关依赖包至兼容版本

fix(host): 修复文件预览响应包装中间件配置问题

- 添加文件预览接口到响应绕过前缀列表
- 避免文件流被 JSON 包装和内存缓冲影响性能
- 优化文件下载和预览的中间件处理逻辑

chore(im): 修复 IM 设计时数据库上下文工厂路径问题

- 优化 ImDbContextDesignTimeFactory 路径查找逻辑
- 修复数据库迁移时连接字符串获取问题
- 更新实体映射命名空间引用
2026-07-03 15:58:58 +08:00

358 lines
12 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 FluentAssertions;
using Microsoft.EntityFrameworkCore;
using Workflow.Application.Features.WorkflowDefinitions.Commands;
using Workflow.Application.Features.WorkflowDefinitions.Handlers;
using Workflow.Application.Features.WorkflowDefinitions.Queries;
using Workflow.Domain.Entities;
using Workflow.Domain.Enums;
using Xunit;
namespace BuildFlow.Workflow.Tests.Handlers;
public class WorkflowDefinitionHandlerTests
{
private static WorkflowDbContext CreateDbContext()
{
var options = new DbContextOptionsBuilder<WorkflowDbContext>()
.UseInMemoryDatabase(Guid.NewGuid().ToString())
.Options;
var db = new WorkflowDbContext(options);
db.SavingChanges += AuditStub.OnSave;
return db; }
#region CreateWorkflowDefinition
[Fact]
public async Task CreateDefinition_SavesToDatabase()
{
// Arrange
await using var db = CreateDbContext();
var handler = new CreateWorkflowDefinitionCommandHandler(db);
var command = new CreateWorkflowDefinitionCommand(
Name: "Leave Approval",
Code: "leave-approval",
Description: "Standard leave approval workflow",
FormDefinitionId: null
);
// Act
var result = await handler.Handle(command, CancellationToken.None);
// Assert
result.Should().NotBeNull();
result.Name.Should().Be("Leave Approval");
result.Code.Should().Be("leave-approval");
result.Description.Should().Be("Standard leave approval workflow");
result.Status.Should().Be(DefinitionStatus.Draft);
result.Version.Should().Be(1);
var entity = await db.WorkflowDefinitions.FirstOrDefaultAsync(d => d.Code == "leave-approval");
entity.Should().NotBeNull();
entity!.Name.Should().Be("Leave Approval");
}
[Fact]
public async Task CreateDefinition_WithDuplicateCode_ThrowsBusinessException()
{
// Arrange
await using var db = CreateDbContext();
var existing = new WorkflowDefinition
{
Id = Guid.NewGuid(),
Name = "Existing",
Code = "duplicate-code",
Status = DefinitionStatus.Draft,
Version = 1
};
db.WorkflowDefinitions.Add(existing);
await db.SaveChangesAsync();
var handler = new CreateWorkflowDefinitionCommandHandler(db);
var command = new CreateWorkflowDefinitionCommand(
Name: "New Workflow",
Code: "duplicate-code",
Description: "Should fail",
FormDefinitionId: null
);
// Act
var act = () => handler.Handle(command, CancellationToken.None);
// Assert
await act.Should().ThrowAsync<BusinessException>()
.WithMessage("*duplicate-code*");
}
#endregion
#region UpdateWorkflowDefinition
[Fact]
public async Task UpdateDefinition_UpdatesFields()
{
// Arrange
await using var db = CreateDbContext();
var definitionId = Guid.NewGuid();
var existing = new WorkflowDefinition
{
Id = definitionId,
Name = "Old Name",
Code = "test-workflow",
Description = "Old description",
Status = DefinitionStatus.Draft,
Version = 1
};
db.WorkflowDefinitions.Add(existing);
await db.SaveChangesAsync();
var handler = new UpdateWorkflowDefinitionCommandHandler(db);
var command = new UpdateWorkflowDefinitionCommand(
Id: definitionId,
Name: "Updated Name",
Description: "Updated description",
DefinitionJson: "{\"nodes\":[],\"edges\":[]}",
FormDefinitionId: null
);
// Act
var result = await handler.Handle(command, CancellationToken.None);
// Assert
result.Should().NotBeNull();
result.Name.Should().Be("Updated Name");
result.Description.Should().Be("Updated description");
result.DefinitionJson.Should().Be("{\"nodes\":[],\"edges\":[]}");
var entity = await db.WorkflowDefinitions.FindAsync(definitionId);
entity.Should().NotBeNull();
entity!.Name.Should().Be("Updated Name");
}
#endregion
#region DeleteWorkflowDefinition
[Fact]
public async Task DeleteDefinition_SoftDeletes()
{
// Arrange
await using var db = CreateDbContext();
var definitionId = Guid.NewGuid();
var existing = new WorkflowDefinition
{
Id = definitionId,
Name = "To Delete",
Code = "to-delete",
Status = DefinitionStatus.Draft,
Version = 1
};
db.WorkflowDefinitions.Add(existing);
await db.SaveChangesAsync();
var handler = new DeleteWorkflowDefinitionCommandHandler(db);
var command = new DeleteWorkflowDefinitionCommand(Id: definitionId);
// Act
await handler.Handle(command, CancellationToken.None);
// Assert - soft delete means the entity still exists but IsDeleted = true
var entity = await db.WorkflowDefinitions
.IgnoreQueryFilters()
.FirstOrDefaultAsync(d => d.Id == definitionId);
entity.Should().NotBeNull();
entity!.IsDeleted.Should().BeTrue();
// Normal query should not return the deleted entity
// Note: InMemory provider does not support global query filters,
// so FindAsync still returns the entity. Verify via filtered query instead.
var activeEntity = await db.WorkflowDefinitions
.FirstOrDefaultAsync(d => d.Id == definitionId);
activeEntity?.IsDeleted.Should().BeTrue();
}
#endregion
#region PublishWorkflowDefinition
[Fact]
public async Task PublishDefinition_ChangesStatusAndIncrementsVersion()
{
// Arrange
await using var db = CreateDbContext();
var definitionId = Guid.NewGuid();
// 发布前需有合法的 X6 graph JSON(含 start + end + 连通边),否则图校验拦截
var startId = Guid.NewGuid();
var endId = Guid.NewGuid();
var edgeId = Guid.NewGuid();
var definitionJson =
"{\"nodes\":[" +
"{\"id\":\"" + startId + "\",\"shape\":\"wf-start\",\"x\":0,\"y\":0,\"data\":{\"title\":\"开始\"}}," +
"{\"id\":\"" + endId + "\",\"shape\":\"wf-end\",\"x\":0,\"y\":200,\"data\":{\"title\":\"结束\"}}" +
"],\"edges\":[" +
"{\"id\":\"" + edgeId + "\",\"shape\":\"wf-edge\"," +
"\"source\":{\"cell\":\"" + startId + "\",\"port\":\"bottom\"}," +
"\"target\":{\"cell\":\"" + endId + "\",\"port\":\"top\"}," +
"\"data\":{\"edgeType\":0}}" +
"]}";
var existing = new WorkflowDefinition
{
Id = definitionId,
Name = "Publishable Workflow",
Code = "publishable",
Status = DefinitionStatus.Draft,
Version = 1,
DefinitionJson = definitionJson
};
db.WorkflowDefinitions.Add(existing);
await db.SaveChangesAsync();
var handler = new PublishWorkflowDefinitionCommandHandler(db);
var command = new PublishWorkflowDefinitionCommand(Id: definitionId);
// Act
await handler.Handle(command, CancellationToken.None);
// Assert
var entity = await db.WorkflowDefinitions.FindAsync(definitionId);
entity.Should().NotBeNull();
entity!.Status.Should().Be(DefinitionStatus.Published);
entity.Version.Should().Be(2);
// 发布应写入不可变快照(版本隔离)
var snapshot = await db.WorkflowDefinitionSnapshots
.FirstOrDefaultAsync(s => s.DefinitionId == definitionId && s.Version == 2);
snapshot.Should().NotBeNull();
snapshot!.DefinitionJson.Should().Be(definitionJson);
}
#endregion
#region GetWorkflowDefinitionList
[Fact]
public async Task GetDefinitionList_ReturnsPaged()
{
// Arrange
await using var db = CreateDbContext();
for (int i = 1; i <= 15; i++)
{
db.WorkflowDefinitions.Add(new WorkflowDefinition
{
Id = Guid.NewGuid(),
Name = $"Workflow {i}",
Code = $"workflow-{i}",
Status = DefinitionStatus.Draft,
Version = 1
});
}
await db.SaveChangesAsync();
var handler = new GetWorkflowDefinitionListQueryHandler(db);
var query = new GetWorkflowDefinitionListQuery(
PageIndex: 1,
PageSize: 10,
Status: null
);
// Act
var result = await handler.Handle(query, CancellationToken.None);
// Assert
result.Should().NotBeNull();
result.Items.Should().HaveCount(10);
result.Total.Should().Be(15);
result.PageIndex.Should().Be(1);
result.PageSize.Should().Be(10);
}
[Fact]
public async Task GetDefinitionList_FiltersByStatus()
{
// Arrange
await using var db = CreateDbContext();
db.WorkflowDefinitions.Add(new WorkflowDefinition
{
Id = Guid.NewGuid(),
Name = "Draft Workflow",
Code = "draft-wf",
Status = DefinitionStatus.Draft,
Version = 1
});
db.WorkflowDefinitions.Add(new WorkflowDefinition
{
Id = Guid.NewGuid(),
Name = "Published Workflow",
Code = "published-wf",
Status = DefinitionStatus.Published,
Version = 2
});
await db.SaveChangesAsync();
var handler = new GetWorkflowDefinitionListQueryHandler(db);
var query = new GetWorkflowDefinitionListQuery(
PageIndex: 1,
PageSize: 10,
Status: DefinitionStatus.Published
);
// Act
var result = await handler.Handle(query, CancellationToken.None);
// Assert
result.Should().NotBeNull();
result.Items.Should().HaveCount(1);
result.Items[0].Status.Should().Be(DefinitionStatus.Published);
result.Total.Should().Be(1);
}
#endregion
#region GetWorkflowDefinitionById
[Fact]
public async Task GetDefinitionById_ReturnsJsonAsSourceOfTruth()
{
// Arrange:JSON 单一事实源下,节点/边数据在 DefinitionJson,Nodes/Edges 集合置空
await using var db = CreateDbContext();
var definitionId = Guid.NewGuid();
var startId = Guid.NewGuid();
var endId = Guid.NewGuid();
var definitionJson =
"{\"nodes\":[" +
"{\"id\":\"" + startId + "\",\"shape\":\"wf-start\",\"x\":100,\"y\":100,\"data\":{\"title\":\"Start\"}}," +
"{\"id\":\"" + endId + "\",\"shape\":\"wf-end\",\"x\":500,\"y\":100,\"data\":{\"title\":\"End\"}}" +
"],\"edges\":[" +
"{\"id\":\"" + Guid.NewGuid() + "\",\"shape\":\"wf-edge\"," +
"\"source\":{\"cell\":\"" + startId + "\",\"port\":\"bottom\"}," +
"\"target\":{\"cell\":\"" + endId + "\",\"port\":\"top\"}," +
"\"data\":{\"edgeType\":0,\"label\":\"to end\"}}" +
"]}";
var definition = new WorkflowDefinition
{
Id = definitionId,
Name = "Workflow With Graph",
Code = "graph-wf",
Status = DefinitionStatus.Published,
Version = 1,
DefinitionJson = definitionJson
};
db.WorkflowDefinitions.Add(definition);
await db.SaveChangesAsync();
var handler = new GetWorkflowDefinitionByIdQueryHandler(db);
var query = new GetWorkflowDefinitionByIdQuery(Id: definitionId);
// Act
var result = await handler.Handle(query, CancellationToken.None);
// Assert:JSON 是权威源,集合为空(P4 清理时一并移除)
result.Should().NotBeNull();
result.Id.Should().Be(definitionId);
result.DefinitionJson.Should().Be(definitionJson);
result.Nodes.Should().NotBeNull().And.BeEmpty();
result.Edges.Should().NotBeNull().And.BeEmpty();
}
#endregion
}