using BuildFlow.Shared.Domain.Common;
using BuildFlow.Shared.Domain.Errors;
using BuildFlow.Shared.Domain.Exceptions;
using FluentAssertions;
using MediatR;
using Microsoft.EntityFrameworkCore;
using NSubstitute;
using Scripting.Application.Services;
using Scripting.Domain.Entities.ScriptFunction;
using Scripting.Domain.Enums;
using Scripting.Infrastructure.Persistence;
using Xunit;
namespace BuildFlow.Scripting.Tests;
///
/// 沙箱语义白名单测试:验证文本正则黑名单覆盖不到的绕过路径被语义分析拦截
/// (已导入命名空间下裸写类型名、反射成员入口、精确禁用类型),
/// 并验证正常语法糖(插值字符串、await、LINQ、JSON)不被误伤。
///
public class SandboxSemanticWhitelistTests
{
private static (ScriptingDbContext db, ScriptExecutionService svc) CreateService()
{
var db = ScriptingTestDbFactory.Create();
var sender = Substitute.For();
var currentUser = Substitute.For();
currentUser.UserId.Returns("test-user");
currentUser.IPAddress.Returns("127.0.0.1");
var svc = new ScriptExecutionService(db, sender, currentUser);
return (db, svc);
}
private static ScriptFunctionEntity SeedScript(ScriptingDbContext db, string code)
{
var script = new ScriptFunctionEntity
{
Id = Guid.NewGuid(),
Name = $"sandbox-{Guid.NewGuid():N}",
DisplayName = "sandbox-test",
Code = code,
Status = ScriptStatusEnum.Published,
TimeoutMs = 5000,
CreatedBy = "test",
UpdatedBy = "test",
OperatorIP = "127.0.0.1",
};
db.Scripts.Add(script);
db.SaveChanges();
return script;
}
// ── 绕过路径:全部应被语义白名单拦截 ──────────────────────────────────
[Theory]
[InlineData("""Environment.GetEnvironmentVariable("PATH"); return 1;""", "System.Environment")]
[InlineData("var x = System.Environment.MachineName; return 1;", "System.Environment")]
[InlineData("_ = new Thread(() => { }); return 1;", "System.Threading.Thread")]
[InlineData("\"abc\".GetType(); return 1;", "GetType")]
[InlineData("var t = typeof(string); return 1;", "System.Type")]
[InlineData("var x = System.AppDomain.CurrentDomain.FriendlyName; return 1;", "System.AppDomain")]
[InlineData("var x = Activator.CreateInstance(typeof(int)); return 1;", "Activator")]
public async Task Execute_BypassAttempt_IsRejectedBySemanticWhitelist(string code, string expectedFragment)
{
var (db, svc) = CreateService();
var script = SeedScript(db, code);
await using var _ = db;
var act = () => svc.ExecuteByIdAsync(script.Id, "{}", CancellationToken.None);
var exception = (await act.Should().ThrowAsync())
.WithMessage("*禁用 API*")
.Which;
exception.Code.Should().Be(ErrorCode.Business);
exception.Message.Should().Contain(expectedFragment);
}
// ── 正常语法糖:不应误伤 ─────────────────────────────────────────────
[Fact]
public async Task Execute_InterpolatedString_Succeeds()
{
var (db, svc) = CreateService();
// 插值字符串编译期依赖 System.Runtime.CompilerServices.DefaultInterpolatedStringHandler,
// 白名单必须放行该命名空间(Unsafe 已单独精确封禁)
var script = SeedScript(db, """return $"x={Variables["a"]}"; """);
await using var _ = db;
var result = await svc.ExecuteByIdAsync(script.Id, """{"a":7}""", CancellationToken.None);
result.Status.Should().Be(ScriptExecutionStatusEnum.Success);
result.ResultJson.Should().Be("\"x=7\"");
}
[Fact]
public async Task Execute_AwaitAndLinq_Succeeds()
{
var (db, svc) = CreateService();
// await 编译期依赖 CompilerServices.TaskAwaiter;LINQ 依赖 System.Linq —— 均应在白名单内
var script = SeedScript(db,
"await Task.Delay(1, CancellationToken); var sum = new[] { 1L, 2L, 3L }.Sum(); return sum;");
await using var _ = db;
var result = await svc.ExecuteByIdAsync(script.Id, "{}", CancellationToken.None);
result.Status.Should().Be(ScriptExecutionStatusEnum.Success);
result.ResultJson.Should().Be("6");
}
[Fact]
public async Task Execute_JsonProcessing_Succeeds()
{
var (db, svc) = CreateService();
var script = SeedScript(db,
"""var doc = JsonDocument.Parse(InputJson); return doc.RootElement.GetProperty("k").GetString();""");
await using var _ = db;
var result = await svc.ExecuteByIdAsync(script.Id, """{"k":"v"}""", CancellationToken.None);
result.Status.Should().Be(ScriptExecutionStatusEnum.Success);
result.ResultJson.Should().Be("\"v\"");
}
[Fact]
public async Task Execute_ScriptDefinedLocalClass_Succeeds()
{
var (db, svc) = CreateService();
// 脚本自定义类型位于全局命名空间,必须放行
var script = SeedScript(db, "class P { public long V { get; set; } } return new P { V = 5 }.V;");
await using var _ = db;
var result = await svc.ExecuteByIdAsync(script.Id, "{}", CancellationToken.None);
result.Status.Should().Be(ScriptExecutionStatusEnum.Success);
result.ResultJson.Should().Be("5");
}
// ── 同一代码只校验一次(缓存命中后仍保持拦截语义由缓存键保证) ────────
[Fact]
public async Task Execute_ForbiddenCodeTwice_SecondAlsoRejected()
{
var (db, svc) = CreateService();
var script = SeedScript(db, "\"abc\".GetType(); return 1;");
await using var _ = db;
await FluentActions.Awaiting(() => svc.ExecuteByIdAsync(script.Id, "{}", CancellationToken.None))
.Should().ThrowAsync();
await FluentActions.Awaiting(() => svc.ExecuteByIdAsync(script.Id, "{}", CancellationToken.None))
.Should().ThrowAsync()
.WithMessage("*禁用 API*");
}
}