build-flow.api/tests/BuildFlow.Scripting.Tests/ScriptExecutionServiceParseVariablesTests.cs
xiangning 505c46ec05 feat(scripting): 添加脚本类型元数据服务与按钮动作链功能
- 扩展 CreateBuildFlowButtonDefinitionRequest 添加 BeforeActionsJson 和 AfterActionsJson 字段
- 为自定义函数请求添加 Type、HttpMethod、HttpUrl、HttpHeaders、HttpBodyTemplate 字段
- 添加 ScriptTypeRegistry 服务用于反射扫描 BuildFlow.* 程序集中的 MediatR 类型
- 实现 GetScriptAssemblyListEndpoint 和 GetScriptTypeMetaEndpoint 两个新端点
- 添加 ScriptTypeMeta 相关 DTO 和 GetScriptTypeMetaQuery 查询处理器
- 为年份和年月字符串添加单元测试避免误判为 DateTime 类型
- 在应用启动时自动扫描并缓存脚本可用类型元数据
2026-07-07 09:07:05 +08:00

184 lines
5.8 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 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");
}
}