build-flow.api/tests/BuildFlow.Scripting.Tests/ScriptTypeRegistryTests.cs
xiangning fb40c47d5b refactor(monitoring): 重构监控模块数据模型为文档模式
- 将 AlertRule 和 ExternalSystem 实体改为使用 JsonData 存储业务字段
- 移除独立的 ExternalSystemCredential 表,凭证信息合并到 ExternalSystem.JsonData
- 添加强类型视图 Data 属性用于访问 JsonData 中的业务数据
- 更新数据库配置将字段存储改为 jsonb 类型并调整索引策略
- 重构告警评估器使用 JsonData 强类型视图读取规则数据
- 更新连接器凭证解析器从 JsonData 中获取凭证信息
- 在 appsettings.json 中添加 monitoring 模块配置
- 将 monitoring 模块集成到主应用程序中并注册服务
- 更新相关处理器使用新的数据模型结构
2026-07-17 12:19:24 +08:00

182 lines
7.1 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 FluentAssertions;
using Scripting.Application.DTOs;
using Scripting.Application.Services;
using Xunit;
namespace BuildFlow.Scripting.Tests;
/// <summary>
/// <see cref="ScriptTypeRegistry"/> 单元测试:反射扫描、关键字/Kind 过滤、分页与类型格式化。
/// 扫描依赖测试运行时加载的 BuildFlow.* 程序集;本测试项目通过 Scripting.Application
/// 传递引用了 Platform.Application,其 Query 类型应被扫描到。
/// </summary>
public class ScriptTypeRegistryTests
{
private readonly ScriptTypeRegistry _registry = new();
public ScriptTypeRegistryTests()
{
// 测试构造时扫描一次,保证后续断言基于已填充状态
_registry.Scan();
}
/// <summary>扫描应至少发现一个 BuildFlow.* IBaseRequest 实现(来自 Platform.Application)。</summary>
[Fact]
public void Scan_Populates_Known_MediatR_Types()
{
var all = _registry.GetAll();
all.Should().NotBeEmpty("Platform.Application 含大量 IRequest 实现,扫描后应非空");
// 抽样:应能找到带 Query/Command 后缀的类型(启发式分类的输入)
all.Should().Contain(m => m.Kind == "Query" || m.Kind == "Command");
}
/// <summary>关键字搜索应按 名/全名/命名空间 模糊过滤。</summary>
[Fact]
public void Search_By_Keyword_Filters()
{
var all = _registry.GetAll();
all.Should().NotBeEmpty();
// 任取一个类型的名字片段作为关键字,搜索结果必须全部命中该关键字
var keyword = PickProbe(all);
var results = _registry.Search(keyword, kind: null, pageIndex: 1, pageSize: 100).ToList();
results.Should().NotBeEmpty($"关键字 '{keyword}' 来源于已扫描类型");
foreach (var m in results)
{
var hit = m.Name.Contains(keyword, StringComparison.OrdinalIgnoreCase)
|| m.FullName.Contains(keyword, StringComparison.OrdinalIgnoreCase)
|| m.Namespace.Contains(keyword, StringComparison.OrdinalIgnoreCase);
hit.Should().BeTrue($"结果 '{m.FullName}' 应匹配关键字 '{keyword}'");
}
}
/// <summary>Kind=Query 过滤后,结果应全部是 Query;Total 计数应与分页前一致。</summary>
[Fact]
public void Search_By_Kind_Query_Filters()
{
if (_registry.GetAll().Count == 0)
{
// 没有类型可过滤时跳过(不应失败)
return;
}
var queryTotal = _registry.Total(keyword: null, kind: "Query");
var queryPage = _registry.Search(keyword: null, kind: "Query", pageIndex: 1, pageSize: 1000).ToList();
queryPage.Count.Should().Be(queryTotal, "Total 与 Search(不限页大小)计数应一致");
queryPage.Should().AllSatisfy(m => m.Kind.Should().Be("Query"));
queryTotal.Should().BeGreaterThan(0, "Platform.Application 至少含一个 Query");
}
/// <summary>分页:第二页应正确跳过第一页条数。</summary>
[Fact]
public void Search_Paginates()
{
var total = _registry.Total(keyword: null, kind: null);
if (total < 2) return;
var firstPageFirst = _registry.Search(null, null, pageIndex: 1, pageSize: 1).First();
var secondPageFirst = _registry.Search(null, null, pageIndex: 2, pageSize: 1).First();
firstPageFirst.FullName.Should().NotBe(secondPageFirst.FullName, "翻页后首页首项应不同于第二页首项");
}
/// <summary>ResponseType 字段:IRequest&lt;T&gt; 实现应能解析出 T 的友好名。</summary>
[Fact]
public void BuildMeta_Extracts_Response_Type()
{
var all = _registry.GetAll();
all.Should().Contain(m => m.ResponseType != null,
"至少一个类型实现了 IRequest<T>,应解析出非空 ResponseType");
}
/// <summary>重复扫描是幂等的:再次扫描后总数不变。</summary>
[Fact]
public void Scan_Is_Idempotent()
{
var before = _registry.GetAll().Count;
_registry.Scan();
var after = _registry.GetAll().Count;
after.Should().Be(before, "重复扫描不应改变类型集合");
}
// -------- FormatTypeName(internal static,经 InternalsVisibleTo 暴露) --------
[Theory]
[InlineData(typeof(int), "int")]
[InlineData(typeof(string), "string")]
[InlineData(typeof(bool), "bool")]
[InlineData(typeof(long), "long")]
[InlineData(typeof(decimal), "decimal")]
[InlineData(typeof(double), "double")]
[InlineData(typeof(Guid), "Guid")]
[InlineData(typeof(DateTime), "DateTime")]
[InlineData(typeof(object), "object")]
public void FormatTypeName_Primitives_Use_CSharp_Alias(Type type, string expected)
{
ScriptTypeRegistry.FormatTypeName(type).Should().Be(expected);
}
[Fact]
public void FormatTypeName_Null_Returns_Question_Mark()
{
ScriptTypeRegistry.FormatTypeName(null).Should().Be("?");
}
[Fact]
public void FormatTypeName_Nullable_Primitive_Appends_Question_Mark()
{
ScriptTypeRegistry.FormatTypeName(typeof(int?)).Should().Be("int?");
ScriptTypeRegistry.FormatTypeName(typeof(Guid?)).Should().Be("Guid?");
ScriptTypeRegistry.FormatTypeName(typeof(DateTime?)).Should().Be("DateTime?");
}
[Fact]
public void FormatTypeName_Generic_Collection_Formats_With_Angle_Brackets()
{
ScriptTypeRegistry.FormatTypeName(typeof(List<int>)).Should().Be("List<int>");
ScriptTypeRegistry.FormatTypeName(typeof(IEnumerable<string>)).Should().Be("IEnumerable<string>");
ScriptTypeRegistry.FormatTypeName(typeof(IReadOnlyList<Guid>)).Should().Be("IReadOnlyList<Guid>");
}
[Fact]
public void FormatTypeName_Dictionary_Formats_Two_Type_Arguments()
{
ScriptTypeRegistry.FormatTypeName(typeof(Dictionary<string, int>))
.Should().Be("Dictionary<string, int>");
}
[Fact]
public void FormatTypeName_Nested_Generic_Formats_Recursively()
{
ScriptTypeRegistry.FormatTypeName(typeof(List<List<string>>))
.Should().Be("List<List<string>>");
}
[Fact]
public void FormatTypeName_Array_Appends_Brackets()
{
ScriptTypeRegistry.FormatTypeName(typeof(int[])).Should().Be("int[]");
ScriptTypeRegistry.FormatTypeName(typeof(string[])).Should().Be("string[]");
ScriptTypeRegistry.FormatTypeName(typeof(List<int>[])).Should().Be("List<int>[]");
}
[Fact]
public void FormatTypeName_Complex_Type_Uses_FullName()
{
ScriptTypeRegistry.FormatTypeName(typeof(System.Text.Json.JsonElement))
.Should().Be("System.Text.Json.JsonElement");
}
// -------- 辅助 --------
/// <summary>从已扫描集合中挑一个适合做关键字探测的样本(取名字的子串,避免取到单字符)。</summary>
private static string PickProbe(IReadOnlyCollection<ScriptTypeMeta> all)
{
// 选一个名字至少 4 字符的类型,截取前 4 字符作为关键字,保证可复现命中
var pick = all.First(m => m.Name.Length >= 4);
return pick.Name[..4];
}
}