using BuildFlow.Shared.Domain.Errors;
using BuildFlow.Shared.Domain.Exceptions;
using FluentAssertions;
using Scripting.Application.Handlers;
using Scripting.Application.Queries;
using Scripting.Application.Services;
using Scripting.Domain.Entities.ScriptExecutionLog;
using Scripting.Domain.Entities.ScriptFunction;
using Scripting.Domain.Enums;
using Scripting.Infrastructure.Persistence;
using Xunit;
namespace BuildFlow.Scripting.Tests;
///
/// scripting 模块查询处理器测试(架构护栏:每个 Handler 至少 1 个测试)。
/// 用真实 InMemory ScriptingDbContext 直接实例化 handler,覆盖:
/// GetScriptDetail(成功 + 不存在→NotFound)、
/// GetScriptExecutionLogs(成功 + 状态过滤 + 空结果)、
/// GetScriptList(成功 + 状态过滤 + 关键字过滤 + 空结果)、
/// GetScriptAssemblyList / GetScriptTypeMeta(委托 ScriptTypeRegistry 的过滤/分页/去重排序)。
///
public class ScriptQueryHandlerTests
{
// ── GetScriptDetail ────────────────────────────────────────────────────
[Fact]
public async Task GetScriptDetail_Existing_ReturnsDto()
{
await using var db = ScriptingTestDbFactory.Create();
var script = await SeedScriptAsync(db, "detail-fn", ScriptStatusEnum.Published);
var handler = new GetScriptDetailQueryHandler(db);
var dto = await handler.Handle(new GetScriptDetailQuery(script.Id), CancellationToken.None);
dto.Should().NotBeNull();
dto.Id.Should().Be(script.Id);
dto.Name.Should().Be("detail-fn");
dto.Status.Should().Be(ScriptStatusEnum.Published);
}
[Fact]
public async Task GetScriptDetail_NotFound_Throws()
{
await using var db = ScriptingTestDbFactory.Create();
var handler = new GetScriptDetailQueryHandler(db);
var act = async () => await handler.Handle(
new GetScriptDetailQuery(Guid.NewGuid()), CancellationToken.None);
var ex = await act.Should().ThrowAsync();
ex.Which.Code.Should().Be(ErrorCode.NotFound);
}
// ── GetScriptExecutionLogs ─────────────────────────────────────────────
[Fact]
public async Task GetScriptExecutionLogs_Existing_ReturnsLogs()
{
await using var db = ScriptingTestDbFactory.Create();
var script = await SeedScriptAsync(db, "log-fn");
await SeedLogAsync(db, script.Id, ScriptExecutionStatusEnum.Success);
var handler = new GetScriptExecutionLogsQueryHandler(db);
var result = await handler.Handle(
new GetScriptExecutionLogsQuery(script.Id, 1, 20), CancellationToken.None);
result.Items.Should().HaveCount(1);
result.Total.Should().Be(1);
result.Items[0].ScriptId.Should().Be(script.Id);
result.Items[0].Status.Should().Be(ScriptExecutionStatusEnum.Success);
}
[Fact]
public async Task GetScriptExecutionLogs_StatusFilter_ReturnsOnlyMatching()
{
await using var db = ScriptingTestDbFactory.Create();
var script = await SeedScriptAsync(db, "log-filter-fn");
await SeedLogAsync(db, script.Id, ScriptExecutionStatusEnum.Success);
await SeedLogAsync(db, script.Id, ScriptExecutionStatusEnum.Failed);
var handler = new GetScriptExecutionLogsQueryHandler(db);
var result = await handler.Handle(
new GetScriptExecutionLogsQuery(script.Id, 1, 20, ScriptExecutionStatusEnum.Success),
CancellationToken.None);
result.Items.Should().HaveCount(1);
result.Items[0].Status.Should().Be(ScriptExecutionStatusEnum.Success);
}
[Fact]
public async Task GetScriptExecutionLogs_NoLogs_ReturnsEmpty()
{
await using var db = ScriptingTestDbFactory.Create();
var script = await SeedScriptAsync(db, "log-empty-fn");
var handler = new GetScriptExecutionLogsQueryHandler(db);
var result = await handler.Handle(
new GetScriptExecutionLogsQuery(script.Id, 1, 20), CancellationToken.None);
result.Items.Should().BeEmpty();
result.Total.Should().Be(0);
}
// ── GetScriptList ──────────────────────────────────────────────────────
[Fact]
public async Task GetScriptList_ReturnsAllSeeded()
{
await using var db = ScriptingTestDbFactory.Create();
await SeedScriptAsync(db, "list-a", ScriptStatusEnum.Published);
await SeedScriptAsync(db, "list-b", ScriptStatusEnum.Draft);
var handler = new GetScriptListQueryHandler(db);
var result = await handler.Handle(
new GetScriptListQuery(1, 20, null, null), CancellationToken.None);
result.Total.Should().Be(2);
result.Items.Should().HaveCount(2);
}
[Fact]
public async Task GetScriptList_StatusFilter_ReturnsOnlyMatching()
{
await using var db = ScriptingTestDbFactory.Create();
await SeedScriptAsync(db, "list-pub", ScriptStatusEnum.Published);
await SeedScriptAsync(db, "list-draft", ScriptStatusEnum.Draft);
var handler = new GetScriptListQueryHandler(db);
var result = await handler.Handle(
new GetScriptListQuery(1, 20, ScriptStatusEnum.Published, null), CancellationToken.None);
result.Items.Should().HaveCount(1);
result.Items[0].Status.Should().Be(ScriptStatusEnum.Published);
}
[Fact]
public async Task GetScriptList_KeywordFilter_MatchesNameOrDisplayName()
{
await using var db = ScriptingTestDbFactory.Create();
await SeedScriptAsync(db, "alpha-fn", displayName: "Alpha 显示");
await SeedScriptAsync(db, "beta-fn", displayName: "Beta 显示");
var handler = new GetScriptListQueryHandler(db);
var result = await handler.Handle(
new GetScriptListQuery(1, 20, null, "alpha"), CancellationToken.None);
result.Items.Should().HaveCount(1);
result.Items[0].Name.Should().Be("alpha-fn");
}
[Fact]
public async Task GetScriptList_NoMatch_ReturnsEmpty()
{
await using var db = ScriptingTestDbFactory.Create();
await SeedScriptAsync(db, "present-fn");
var handler = new GetScriptListQueryHandler(db);
var result = await handler.Handle(
new GetScriptListQuery(1, 20, null, "absent"), CancellationToken.None);
result.Items.Should().BeEmpty();
result.Total.Should().Be(0);
}
// ── GetScriptAssemblyList ──────────────────────────────────────────────
[Fact]
public async Task GetScriptAssemblyList_ReturnsDistinctSortedAssemblies()
{
var registry = new ScriptTypeRegistry();
registry.Scan();
var handler = new GetScriptAssemblyListQueryHandler(registry);
var result = await handler.Handle(new GetScriptAssemblyListQuery(), CancellationToken.None);
result.Should().NotBeNull();
// 去重:每条只出现一次
result.Distinct().Should().HaveCount(result.Count, "handler 应做 Distinct");
// 排序:与直接基于 registry 计算的结果一致
var expected = registry.GetAll()
.Select(m => m.Assembly)
.Distinct()
.OrderBy(a => a)
.ToList();
result.Should().ContainInOrder(expected);
}
// ── GetScriptTypeMeta ──────────────────────────────────────────────────
[Fact]
public async Task GetScriptTypeMeta_All_ReturnsPagedMeta()
{
var registry = new ScriptTypeRegistry();
registry.Scan();
var handler = new GetScriptTypeMetaQueryHandler(registry);
var page = await handler.Handle(
new GetScriptTypeMetaQuery(null, null, 1, 50), CancellationToken.None);
page.Items.Should().NotBeNull();
page.Items.Count.Should().BeLessThanOrEqualTo(50);
page.PageIndex.Should().Be(1);
page.PageSize.Should().Be(50);
page.Total.Should().BeGreaterThanOrEqualTo(0);
}
[Fact]
public async Task GetScriptTypeMeta_KindFilter_ReturnsOnlyThatKind()
{
var registry = new ScriptTypeRegistry();
registry.Scan();
var handler = new GetScriptTypeMetaQueryHandler(registry);
var expectedTotal = registry.Total(null, "Query");
if (expectedTotal == 0) return; // 无 Query 类型时跳过,避免误判
var page = await handler.Handle(
new GetScriptTypeMetaQuery(null, "Query", 1, 100), CancellationToken.None);
page.Items.Should().OnlyContain(m => m.Kind == "Query");
page.Total.Should().Be(expectedTotal);
}
// ── 夹具 ───────────────────────────────────────────────────────────────
private static async Task SeedScriptAsync(
ScriptingDbContext db,
string name,
ScriptStatusEnum status = ScriptStatusEnum.Draft,
string? displayName = null)
{
var script = new ScriptFunctionEntity
{
Id = Guid.NewGuid(),
Name = name,
DisplayName = displayName ?? name,
Code = "return 1;",
Status = status,
Version = 1,
TimeoutMs = 5000,
CreatedBy = "test",
UpdatedBy = "test",
OperatorIP = "127.0.0.1",
};
db.Scripts.Add(script);
await db.SaveChangesAsync();
return script;
}
private static async Task SeedLogAsync(
ScriptingDbContext db,
Guid functionId,
ScriptExecutionStatusEnum status,
string functionName = "fn")
{
var log = new ScriptExecutionLogEntity
{
Id = Guid.NewGuid(),
FunctionId = functionId,
FunctionName = functionName,
Status = status,
InputJson = "{}",
CreatedBy = "test",
UpdatedBy = "test",
};
db.ScriptExecutionLogs.Add(log);
await db.SaveChangesAsync();
return log;
}
}