using Microsoft.Extensions.Logging.Abstractions; using NSubstitute; using ScadaLink.Commons.Entities.InboundApi; using ScadaLink.Commons.Interfaces.Services; using ScadaLink.Communication; namespace ScadaLink.InboundAPI.Tests; /// /// WP-3: Tests for script execution on central — timeout, handler dispatch, error handling. /// WP-5: Safe error messages. /// public class InboundScriptExecutorTests { private readonly InboundScriptExecutor _executor; private readonly RouteHelper _route; public InboundScriptExecutorTests() { _executor = new InboundScriptExecutor(NullLogger.Instance, Substitute.For()); var locator = Substitute.For(); var commService = Substitute.For( Microsoft.Extensions.Options.Options.Create(new CommunicationOptions()), NullLogger.Instance); _route = new RouteHelper(locator, commService); } [Fact] public async Task RegisteredHandler_ExecutesSuccessfully() { var method = new ApiMethod("test", "return 42;") { Id = 1, TimeoutSeconds = 10 }; _executor.RegisterHandler("test", async ctx => { await Task.CompletedTask; return new { result = 42 }; }); var result = await _executor.ExecuteAsync( method, new Dictionary(), _route, TimeSpan.FromSeconds(10)); Assert.True(result.Success); Assert.NotNull(result.ResultJson); Assert.Contains("42", result.ResultJson); } [Fact] public async Task UnregisteredHandler_InvalidScript_ReturnsCompilationFailure() { // Use an invalid script that cannot be compiled by Roslyn var method = new ApiMethod("unknown", "%%% invalid C# %%%") { Id = 1, TimeoutSeconds = 10 }; var result = await _executor.ExecuteAsync( method, new Dictionary(), _route, TimeSpan.FromSeconds(10)); Assert.False(result.Success); Assert.Contains("Script compilation failed", result.ErrorMessage); } [Fact] public async Task UnregisteredHandler_ValidScript_LazyCompiles() { // Valid script that is not pre-registered triggers lazy compilation var method = new ApiMethod("lazy", "return 1;") { Id = 1, TimeoutSeconds = 10 }; var result = await _executor.ExecuteAsync( method, new Dictionary(), _route, TimeSpan.FromSeconds(10)); Assert.True(result.Success); } [Fact] public async Task HandlerThrows_ReturnsSafeErrorMessage() { var method = new ApiMethod("failing", "throw new Exception();") { Id = 1, TimeoutSeconds = 10 }; _executor.RegisterHandler("failing", _ => throw new InvalidOperationException("internal detail leak")); var result = await _executor.ExecuteAsync( method, new Dictionary(), _route, TimeSpan.FromSeconds(10)); Assert.False(result.Success); // WP-5: Safe error message — should NOT contain "internal detail leak" Assert.Equal("Internal script error", result.ErrorMessage); } [Fact] public async Task HandlerTimesOut_ReturnsTimeoutError() { var method = new ApiMethod("slow", "Thread.Sleep(60000);") { Id = 1, TimeoutSeconds = 1 }; _executor.RegisterHandler("slow", async ctx => { await Task.Delay(TimeSpan.FromSeconds(60), ctx.CancellationToken); return "never"; }); var result = await _executor.ExecuteAsync( method, new Dictionary(), _route, TimeSpan.FromMilliseconds(100)); Assert.False(result.Success); Assert.Contains("timed out", result.ErrorMessage); } [Fact] public async Task HandlerAccessesParameters() { var method = new ApiMethod("echo", "return params;") { Id = 1, TimeoutSeconds = 10 }; _executor.RegisterHandler("echo", async ctx => { await Task.CompletedTask; return ctx.Parameters["name"]; }); var parameters = new Dictionary { { "name", "ScadaLink" } }; var result = await _executor.ExecuteAsync( method, parameters, _route, TimeSpan.FromSeconds(10)); Assert.True(result.Success); Assert.Contains("ScadaLink", result.ResultJson!); } }