Phase 8: Production readiness — failover tests, security hardening, sandboxing, deployment docs

- WP-1-3: Central/site failover + dual-node recovery tests (17 tests)
- WP-4: Performance testing framework for target scale (7 tests)
- WP-5: Security hardening (LDAPS, JWT key length, no secrets in logs) (11 tests)
- WP-6: Script sandboxing adversarial tests (28 tests, all forbidden APIs)
- WP-7: Recovery drill test scaffolds (5 tests)
- WP-8: Observability validation (structured logs, correlation IDs, metrics) (6 tests)
- WP-9: Message contract compatibility (forward/backward compat) (18 tests)
- WP-10: Deployment packaging (installation guide, production checklist, topology)
- WP-11: Operational runbooks (failover, troubleshooting, maintenance)
92 new tests, all passing. Zero warnings.
This commit is contained in:
Joseph Doherty
2026-03-16 22:12:31 -04:00
parent 3b2320bd35
commit b659978764
68 changed files with 6253 additions and 44 deletions
@@ -0,0 +1,109 @@
using System.Text.Json;
using Microsoft.Extensions.Logging;
using ScadaLink.Commons.Entities.InboundApi;
namespace ScadaLink.InboundAPI;
/// <summary>
/// WP-3: Executes the C# script associated with an inbound API method.
/// The script receives input parameters and a route helper, and returns a result
/// that is serialized as the JSON response.
///
/// In a full implementation this would use Roslyn scripting. For now, scripts
/// are a simple dispatch table so the rest of the pipeline can be tested end-to-end.
/// </summary>
public class InboundScriptExecutor
{
private readonly ILogger<InboundScriptExecutor> _logger;
private readonly Dictionary<string, Func<InboundScriptContext, Task<object?>>> _scriptHandlers = new();
public InboundScriptExecutor(ILogger<InboundScriptExecutor> logger)
{
_logger = logger;
}
/// <summary>
/// Registers a compiled script handler for a method name.
/// In production, this would be called after Roslyn compilation of the method's Script property.
/// </summary>
public void RegisterHandler(string methodName, Func<InboundScriptContext, Task<object?>> handler)
{
_scriptHandlers[methodName] = handler;
}
/// <summary>
/// Executes the script for the given method with the provided context.
/// </summary>
public async Task<InboundScriptResult> ExecuteAsync(
ApiMethod method,
IReadOnlyDictionary<string, object?> parameters,
RouteHelper route,
TimeSpan timeout,
CancellationToken cancellationToken = default)
{
try
{
using var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
cts.CancelAfter(timeout);
var context = new InboundScriptContext(parameters, route, cts.Token);
object? result;
if (_scriptHandlers.TryGetValue(method.Name, out var handler))
{
result = await handler(context).WaitAsync(cts.Token);
}
else
{
// No compiled handler — this means the script hasn't been registered.
// In production, we'd compile the method.Script and cache it.
return new InboundScriptResult(false, null, "Script not compiled or registered for this method");
}
var resultJson = result != null
? JsonSerializer.Serialize(result)
: null;
return new InboundScriptResult(true, resultJson, null);
}
catch (OperationCanceledException)
{
_logger.LogWarning("Script execution timed out for method {Method}", method.Name);
return new InboundScriptResult(false, null, "Script execution timed out");
}
catch (Exception ex)
{
_logger.LogError(ex, "Script execution failed for method {Method}", method.Name);
// WP-5: Safe error message, no internal details
return new InboundScriptResult(false, null, "Internal script error");
}
}
}
/// <summary>
/// Context provided to inbound API scripts.
/// </summary>
public class InboundScriptContext
{
public IReadOnlyDictionary<string, object?> Parameters { get; }
public RouteHelper Route { get; }
public CancellationToken CancellationToken { get; }
public InboundScriptContext(
IReadOnlyDictionary<string, object?> parameters,
RouteHelper route,
CancellationToken cancellationToken = default)
{
Parameters = parameters;
Route = route;
CancellationToken = cancellationToken;
}
}
/// <summary>
/// Result of executing an inbound API script.
/// </summary>
public record InboundScriptResult(
bool Success,
string? ResultJson,
string? ErrorMessage);