build-flow.api/tests/BuildFlow.Scripting.Tests/ScriptCommandHandlerTests.cs
xiangning e9cc9247f2 test(scripting): 集成测试 + 按钮链回归测试
ScriptExecutionServiceTests:
- 脚本执行返回结果(简单+变量读取)
- 沙箱拦截(File/Reflection 禁用 API 抛异常)
- 草稿可测(ExecuteTestAsync 跳过 Published)
- 状态守卫(Draft/Disabled 拒绝正式执行)
- 执行日志写入(success/timeout/failed)
- 业务异常冒泡不包裹
- 编译缓存(同代码两次执行)
- Logger 输出收集
- ExecuteByNameAsync 按名执行

ScriptCommandHandlerTests:
- Create 唯一名校验 + 默认超时
- Update Disabled 拒绝 + Published 自增版本
- Delete Published 拒绝 + Draft 软删除
- Publish 状态+版本

BuildFlowButtonActionTests(回归):
- ExecuteAsync_RunFunctionAction 通过 IScriptExecutionService 执行脚本
- ExecuteAsync_DraftButton 状态守卫

Task T9 of scripting module refactor (final)
2026-07-07 11:12:28 +08:00

185 lines
7.3 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.Shared.Domain.Exceptions;
using FluentAssertions;
using Microsoft.EntityFrameworkCore;
using Scripting.Application.Commands;
using Scripting.Application.Handlers;
using Scripting.Domain.Entities;
using Scripting.Domain.Enums;
using Scripting.Infrastructure.Persistence;
using Xunit;
namespace BuildFlow.Scripting.Tests;
/// <summary>
/// 脚本 CQRS 命令处理器测试(spec §13 #6 可编辑性、生命周期守卫)。
/// 用真实 InMemory ScriptingDbContext 直接实例化 handler,覆盖:
/// Create(唯一名校验 + 默认超时)、Update(Disabled 拒绝 + Published 自增版本)、
/// Delete(Published 拒绝 + Draft 软删除)、Publish(状态 + 版本)。
/// </summary>
public class ScriptCommandHandlerTests
{
// ── Create ─────────────────────────────────────────────────────────────
[Fact]
public async Task CreateScript_DuplicateName_ThrowsBusinessException()
{
await using var db = ScriptingTestDbFactory.Create();
await SeedAsync(db, name: "dup-fn");
var handler = new CreateScriptCommandHandler(db);
var act = () => handler.Handle(
new CreateScriptCommand("dup-fn", "显示名", null, "return 1;", null, null, 0),
CancellationToken.None);
await act.Should().ThrowAsync<BusinessException>().WithMessage("*已存在*");
}
[Fact]
public async Task CreateScript_DefaultsTimeoutTo5000()
{
await using var db = ScriptingTestDbFactory.Create();
var handler = new CreateScriptCommandHandler(db);
var dto = await handler.Handle(
new CreateScriptCommand("default-timeout", "默认超时", null, "return 1;", null, null, 0),
CancellationToken.None);
dto.TimeoutMs.Should().Be(5000);
dto.Status.Should().Be(ScriptStatus.Draft);
dto.Version.Should().Be(1);
var entity = await db.Scripts.FindAsync(dto.Id);
entity.Should().NotBeNull();
entity!.TimeoutMs.Should().Be(5000);
}
[Fact]
public async Task CreateScript_NewName_PersistsEntity()
{
await using var db = ScriptingTestDbFactory.Create();
var handler = new CreateScriptCommandHandler(db);
var dto = await handler.Handle(
new CreateScriptCommand("unique-fn", "唯一函数", "desc", "return 42;", null, null, 3000),
CancellationToken.None);
dto.Name.Should().Be("unique-fn");
dto.Code.Should().Be("return 42;");
dto.TimeoutMs.Should().Be(3000);
}
// ── Update ─────────────────────────────────────────────────────────────
[Fact]
public async Task UpdateScript_DisabledScript_Throws()
{
await using var db = ScriptingTestDbFactory.Create();
var script = await SeedAsync(db, name: "disabled-fn", status: ScriptStatus.Disabled);
var handler = new UpdateScriptCommandHandler(db);
var act = () => handler.Handle(
new UpdateScriptCommand(script.Id, "新名", null, "return 2;", null, null, 5000),
CancellationToken.None);
await act.Should().ThrowAsync<BusinessException>().WithMessage("*不能编辑*");
}
[Fact]
public async Task UpdateScript_Published_IncrementsVersion()
{
await using var db = ScriptingTestDbFactory.Create();
var script = await SeedAsync(db, name: "pub-fn", status: ScriptStatus.Published, version: 3);
var handler = new UpdateScriptCommandHandler(db);
var dto = await handler.Handle(
new UpdateScriptCommand(script.Id, "改后", null, "return 99;", null, null, 5000),
CancellationToken.None);
dto.Version.Should().Be(4);
dto.DisplayName.Should().Be("改后");
dto.Code.Should().Be("return 99;");
dto.Status.Should().Be(ScriptStatus.Published); // 保持已发布
}
[Fact]
public async Task UpdateScript_Draft_KeepsVersionAtOne()
{
await using var db = ScriptingTestDbFactory.Create();
var script = await SeedAsync(db, name: "draft-fn", status: ScriptStatus.Draft);
var handler = new UpdateScriptCommandHandler(db);
var dto = await handler.Handle(
new UpdateScriptCommand(script.Id, "草稿改", null, "return 1;", null, null, 5000),
CancellationToken.None);
dto.Version.Should().Be(1); // 草稿编辑不递增版本
}
// ── Delete ─────────────────────────────────────────────────────────────
[Fact]
public async Task DeleteScript_Published_Throws()
{
await using var db = ScriptingTestDbFactory.Create();
var script = await SeedAsync(db, name: "del-pub", status: ScriptStatus.Published);
var handler = new DeleteScriptCommandHandler(db);
var act = () => handler.Handle(new DeleteScriptCommand(script.Id), CancellationToken.None);
await act.Should().ThrowAsync<BusinessException>().WithMessage("*不能删除*");
}
[Fact]
public async Task DeleteScript_Draft_SoftDeletes()
{
await using var db = ScriptingTestDbFactory.Create();
var script = await SeedAsync(db, name: "del-draft", status: ScriptStatus.Draft);
var handler = new DeleteScriptCommandHandler(db);
await handler.Handle(new DeleteScriptCommand(script.Id), CancellationToken.None);
var entity = await db.Scripts.IgnoreQueryFilters().FirstAsync(x => x.Id == script.Id);
entity.IsDeleted.Should().BeTrue();
}
// ── Publish ────────────────────────────────────────────────────────────
[Fact]
public async Task PublishScript_SetsPublishedAndIncrementsVersion()
{
await using var db = ScriptingTestDbFactory.Create();
var script = await SeedAsync(db, name: "to-publish", status: ScriptStatus.Draft, version: 1);
var handler = new PublishScriptCommandHandler(db);
var dto = await handler.Handle(new PublishScriptCommand(script.Id), CancellationToken.None);
dto.Status.Should().Be(ScriptStatus.Published);
dto.Version.Should().Be(2);
}
// ── 夹具 ───────────────────────────────────────────────────────────────
private static async Task<ScriptFunction> SeedAsync(
ScriptingDbContext db,
string name,
ScriptStatus status = ScriptStatus.Draft,
int version = 1)
{
var script = new ScriptFunction
{
Id = Guid.NewGuid(),
Name = name,
DisplayName = name,
Code = "return 1;",
Status = status,
Version = version,
TimeoutMs = 5000,
CreatedBy = "test",
UpdatedBy = "test",
OperatorIP = "127.0.0.1",
};
db.Scripts.Add(script);
await db.SaveChangesAsync();
return script;
}
}