fix(inbound-api): resolve InboundAPI-014..017 — return-value validation, reflection-gateway hardening, deadline-bound routed calls, RouteHelper test coverage
This commit is contained in:
@@ -3,7 +3,6 @@ using Microsoft.Extensions.Logging.Abstractions;
|
||||
using NSubstitute;
|
||||
using ScadaLink.Commons.Entities.InboundApi;
|
||||
using ScadaLink.Commons.Interfaces.Services;
|
||||
using ScadaLink.Communication;
|
||||
|
||||
namespace ScadaLink.InboundAPI.Tests;
|
||||
|
||||
@@ -20,10 +19,8 @@ public class 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);
|
||||
var router = Substitute.For<IInstanceRouter>();
|
||||
_route = new RouteHelper(locator, router);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -364,6 +361,72 @@ public class InboundScriptExecutorTests
|
||||
Assert.True(_executor.CompileAndRegister(good));
|
||||
}
|
||||
|
||||
// --- InboundAPI-014: the script return value is validated against ReturnDefinition ---
|
||||
|
||||
[Fact]
|
||||
public async Task ReturnValue_MatchingReturnDefinition_Succeeds()
|
||||
{
|
||||
var method = new ApiMethod("shaped", "return x;")
|
||||
{
|
||||
Id = 1,
|
||||
TimeoutSeconds = 10,
|
||||
ReturnDefinition = """[{"name":"siteName","type":"String"},{"name":"total","type":"Integer"}]""",
|
||||
};
|
||||
_executor.RegisterHandler("shaped", async ctx =>
|
||||
{
|
||||
await Task.CompletedTask;
|
||||
return new { siteName = "Site Alpha", total = 14250 };
|
||||
});
|
||||
|
||||
var result = await _executor.ExecuteAsync(
|
||||
method, new Dictionary<string, object?>(), _route, TimeSpan.FromSeconds(10));
|
||||
|
||||
Assert.True(result.Success, result.ErrorMessage);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ReturnValue_NotMatchingReturnDefinition_ReturnsFailureNotMalformed200()
|
||||
{
|
||||
// The script returns a structure inconsistent with the declared return
|
||||
// definition (missing 'total'). It must surface as a failure, not a 200.
|
||||
var method = new ApiMethod("misshaped", "return x;")
|
||||
{
|
||||
Id = 1,
|
||||
TimeoutSeconds = 10,
|
||||
ReturnDefinition = """[{"name":"siteName","type":"String"},{"name":"total","type":"Integer"}]""",
|
||||
};
|
||||
_executor.RegisterHandler("misshaped", async ctx =>
|
||||
{
|
||||
await Task.CompletedTask;
|
||||
return new { siteName = "Site Alpha" }; // 'total' missing
|
||||
});
|
||||
|
||||
var result = await _executor.ExecuteAsync(
|
||||
method, new Dictionary<string, object?>(), _route, TimeSpan.FromSeconds(10));
|
||||
|
||||
Assert.False(result.Success);
|
||||
Assert.Null(result.ResultJson);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ReturnValue_NoReturnDefinition_IsUnconstrained()
|
||||
{
|
||||
// A method with no ReturnDefinition keeps the prior behaviour — the return
|
||||
// value is serialized as-is.
|
||||
var method = new ApiMethod("free", "return x;") { Id = 1, TimeoutSeconds = 10 };
|
||||
_executor.RegisterHandler("free", async ctx =>
|
||||
{
|
||||
await Task.CompletedTask;
|
||||
return new { whatever = 1 };
|
||||
});
|
||||
|
||||
var result = await _executor.ExecuteAsync(
|
||||
method, new Dictionary<string, object?>(), _route, TimeSpan.FromSeconds(10));
|
||||
|
||||
Assert.True(result.Success, result.ErrorMessage);
|
||||
Assert.Contains("whatever", result.ResultJson!);
|
||||
}
|
||||
|
||||
private sealed class CompileLogCounter
|
||||
{
|
||||
public int CompilationFailures;
|
||||
|
||||
Reference in New Issue
Block a user