fix(inbound): authorize+secure Database helper, async/deadline-bound DB, wait-timeout-bound WaitForAttribute
Resolves InboundAPI-026/027/028/029 (+ newly-surfaced -030). - 026: authorize the scoped Database helper in the design doc; SQL-injection protection is parameter binding (values never concatenated); allow writes via ExecuteAsync; drop the false 'read-only' claim. Named connections only. - 027: async ADO.NET end-to-end (no .GetAwaiter().GetResult()); honour the method deadline token on ExecuteScalarAsync/ExecuteReaderAsync/ExecuteNonQueryAsync + a CommandTimeout backstop derived from the method timeout. - 028: negative-path tests (null-gateway, deadline cancellation, parameterization) + e2e Database + WaitForAttribute cases through the real endpoint. - 029: WaitForAttribute is bounded by its WAIT timeout (per-wait CTS + client-abort + explicit token), NOT the method deadline (spec §6) — a long wait may outlive the method timeout; WithRequestAborted threads the raw client-abort token separately. - 030: Central UI compile-surface mirrors (InboundScriptHost / SandboxInboundScriptHost) gained the Database member (drifted since the runtime helper was added) so the authorized async API type-checks at the design-time gate.
This commit is contained in:
@@ -214,10 +214,15 @@ public class RouteHelperTests
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task WaitForAttribute_WithNoExplicitToken_InheritsMethodDeadlineToken()
|
||||
public async Task WaitForAttribute_IsNotBoundByMethodDeadline_WaitTimeoutGoverns()
|
||||
{
|
||||
// InboundAPI-029 (spec §6): unlike Call/GetAttributes/SetAttributes, the wait is
|
||||
// bounded by its OWN timeout, not the method deadline. Even with the method
|
||||
// deadline ALREADY cancelled, the wait still proceeds and returns the site's
|
||||
// wait-timeout result — the deadline does not cut it short.
|
||||
SiteResolves("inst-1", "SiteA");
|
||||
using var deadline = new CancellationTokenSource();
|
||||
await deadline.CancelAsync(); // method deadline already elapsed
|
||||
CancellationToken seen = default;
|
||||
_router.RouteToWaitForAttributeAsync("SiteA", Arg.Any<RouteToWaitForAttributeRequest>(), Arg.Do<CancellationToken>(t => seen = t))
|
||||
.Returns(ci => new RouteToWaitForAttributeResponse(
|
||||
@@ -226,9 +231,52 @@ public class RouteHelperTests
|
||||
Success: true, ErrorMessage: null, DateTimeOffset.UtcNow));
|
||||
|
||||
var bound = CreateHelper().WithDeadline(deadline.Token);
|
||||
var matched = await bound.To("inst-1").WaitForAttribute("Flag", true, TimeSpan.FromSeconds(30));
|
||||
|
||||
Assert.False(matched); // the site's wait-timeout result, not a cancellation
|
||||
Assert.False(seen.IsCancellationRequested); // the wait token ignores the cancelled method deadline
|
||||
Assert.NotEqual(deadline.Token, seen); // it is a per-wait token, not the method deadline
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task WaitForAttribute_ExplicitToken_IsHonoured()
|
||||
{
|
||||
// An explicit caller token still applies (a tighter bound the script chose).
|
||||
SiteResolves("inst-1", "SiteA");
|
||||
using var explicitCts = new CancellationTokenSource();
|
||||
await explicitCts.CancelAsync();
|
||||
CancellationToken seen = default;
|
||||
_router.RouteToWaitForAttributeAsync("SiteA", Arg.Any<RouteToWaitForAttributeRequest>(), Arg.Do<CancellationToken>(t => seen = t))
|
||||
.Returns(ci => new RouteToWaitForAttributeResponse(
|
||||
((RouteToWaitForAttributeRequest)ci[1]).CorrelationId,
|
||||
Matched: false, Value: null, Quality: null, TimedOut: true,
|
||||
Success: true, ErrorMessage: null, DateTimeOffset.UtcNow));
|
||||
|
||||
await CreateHelper().To("inst-1")
|
||||
.WaitForAttribute("Flag", true, TimeSpan.FromSeconds(30), explicitCts.Token);
|
||||
|
||||
Assert.True(seen.IsCancellationRequested); // the explicit caller token cancels the wait
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task WaitForAttribute_ClientDisconnect_CancelsTheWait()
|
||||
{
|
||||
// A client disconnect (the raw request-abort token, threaded via WithRequestAborted)
|
||||
// still cancels the wait even though the method deadline does not bound it.
|
||||
SiteResolves("inst-1", "SiteA");
|
||||
using var clientAbort = new CancellationTokenSource();
|
||||
await clientAbort.CancelAsync();
|
||||
CancellationToken seen = default;
|
||||
_router.RouteToWaitForAttributeAsync("SiteA", Arg.Any<RouteToWaitForAttributeRequest>(), Arg.Do<CancellationToken>(t => seen = t))
|
||||
.Returns(ci => new RouteToWaitForAttributeResponse(
|
||||
((RouteToWaitForAttributeRequest)ci[1]).CorrelationId,
|
||||
Matched: false, Value: null, Quality: null, TimedOut: true,
|
||||
Success: true, ErrorMessage: null, DateTimeOffset.UtcNow));
|
||||
|
||||
var bound = CreateHelper().WithRequestAborted(clientAbort.Token);
|
||||
await bound.To("inst-1").WaitForAttribute("Flag", true, TimeSpan.FromSeconds(30));
|
||||
|
||||
Assert.Equal(deadline.Token, seen);
|
||||
Assert.True(seen.IsCancellationRequested); // the client-abort token cancels the wait
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
||||
Reference in New Issue
Block a user