- 扩展 CreateBuildFlowButtonDefinitionRequest 添加 BeforeActionsJson 和 AfterActionsJson 字段 - 为自定义函数请求添加 Type、HttpMethod、HttpUrl、HttpHeaders、HttpBodyTemplate 字段 - 添加 ScriptTypeRegistry 服务用于反射扫描 BuildFlow.* 程序集中的 MediatR 类型 - 实现 GetScriptAssemblyListEndpoint 和 GetScriptTypeMetaEndpoint 两个新端点 - 添加 ScriptTypeMeta 相关 DTO 和 GetScriptTypeMetaQuery 查询处理器 - 为年份和年月字符串添加单元测试避免误判为 DateTime 类型 - 在应用启动时自动扫描并缓存脚本可用类型元数据
184 lines
5.8 KiB
C#
184 lines
5.8 KiB
C#
using System.Text.Json;
|
||
using FluentAssertions;
|
||
using Scripting.Application.Services;
|
||
using Xunit;
|
||
|
||
namespace BuildFlow.Scripting.Tests;
|
||
|
||
/// <summary>
|
||
/// ScriptExecutionService.ParseVariables 类型映射逻辑测试(纯函数,无需 DI/数据库)。
|
||
/// 验证 JSON 输入被正确转为原生 .NET 类型,让脚本能直接 (decimal)Variables["x"] 而非操作 JsonElement。
|
||
/// </summary>
|
||
public class ScriptExecutionServiceParseVariablesTests
|
||
{
|
||
// ParseVariables 是 internal static,通过 InternalsVisibleTo 暴露给本测试程序集
|
||
private static IReadOnlyDictionary<string, object?> Parse(string json)
|
||
=> ScriptExecutionService.ParseVariables(json);
|
||
|
||
[Fact]
|
||
public void Empty_Or_Whitespace_Input_Returns_Empty_Dictionary()
|
||
{
|
||
Parse("").Should().BeEmpty();
|
||
Parse(" ").Should().BeEmpty();
|
||
}
|
||
|
||
[Fact]
|
||
public void Integer_Number_Maps_To_Long()
|
||
{
|
||
var vars = Parse("{\"count\": 42, \"big\": 9007199254740993}");
|
||
|
||
vars["count"].Should().BeOfType<long>().Which.Should().Be(42L);
|
||
// 9007199254740993 超出 double 精度但 long 可表示,应保持 long
|
||
vars["big"].Should().BeOfType<long>().Which.Should().Be(9007199254740993L);
|
||
}
|
||
|
||
[Fact]
|
||
public void Decimal_Number_Maps_To_Decimal_For_Money_Friendliness()
|
||
{
|
||
var vars = Parse("{\"price\": 19.99}");
|
||
|
||
vars["price"].Should().BeOfType<decimal>().Which.Should().Be(19.99m);
|
||
}
|
||
|
||
[Fact]
|
||
public void Bool_Maps_To_Boolean()
|
||
{
|
||
var vars = Parse("{\"active\": true, \"deleted\": false}");
|
||
|
||
vars["active"].Should().BeOfType<bool>().Which.Should().BeTrue();
|
||
vars["deleted"].Should().BeOfType<bool>().Which.Should().BeFalse();
|
||
}
|
||
|
||
[Fact]
|
||
public void Null_Maps_To_Null()
|
||
{
|
||
var vars = Parse("{\"name\": null}");
|
||
|
||
vars["name"].Should().BeNull();
|
||
}
|
||
|
||
[Fact]
|
||
public void Plain_String_Maps_To_String()
|
||
{
|
||
var vars = Parse("{\"title\": \"hello world\"}");
|
||
|
||
vars["title"].Should().BeOfType<string>().Which.Should().Be("hello world");
|
||
}
|
||
|
||
[Fact]
|
||
public void Iso8601_String_Maps_To_DateTime()
|
||
{
|
||
var vars = Parse("{\"createdAt\": \"2026-07-06T10:30:00Z\"}");
|
||
|
||
vars["createdAt"].Should().BeOfType<DateTime>();
|
||
var dt = (DateTime)vars["createdAt"]!;
|
||
dt.Year.Should().Be(2026);
|
||
dt.Month.Should().Be(7);
|
||
dt.Day.Should().Be(6);
|
||
dt.Hour.Should().Be(10);
|
||
dt.Minute.Should().Be(30);
|
||
}
|
||
|
||
[Fact]
|
||
public void Guid_String_Maps_To_Guid()
|
||
{
|
||
var guidStr = "f47ac10b-58cc-4372-a567-0e02b2c3d479";
|
||
var vars = Parse("{\"id\": \"" + guidStr + "\"}");
|
||
|
||
vars["id"].Should().BeOfType<Guid>();
|
||
((Guid)vars["id"]!).Should().Be(Guid.Parse(guidStr));
|
||
}
|
||
|
||
/// <summary>ISO 8601 日期优先于 Guid(日期串不会误判为 Guid)。</summary>
|
||
[Fact]
|
||
public void Date_Parsed_Before_Guid_Check()
|
||
{
|
||
var vars = Parse("{\"ts\": \"2026-01-15\"}");
|
||
vars["ts"].Should().BeOfType<DateTime>();
|
||
}
|
||
|
||
/// <summary>
|
||
/// 年份串 "2026" 不应误判为 DateTime(会误伤客户/订单 ID 等数字样字符串)。
|
||
/// </summary>
|
||
[Fact]
|
||
public void Year_Only_String_Remains_String()
|
||
{
|
||
var vars = Parse("{\"year\": \"2026\"}");
|
||
|
||
vars["year"].Should().BeOfType<string>().Which.Should().Be("2026");
|
||
}
|
||
|
||
/// <summary>
|
||
/// 年月串 "2026-07" 不应误判为 DateTime(缺少日期分隔,非完整 ISO-8601)。
|
||
/// </summary>
|
||
[Fact]
|
||
public void Year_Month_String_Remains_String()
|
||
{
|
||
var vars = Parse("{\"period\": \"2026-07\"}");
|
||
|
||
vars["period"].Should().BeOfType<string>().Which.Should().Be("2026-07");
|
||
}
|
||
|
||
[Fact]
|
||
public void Object_Maps_To_JsonElement()
|
||
{
|
||
var vars = Parse("{\"nested\": {\"a\": 1, \"b\": 2}}");
|
||
|
||
vars["nested"].Should().BeOfType<JsonElement>();
|
||
var el = (JsonElement)vars["nested"]!;
|
||
el.ValueKind.Should().Be(JsonValueKind.Object);
|
||
}
|
||
|
||
[Fact]
|
||
public void Array_Maps_To_JsonElement()
|
||
{
|
||
var vars = Parse("{\"items\": [1, 2, 3]}");
|
||
|
||
vars["items"].Should().BeOfType<JsonElement>();
|
||
var el = (JsonElement)vars["items"]!;
|
||
el.ValueKind.Should().Be(JsonValueKind.Array);
|
||
el.GetArrayLength().Should().Be(3);
|
||
}
|
||
|
||
[Fact]
|
||
public void Mixed_Types_All_Resolve_Correctly()
|
||
{
|
||
var json = "{\"count\": 100, \"price\": 49.95, \"active\": true, \"name\": \"widget\", "
|
||
+ "\"id\": \"f47ac10b-58cc-4372-a567-0e02b2c3d479\", \"date\": \"2026-07-06T00:00:00\", "
|
||
+ "\"meta\": {\"key\": \"val\"}, \"nullable\": null}";
|
||
|
||
var vars = Parse(json);
|
||
|
||
vars["count"].Should().BeOfType<long>();
|
||
vars["price"].Should().BeOfType<decimal>();
|
||
vars["active"].Should().BeOfType<bool>();
|
||
vars["name"].Should().BeOfType<string>();
|
||
vars["id"].Should().BeOfType<Guid>();
|
||
vars["date"].Should().BeOfType<DateTime>();
|
||
vars["meta"].Should().BeOfType<JsonElement>();
|
||
vars["nullable"].Should().BeNull();
|
||
}
|
||
|
||
/// <summary>脚本端典型用法:原生类型可直接强转,无需 GetInt64() 等中间步骤。</summary>
|
||
[Fact]
|
||
public void Script_Idiom_Direct_Cast_Works()
|
||
{
|
||
var vars = Parse("{\"qty\": 5, \"rate\": 12.5}");
|
||
|
||
// 模拟脚本里写 (long)Variables["qty"] * (decimal)Variables["rate"]
|
||
var result = (long)vars["qty"]! * (decimal)vars["rate"]!;
|
||
result.Should().Be(62.5m);
|
||
}
|
||
|
||
/// <summary>Ordinal 键比较器:键名大小写敏感。</summary>
|
||
[Fact]
|
||
public void Keys_Are_Case_Sensitive_Ordinal()
|
||
{
|
||
var vars = Parse("{\"Name\": \"a\", \"name\": \"b\"}");
|
||
|
||
vars.Should().HaveCount(2);
|
||
vars["Name"].Should().Be("a");
|
||
vars["name"].Should().Be("b");
|
||
}
|
||
}
|