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; } }