Files
scadalink-design/tests/ScadaLink.InboundAPI.Tests/InboundScriptExecutorTests.cs
Joseph Doherty 7740a3bcf9 feat: add JoeAppEngine OPC UA nodes, fix DCL auto-reconnect and quality push
- Add JoeAppEngine folder to OPC UA nodes.json (BTCS, AlarmCntsBySeverity, Scheduler/ScanTime)
- Fix DataConnectionActor: capture Self in PreStart for use from non-actor threads,
  preventing Self.Tell failure in Disconnected event handler
- Implement InstanceActor.HandleConnectionQualityChanged to mark attributes Bad on disconnect
- Fix LmxFakeProxy TagMapper to serialize arrays as JSON instead of "System.Int32[]"
- Allow DataType and DataSourceReference updates in TemplateService.UpdateAttributeAsync
- Update test_infra_opcua.md with JoeAppEngine documentation
2026-03-19 13:27:54 -04:00

136 lines
4.5 KiB
C#

using Microsoft.Extensions.Logging.Abstractions;
using NSubstitute;
using ScadaLink.Commons.Entities.InboundApi;
using ScadaLink.Commons.Interfaces.Services;
using ScadaLink.Communication;
namespace ScadaLink.InboundAPI.Tests;
/// <summary>
/// WP-3: Tests for script execution on central — timeout, handler dispatch, error handling.
/// WP-5: Safe error messages.
/// </summary>
public class InboundScriptExecutorTests
{
private readonly InboundScriptExecutor _executor;
private readonly RouteHelper _route;
public InboundScriptExecutorTests()
{
_executor = new InboundScriptExecutor(NullLogger<InboundScriptExecutor>.Instance, Substitute.For<IServiceProvider>());
var locator = Substitute.For<IInstanceLocator>();
var commService = Substitute.For<CommunicationService>(
Microsoft.Extensions.Options.Options.Create(new CommunicationOptions()),
NullLogger<CommunicationService>.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<string, object?>(),
_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<string, object?>(),
_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<string, object?>(),
_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<string, object?>(),
_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<string, object?>(),
_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<string, object?> { { "name", "ScadaLink" } };
var result = await _executor.ExecuteAsync(
method, parameters, _route, TimeSpan.FromSeconds(10));
Assert.True(result.Success);
Assert.Contains("ScadaLink", result.ResultJson!);
}
}