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)
185 lines
7.3 KiB
C#
185 lines
7.3 KiB
C#
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;
|
||
}
|
||
}
|