- 从文件模块的多部分上传处理器中移除未使用的实体引用 - 从即时通讯模块的群组成员处理器中移除未使用的实体引用 - 从RAG模块的AI模型配置中移除未使用的实体引用 - 从监控模块的告警规则配置中移除未使用的实体引用 - 从平台模块的角色权限处理器中移除未使用的实体引用 - 从脚本模块的执行日志实体引用中进行清理 - 从多个基础设施配置类中移除未使用的依赖项 - 优化测试文件中的导入声明,移除不必要的引用
241 lines
9.0 KiB
C#
241 lines
9.0 KiB
C#
using FluentAssertions;
|
||
using BuildFlow.Shared.Domain.Common;
|
||
using Microsoft.EntityFrameworkCore;
|
||
using NSubstitute;
|
||
using Scripting.Application.DTOs;
|
||
using Scripting.Application.Services;
|
||
using Scripting.Domain.Entities.ScriptFunction;
|
||
using Scripting.Domain.Enums;
|
||
using Scripting.Infrastructure.Persistence;
|
||
using Workflow.Application.BuildFlow.Buttons.Commands;
|
||
using Workflow.Application.BuildFlow.Buttons.Handlers;
|
||
using Workflow.Application.BuildFlow.Buttons.Services;
|
||
using Workflow.Domain.Entities;
|
||
using Workflow.Domain.Enums;
|
||
using Xunit;
|
||
|
||
namespace BuildFlow.Workflow.Tests.BuildFlow;
|
||
|
||
/// <summary>
|
||
/// 按钮动作链测试,约束按钮配置驱动后端动作执行,而不是前端写死行为。
|
||
/// </summary>
|
||
public class BuildFlowButtonActionTests
|
||
{
|
||
[Fact]
|
||
public async Task CreateButtonDefinition_PersistsActionMetadata()
|
||
{
|
||
await using var db = TestDbFactory.Create();
|
||
var handler = new CreateBuildFlowButtonDefinitionCommandHandler(db);
|
||
|
||
var button = await handler.Handle(
|
||
new CreateBuildFlowButtonDefinitionCommand(
|
||
null,
|
||
null,
|
||
null,
|
||
null,
|
||
"submit",
|
||
"提交",
|
||
"send",
|
||
BuildFlowButtonPlacement.FormToolbar,
|
||
"form:submit",
|
||
null,
|
||
null,
|
||
"""{"title":"确认提交?"}""",
|
||
null,
|
||
"""[]""",
|
||
null,
|
||
10),
|
||
CancellationToken.None);
|
||
|
||
button.Code.Should().Be("submit");
|
||
button.PermissionCode.Should().Be("form:submit");
|
||
button.Status.Should().Be(BuildFlowMetadataStatus.Draft);
|
||
}
|
||
|
||
[Fact]
|
||
public async Task PublishButtonDefinition_ShouldMakeButtonExecutableByRuntime()
|
||
{
|
||
await using var db = TestDbFactory.Create();
|
||
var button = CreateButton();
|
||
button.Status = BuildFlowMetadataStatus.Draft;
|
||
db.BuildFlowButtons.Add(button);
|
||
await db.SaveChangesAsync();
|
||
|
||
var handler = new PublishBuildFlowButtonDefinitionCommandHandler(db);
|
||
|
||
var result = await handler.Handle(
|
||
new PublishBuildFlowButtonDefinitionCommand(button.Id),
|
||
CancellationToken.None);
|
||
|
||
result.Status.Should().Be(BuildFlowMetadataStatus.Published);
|
||
(await db.BuildFlowButtons.FindAsync(button.Id))!.Status
|
||
.Should().Be(BuildFlowMetadataStatus.Published);
|
||
}
|
||
|
||
[Fact]
|
||
public async Task DisableButtonDefinition_ShouldRemoveButtonFromRuntime()
|
||
{
|
||
await using var db = TestDbFactory.Create();
|
||
var button = CreateButton();
|
||
db.BuildFlowButtons.Add(button);
|
||
await db.SaveChangesAsync();
|
||
|
||
var handler = new DisableBuildFlowButtonDefinitionCommandHandler(db);
|
||
|
||
var result = await handler.Handle(
|
||
new DisableBuildFlowButtonDefinitionCommand(button.Id),
|
||
CancellationToken.None);
|
||
|
||
result.Status.Should().Be(BuildFlowMetadataStatus.Disabled);
|
||
(await db.BuildFlowButtons.FindAsync(button.Id))!.Status
|
||
.Should().Be(BuildFlowMetadataStatus.Disabled);
|
||
}
|
||
|
||
// ── T6 回归:执行器已改造为依赖 IScriptExecutionService + ScriptingDbContext ───
|
||
|
||
/// <summary>
|
||
/// 回归测试(spec §13 #7 按钮动作链):runFunction 动作通过 IScriptExecutionService 执行脚本。
|
||
/// 验证 T6 的 rewiring 后执行器正确委托给新的脚本执行服务,且返回成功结果。
|
||
/// </summary>
|
||
[Fact]
|
||
public async Task ExecuteAsync_RunFunctionAction_ExecutesScriptViaService()
|
||
{
|
||
// Setup: WorkflowDbContext(InMemory)+ ScriptingDbContext(InMemory)+ Mock IScriptExecutionService
|
||
await using var wfDb = TestDbFactory.Create();
|
||
await using var scriptingDb = CreateScriptingDb();
|
||
|
||
var scriptId = Guid.NewGuid();
|
||
// 脚本需存在(执行器 EnsureFunctionScopeAsync 会查 ScriptingDbContext)
|
||
scriptingDb.Scripts.Add(new ScriptFunctionEntity
|
||
{
|
||
Id = scriptId,
|
||
Name = "calc",
|
||
DisplayName = "计算",
|
||
Code = "return 1;",
|
||
Status = ScriptStatusEnum.Published,
|
||
TimeoutMs = 5000,
|
||
CreatedBy = "test",
|
||
UpdatedBy = "test",
|
||
OperatorIP = "127.0.0.1",
|
||
});
|
||
await scriptingDb.SaveChangesAsync();
|
||
|
||
var buttonId = Guid.NewGuid();
|
||
wfDb.BuildFlowButtons.Add(new BuildFlowButtonDefinition
|
||
{
|
||
Id = buttonId,
|
||
Code = "run",
|
||
Label = "运行脚本",
|
||
Placement = BuildFlowButtonPlacement.FormToolbar,
|
||
Status = BuildFlowMetadataStatus.Published,
|
||
// 无 PermissionCode → 跳过权限校验,专注验证执行链路
|
||
ActionsJson = $$"""
|
||
[
|
||
{
|
||
"type": "runFunction",
|
||
"target": "{{scriptId}}",
|
||
"params": { "amount": "$input.amount" }
|
||
}
|
||
]
|
||
""",
|
||
CreatedBy = "test",
|
||
UpdatedBy = "test",
|
||
OperatorIP = "127.0.0.1",
|
||
});
|
||
await wfDb.SaveChangesAsync();
|
||
|
||
// Mock 执行服务:返回已知成功结果,并记录被调用的 scriptId + inputJson
|
||
var execService = Substitute.For<IScriptExecutionService>();
|
||
execService.ExecuteByIdAsync(
|
||
scriptId,
|
||
Arg.Is<string>(j => j.Contains("\"amount\":100")),
|
||
Arg.Any<CancellationToken>())
|
||
.Returns(new ScriptExecutionResult(
|
||
ScriptExecutionStatusEnum.Success, "100", null, 5, null));
|
||
|
||
var executor = new BuildFlowButtonActionExecutor(wfDb, scriptingDb, execService);
|
||
|
||
var result = await executor.ExecuteAsync(buttonId, """{"amount":100}""", CancellationToken.None);
|
||
|
||
// 断言:执行服务被正确调用,按钮执行返回成功
|
||
result.Success.Should().BeTrue();
|
||
await execService.Received(1).ExecuteByIdAsync(
|
||
scriptId,
|
||
Arg.Is<string>(j => j.Contains("\"amount\":100")),
|
||
Arg.Any<CancellationToken>());
|
||
// 整体执行日志应写入 WorkflowDbContext
|
||
(await wfDb.BuildFlowActionExecutionLogs.CountAsync()).Should().Be(1);
|
||
}
|
||
|
||
/// <summary>未发布按钮不可执行(状态守卫)。</summary>
|
||
[Fact]
|
||
public async Task ExecuteAsync_DraftButton_ThrowsBusinessException()
|
||
{
|
||
await using var wfDb = TestDbFactory.Create();
|
||
await using var scriptingDb = CreateScriptingDb();
|
||
var execService = Substitute.For<IScriptExecutionService>();
|
||
|
||
var buttonId = Guid.NewGuid();
|
||
wfDb.BuildFlowButtons.Add(new BuildFlowButtonDefinition
|
||
{
|
||
Id = buttonId,
|
||
Code = "draft-btn",
|
||
Label = "草稿按钮",
|
||
Placement = BuildFlowButtonPlacement.FormToolbar,
|
||
Status = BuildFlowMetadataStatus.Draft, // 未发布
|
||
ActionsJson = "[]",
|
||
CreatedBy = "test",
|
||
UpdatedBy = "test",
|
||
OperatorIP = "127.0.0.1",
|
||
});
|
||
await wfDb.SaveChangesAsync();
|
||
|
||
var executor = new BuildFlowButtonActionExecutor(wfDb, scriptingDb, execService);
|
||
|
||
var act = () => executor.ExecuteAsync(buttonId, "{}", CancellationToken.None);
|
||
|
||
await act.Should().ThrowAsync<BusinessException>().WithMessage("*未发布*");
|
||
// 执行服务不应被调用
|
||
await execService.DidNotReceiveWithAnyArgs().ExecuteByIdAsync(default, default!, default);
|
||
}
|
||
|
||
// 注:原 7 个 BuildFlowButtonActionExecutor 执行器用例(基于已删除的 CustomFunctionExecutionService 链路)
|
||
// 已在 T6 删除。上方两个用例基于新的 IScriptExecutionService 链路重写,作为动作链回归覆盖。
|
||
|
||
private static BuildFlowButtonDefinition CreateButton()
|
||
=> new()
|
||
{
|
||
Id = Guid.NewGuid(),
|
||
Code = "submit-and-run",
|
||
Label = "提交并计算",
|
||
Placement = BuildFlowButtonPlacement.FormToolbar,
|
||
Status = BuildFlowMetadataStatus.Published,
|
||
ActionsJson = """
|
||
[
|
||
{
|
||
"type": "runFunction",
|
||
"target": "00000000-0000-0000-0000-000000000001",
|
||
"params": { "amount": "$input.amount" },
|
||
"runPolicy": "stopOnFailure"
|
||
}
|
||
]
|
||
""",
|
||
};
|
||
|
||
/// <summary>新建带审计模拟的 InMemory ScriptingDbContext(镜像 Scripting.Tests 模式)。</summary>
|
||
private static ScriptingDbContext CreateScriptingDb()
|
||
{
|
||
var options = new DbContextOptionsBuilder<ScriptingDbContext>()
|
||
.UseInMemoryDatabase($"wf-scripting-test-{Guid.NewGuid()}")
|
||
.Options;
|
||
// 传入测试用户上下文以激活 AuditInterceptor,自动填充审计字段
|
||
return new ScriptingDbContext(options, new TestCurrentUserContext());
|
||
}
|
||
|
||
private sealed class TestCurrentUserContext : ICurrentUserContext
|
||
{
|
||
public string UserId => "test-user";
|
||
public string IPAddress => "127.0.0.1";
|
||
}
|
||
}
|