- ScriptGlobals 注入 ISender/ICurrentUserContext/CancellationToken/ScriptLogger - ScriptLogger 收集日志写入 ScriptExecutionLog.LogLines - 变量类型升级:JsonElement→原生类型(long/double/decimal/bool/string/DateTime/Guid) - 程序集白名单(含 BuildFlow.* Application/Domain,排除 Infrastructure) - 代码黑名单扫描(防 System.Reflection/IO.File/Net.Http 等) - 编译缓存(按 code SHA256 缓存委托,上限 500) - 调用深度计数器(AsyncLocal,超 5 抛异常,防递归) - 草稿可测(TestExecuteAsync 跳过 Published 校验) Task T3 of scripting module refactor
162 lines
5.1 KiB
C#
162 lines
5.1 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>();
|
||
}
|
||
|
||
[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");
|
||
}
|
||
}
|