using BuildFlow.Shared.Domain.Exceptions;
using FluentAssertions;
using Microsoft.EntityFrameworkCore;
using Scripting.Application.Commands;
using Scripting.Application.Handlers;
using Scripting.Domain.Entities.ScriptFunction;
using Scripting.Domain.Enums;
using Scripting.Infrastructure.Persistence;
using Xunit;
namespace BuildFlow.Scripting.Tests;
///
/// 脚本 CQRS 命令处理器测试(spec §13 #6 可编辑性、生命周期守卫)。
/// 用真实 InMemory ScriptingDbContext 直接实例化 handler,覆盖:
/// Create(唯一名校验 + 默认超时)、Update(Disabled 拒绝 + Published 自增版本)、
/// Delete(Published 拒绝 + Draft 软删除)、Publish(状态 + 版本)。
///
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().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(ScriptStatusEnum.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: ScriptStatusEnum.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().WithMessage("*不能编辑*");
}
[Fact]
public async Task UpdateScript_Published_IncrementsVersion()
{
await using var db = ScriptingTestDbFactory.Create();
var script = await SeedAsync(db, name: "pub-fn", status: ScriptStatusEnum.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(ScriptStatusEnum.Published); // 保持已发布
}
[Fact]
public async Task UpdateScript_Draft_KeepsVersionAtOne()
{
await using var db = ScriptingTestDbFactory.Create();
var script = await SeedAsync(db, name: "draft-fn", status: ScriptStatusEnum.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: ScriptStatusEnum.Published);
var handler = new DeleteScriptCommandHandler(db);
var act = () => handler.Handle(new DeleteScriptCommand(script.Id), CancellationToken.None);
await act.Should().ThrowAsync().WithMessage("*不能删除*");
}
[Fact]
public async Task DeleteScript_Draft_SoftDeletes()
{
await using var db = ScriptingTestDbFactory.Create();
var script = await SeedAsync(db, name: "del-draft", status: ScriptStatusEnum.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: ScriptStatusEnum.Draft, version: 1);
var handler = new PublishScriptCommandHandler(db);
var dto = await handler.Handle(new PublishScriptCommand(script.Id), CancellationToken.None);
dto.Status.Should().Be(ScriptStatusEnum.Published);
dto.Version.Should().Be(2);
}
// ── 夹具 ───────────────────────────────────────────────────────────────
private static async Task SeedAsync(
ScriptingDbContext db,
string name,
ScriptStatusEnum status = ScriptStatusEnum.Draft,
int version = 1)
{
var script = new ScriptFunctionEntity
{
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;
}
}