build-flow.api/tests/BuildFlow.Scripting.Tests/ScriptExecutionServiceParseVariablesTests.cs
xiangning 624579cf78 feat(scripting): ScriptGlobals + ScriptExecutionService + 沙箱 + 编译缓存
- 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
2026-07-07 08:42:49 +08:00

162 lines
5.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 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");
}
}