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();
|
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
|
try
|
||||||
|
|||||||
@@ -599,6 +599,51 @@ public class InboundScriptExecutorTests
|
|||||||
Assert.Contains("99", result.ResultJson!);
|
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
|
// SQLite-backed IDatabaseGateway fake (mirrors InboundDatabaseHelperTests). The
|
||||||
// shared in-memory db is seeded with Machine(Code,SAPID)=('Z28061A','131453').
|
// shared in-memory db is seeded with Machine(Code,SAPID)=('Z28061A','131453').
|
||||||
private sealed class SqliteGateway : IDatabaseGateway
|
private sealed class SqliteGateway : IDatabaseGateway
|
||||||
|
|||||||
Reference in New Issue
Block a user