fix(inbound): log swallowed scope-creation failure + test scope disposal on script throw
This commit is contained in:
@@ -251,9 +251,14 @@ public class InboundScriptExecutor
|
||||
{
|
||||
scope = _serviceProvider.CreateScope();
|
||||
}
|
||||
catch (InvalidOperationException)
|
||||
catch (InvalidOperationException ex)
|
||||
{
|
||||
// No scope factory available (provider does not support scoping).
|
||||
// No scope factory available (e.g. a non-scoping test-double provider).
|
||||
// In production this should never happen; log so a genuine container
|
||||
// misconfiguration is visible rather than silently disabling Database.
|
||||
_logger.LogWarning(ex,
|
||||
"Could not create a DI scope for method {Method}; Database will be unavailable to its script",
|
||||
method.Name);
|
||||
}
|
||||
|
||||
try
|
||||
|
||||
@@ -599,6 +599,51 @@ public class InboundScriptExecutorTests
|
||||
Assert.Contains("99", result.ResultJson!);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ScriptThrows_DisposesDiScope()
|
||||
{
|
||||
// When the script handler throws, ExecuteAsync must still dispose the
|
||||
// per-execution DI scope it created (regression guard for the finally block).
|
||||
var scopeServiceProvider = Substitute.For<IServiceProvider>();
|
||||
// GetService<IDatabaseGateway>() returns null — script never needs it.
|
||||
scopeServiceProvider.GetService(typeof(IDatabaseGateway)).Returns((object?)null);
|
||||
|
||||
var scope = Substitute.For<IServiceScope>();
|
||||
scope.ServiceProvider.Returns(scopeServiceProvider);
|
||||
|
||||
var factory = Substitute.For<IServiceScopeFactory>();
|
||||
factory.CreateScope().Returns(scope);
|
||||
|
||||
// Wire the factory into the provider so CreateScope() extension finds it.
|
||||
var provider = Substitute.For<IServiceProvider>();
|
||||
provider.GetService(typeof(IServiceScopeFactory)).Returns(factory);
|
||||
|
||||
var executor = new InboundScriptExecutor(
|
||||
NullLogger<InboundScriptExecutor>.Instance, provider);
|
||||
|
||||
// A pre-registered handler that throws; ReturnDefinition=null so validation
|
||||
// is not the failure path.
|
||||
var method = new ApiMethod("throws", "throw new Exception(\"boom\");")
|
||||
{
|
||||
Id = 1,
|
||||
TimeoutSeconds = 10,
|
||||
ReturnDefinition = null,
|
||||
};
|
||||
executor.RegisterHandler("throws", _ => throw new Exception("boom"));
|
||||
|
||||
var result = await executor.ExecuteAsync(
|
||||
method,
|
||||
new Dictionary<string, object?>(),
|
||||
_route,
|
||||
TimeSpan.FromSeconds(10));
|
||||
|
||||
// The executor must swallow the script exception and return a non-success result.
|
||||
Assert.False(result.Success);
|
||||
|
||||
// The scope must have been disposed via the finally block.
|
||||
scope.Received(1).Dispose();
|
||||
}
|
||||
|
||||
// SQLite-backed IDatabaseGateway fake (mirrors InboundDatabaseHelperTests). The
|
||||
// shared in-memory db is seeded with Machine(Code,SAPID)=('Z28061A','131453').
|
||||
private sealed class SqliteGateway : IDatabaseGateway
|
||||
|
||||
Reference in New Issue
Block a user