The script-analysis sandbox Notify surface was stale after the Notification Outbox change: SandboxNotifyTarget.Send returned Task<NotificationResult> and there was no Status method, while production NotifyTarget.Send returns Task<string> (a NotificationId) plus NotifyHelper.Status. A script that test-ran cleanly in the sandbox would not compile against the real site runtime. - Move the NotificationDeliveryStatus record from ScadaLink.SiteRuntime.Scripts into ScadaLink.Commons.Messages.Notification so both production and the CentralUI sandbox reference the exact same type (CentralUI does not, and should not, reference SiteRuntime). Production NotifyHelper.Status is otherwise untouched. - Rewrite SandboxNotifyHelper/SandboxNotifyTarget to be a signature-faithful no-op fake: Send returns Task<string> (a fake NotificationId), Status returns Task<NotificationDeliveryStatus>. Production now enqueues into the site S&F engine, which has no central-side equivalent in the sandbox, so the fake no longer carries an INotificationDeliveryService. - Add script-analysis tests proving a script using the new Notify shape both diagnoses clean and runs in the sandbox.
143 lines
5.4 KiB
C#
143 lines
5.4 KiB
C#
using System.Data.Common;
|
|
using ScadaLink.Commons.Interfaces.Services;
|
|
using ScadaLink.Commons.Messages.Notification;
|
|
|
|
namespace ScadaLink.CentralUI.ScriptAnalysis;
|
|
|
|
/// <summary>
|
|
/// User-facing surface for <c>ExternalSystem.Call</c> /
|
|
/// <c>ExternalSystem.CachedCall</c> inside a Test Run. Mirrors
|
|
/// ExternalSystemHelper in ScadaLink.SiteRuntime.Scripts.ScriptRuntimeContext
|
|
/// so the same user code compiles against both. When constructed with a null
|
|
/// client (the editor's metadata-only analysis pass) every call throws
|
|
/// <see cref="ScriptSandboxException"/>; with a real client wired in (a Test
|
|
/// Run) calls hit the live HTTP path.
|
|
/// </summary>
|
|
public class SandboxExternalHelper
|
|
{
|
|
private readonly IExternalSystemClient? _client;
|
|
private readonly string _instanceName;
|
|
|
|
public SandboxExternalHelper(IExternalSystemClient? client, string instanceName)
|
|
{
|
|
_client = client;
|
|
_instanceName = instanceName;
|
|
}
|
|
|
|
public Task<ExternalCallResult> Call(
|
|
string systemName,
|
|
string methodName,
|
|
IReadOnlyDictionary<string, object?>? parameters = null,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
if (_client == null)
|
|
throw new ScriptSandboxException(
|
|
$"External.Call(\"{systemName}\", \"{methodName}\") — external system client not configured for Test Run.");
|
|
return _client.CallAsync(systemName, methodName, parameters, cancellationToken);
|
|
}
|
|
|
|
public Task<ExternalCallResult> CachedCall(
|
|
string systemName,
|
|
string methodName,
|
|
IReadOnlyDictionary<string, object?>? parameters = null,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
if (_client == null)
|
|
throw new ScriptSandboxException(
|
|
$"External.CachedCall(\"{systemName}\", \"{methodName}\") — external system client not configured for Test Run.");
|
|
return _client.CachedCallAsync(systemName, methodName, parameters, _instanceName, cancellationToken);
|
|
}
|
|
}
|
|
|
|
public class SandboxDatabaseHelper
|
|
{
|
|
private readonly IDatabaseGateway? _gateway;
|
|
private readonly string _instanceName;
|
|
|
|
public SandboxDatabaseHelper(IDatabaseGateway? gateway, string instanceName)
|
|
{
|
|
_gateway = gateway;
|
|
_instanceName = instanceName;
|
|
}
|
|
|
|
public Task<DbConnection> Connection(string name, CancellationToken cancellationToken = default)
|
|
{
|
|
if (_gateway == null)
|
|
throw new ScriptSandboxException(
|
|
$"Database.Connection(\"{name}\") — database gateway not configured for Test Run.");
|
|
return _gateway.GetConnectionAsync(name, cancellationToken);
|
|
}
|
|
|
|
public Task CachedWrite(
|
|
string name,
|
|
string sql,
|
|
IReadOnlyDictionary<string, object?>? parameters = null,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
if (_gateway == null)
|
|
throw new ScriptSandboxException(
|
|
$"Database.CachedWrite(\"{name}\") — database gateway not configured for Test Run.");
|
|
return _gateway.CachedWriteAsync(name, sql, parameters, _instanceName, cancellationToken);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Sandbox mirror of <c>ScadaLink.SiteRuntime.Scripts.NotifyHelper</c> — the
|
|
/// <c>Notify</c> global. Signature-faithful to production so the same user code
|
|
/// (<c>Notify.To(...).Send(...)</c> / <c>Notify.Status(...)</c>) compiles
|
|
/// identically against both surfaces.
|
|
///
|
|
/// In the Notification Outbox design production no longer delivers notification
|
|
/// email inline — <c>Notify.Send</c> enqueues into the site Store-and-Forward
|
|
/// Engine and returns a <c>NotificationId</c>. The sandbox has no S&F engine
|
|
/// and no central, so it is a pure no-op fake: <c>Send</c> returns a generated
|
|
/// fake id and <c>Status</c> returns a placeholder <see cref="NotificationDeliveryStatus"/>.
|
|
/// Nothing is delivered.
|
|
/// </summary>
|
|
public class SandboxNotifyHelper
|
|
{
|
|
private readonly string _instanceName;
|
|
|
|
public SandboxNotifyHelper(string instanceName)
|
|
{
|
|
_instanceName = instanceName;
|
|
}
|
|
|
|
/// <summary>Selects the notification list to send to.</summary>
|
|
public SandboxNotifyTarget To(string listName) =>
|
|
new(listName, _instanceName);
|
|
|
|
/// <summary>
|
|
/// Queries the delivery status of a previously-sent notification. The
|
|
/// sandbox never delivers, so this always reports the placeholder
|
|
/// <c>Unknown</c> status — it exists for signature fidelity with
|
|
/// <c>NotifyHelper.Status</c>.
|
|
/// </summary>
|
|
public Task<NotificationDeliveryStatus> Status(string notificationId) =>
|
|
Task.FromResult(new NotificationDeliveryStatus("Unknown", 0, null, null));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Sandbox mirror of <c>ScadaLink.SiteRuntime.Scripts.NotifyTarget</c> — the
|
|
/// target of <c>Notify.To("listName")</c>.
|
|
/// </summary>
|
|
public class SandboxNotifyTarget
|
|
{
|
|
private readonly string _listName;
|
|
private readonly string _instanceName;
|
|
|
|
internal SandboxNotifyTarget(string listName, string instanceName)
|
|
{
|
|
_listName = listName;
|
|
_instanceName = instanceName;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Mirrors <c>NotifyTarget.Send</c> — returns a <c>NotificationId</c>. In
|
|
/// the sandbox nothing is enqueued or delivered; a fake id is returned so
|
|
/// the call type-checks identically to production.
|
|
/// </summary>
|
|
public Task<string> Send(string subject, string message, CancellationToken cancellationToken = default) =>
|
|
Task.FromResult(Guid.NewGuid().ToString("N"));
|
|
}
|