Files
ScadaBridge/tests/ZB.MOM.WW.ScadaBridge.IntegrationTests/IntegrationSurfaceTests.cs
T
Joseph Doherty 513fa1aa16 docs(tests): fix CommentChecker findings across tests/ — 13,392 → 0 (repo-wide scan clean)
Comment/XML-doc changes only:
- add summaries to ~10k test methods, fixtures, fakes and helpers
  (+ <param> on theories, <returns> on async Task methods,
  <inheritdoc/> on interface implementations the checker can resolve)
- strip ~2.6k project-tracking references (WP*, P*-*, row-NN, Task N,
  #NN, Component-0NN) from comments, keeping the explanatory text;
  reword false positives in comments only
- real docs on fakes implementing BCL/library interfaces
  (IDisposable, ILogger, IOptionsMonitor, IAsyncLifetime)
- reword the last 5 src/ false positives (CWE-1236, failures-1,
  year-0001, M365)

Only non-comment edits: four one-line nested classes split across lines
so members can carry docs. Build 0 errors / 0 warnings.
2026-10-07 15:05:49 -04:00

182 lines
8.8 KiB
C#

using System.Net;
using System.Text.Json;
using NSubstitute;
using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Services;
using ZB.MOM.WW.ScadaBridge.InboundAPI;
namespace ZB.MOM.WW.ScadaBridge.IntegrationTests;
/// <summary>
/// End-to-end integration tests for Phase 7 integration surfaces.
/// </summary>
public class IntegrationSurfaceTests
{
// ── Inbound API: parameter validation + error codes ──
// Auth re-arch (C5): the in-repo ApiKeyValidator full-flow case was removed
// with the entity. Inbound auth now runs through the shared
// ZB.MOM.WW.Auth.ApiKeys verifier (Bearer sbk_<keyId>_<secret>); its coverage
// lives in the InboundAPI endpoint + Auth-library tests.
/// <summary>Verifies that inbound API parameter validation handles the extended types and reports the right error codes.</summary>
[Fact]
public void InboundAPI_ParameterValidation_ExtendedTypes()
{
// Validates the full extended type system: Boolean, Integer, Float, String, Object, List.
var definitions = JsonSerializer.Serialize(new[]
{
new { Name = "flag", Type = "Boolean", Required = true },
new { Name = "count", Type = "Integer", Required = true },
new { Name = "ratio", Type = "Float", Required = true },
new { Name = "name", Type = "String", Required = true },
new { Name = "config", Type = "Object", Required = true },
new { Name = "tags", Type = "List", Required = true }
});
var json = "{\"flag\":true,\"count\":42,\"ratio\":3.14,\"name\":\"test\",\"config\":{\"k\":\"v\"},\"tags\":[1,2]}";
using var doc = JsonDocument.Parse(json);
var result = ParameterValidator.Validate(doc.RootElement.Clone(), definitions);
Assert.True(result.IsValid);
Assert.Equal(true, result.Parameters["flag"]);
Assert.Equal((long)42, result.Parameters["count"]);
Assert.Equal(3.14, result.Parameters["ratio"]);
Assert.Equal("test", result.Parameters["name"]);
Assert.NotNull(result.Parameters["config"]);
Assert.NotNull(result.Parameters["tags"]);
}
// ── External System: error classification ──
/// <summary>Verifies that external system HTTP errors are classified as transient or permanent.</summary>
[Fact]
public void ExternalSystem_ErrorClassification_TransientVsPermanent()
{
// Verify the full classification spectrum
Assert.True(ExternalSystemGateway.ErrorClassifier.IsTransient(HttpStatusCode.InternalServerError));
Assert.True(ExternalSystemGateway.ErrorClassifier.IsTransient(HttpStatusCode.ServiceUnavailable));
Assert.True(ExternalSystemGateway.ErrorClassifier.IsTransient(HttpStatusCode.RequestTimeout));
Assert.True(ExternalSystemGateway.ErrorClassifier.IsTransient((HttpStatusCode)429));
Assert.False(ExternalSystemGateway.ErrorClassifier.IsTransient(HttpStatusCode.BadRequest));
Assert.False(ExternalSystemGateway.ErrorClassifier.IsTransient(HttpStatusCode.Unauthorized));
Assert.False(ExternalSystemGateway.ErrorClassifier.IsTransient(HttpStatusCode.Forbidden));
Assert.False(ExternalSystemGateway.ErrorClassifier.IsTransient(HttpStatusCode.NotFound));
}
// ── Notification: mock SMTP delivery ──
// The site-shaped NotificationDeliveryService that this case exercised
// was removed when sites stopped delivering notifications. The central SMTP
// delivery path is now covered end-to-end by
// ZB.MOM.WW.ScadaBridge.NotificationOutbox.Tests.Delivery.EmailNotificationDeliveryAdapterTests;
// no equivalent integration-surface assertion is needed here.
// ── Script Context: integration API wiring ──
/// <summary>Verifies that the script context routes external system calls to the configured client.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task ScriptContext_ExternalSystem_Call_Wired()
{
// Verify that ExternalSystem.Call is accessible from ScriptRuntimeContext
var mockClient = Substitute.For<IExternalSystemClient>();
mockClient.CallAsync("api", "getData", null, Arg.Any<CancellationToken>())
.Returns(new ExternalCallResult(true, "{\"value\":1}", null));
var context = CreateMinimalScriptContext(externalSystemClient: mockClient);
var result = await context.ExternalSystem.Call("api", "getData");
Assert.True(result.Success);
Assert.Equal("{\"value\":1}", result.ResponseJson);
}
/// <summary>Verifies that the script context routes notification sends to the configured service.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task ScriptContext_Notify_Send_Wired()
{
// Notification Outbox: Notify.Send enqueues into the site Store-and-Forward
// Engine and returns the NotificationId handle immediately.
// A real temp-file LocalDb, not the shared-cache in-memory database this used
// before: StoreAndForwardStorage takes ILocalDb now, and LocalDb has no
// in-memory mode.
using var localDb = ZB.MOM.WW.ScadaBridge.TestSupport.TestLocalDb.CreateTemp("NotifyWired");
var storage = new StoreAndForward.StoreAndForwardStorage(
localDb.Db, Microsoft.Extensions.Logging.Abstractions.NullLogger<StoreAndForward.StoreAndForwardStorage>.Instance);
await storage.InitializeAsync();
var saf = new StoreAndForward.StoreAndForwardService(
storage, new StoreAndForward.StoreAndForwardOptions(),
Microsoft.Extensions.Logging.Abstractions.NullLogger<StoreAndForward.StoreAndForwardService>.Instance);
var context = CreateMinimalScriptContext(storeAndForward: saf);
var notificationId = await context.Notify.To("ops").Send("Alert", "Body");
Assert.False(string.IsNullOrEmpty(notificationId));
var buffered = await saf.GetMessageByIdAsync(notificationId);
Assert.NotNull(buffered);
}
/// <summary>Verifies that an external system call throws when no client is configured.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task ScriptContext_ExternalSystem_NoClient_Throws()
{
var context = CreateMinimalScriptContext();
await Assert.ThrowsAsync<InvalidOperationException>(
() => context.ExternalSystem.Call("api", "method"));
}
/// <summary>Verifies that a database call throws when no gateway is configured.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task ScriptContext_Database_NoGateway_Throws()
{
var context = CreateMinimalScriptContext();
await Assert.ThrowsAsync<InvalidOperationException>(
() => context.Database.Connection("db"));
}
/// <summary>Verifies that a notification send throws when no service is configured.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task ScriptContext_Notify_NoService_Throws()
{
// No Store-and-Forward Engine wired → Notify.Send cannot enqueue and throws.
var context = CreateMinimalScriptContext();
await Assert.ThrowsAsync<InvalidOperationException>(
() => context.Notify.To("list").Send("subj", "body"));
}
private static SiteRuntime.Scripts.ScriptRuntimeContext CreateMinimalScriptContext(
IExternalSystemClient? externalSystemClient = null,
IDatabaseGateway? databaseGateway = null,
StoreAndForward.StoreAndForwardService? storeAndForward = null)
{
// Create a minimal context — we use Substitute.For<IActorRef> which is fine since
// we won't exercise Akka functionality in these tests.
var actorRef = Substitute.For<Akka.Actor.IActorRef>();
var compilationService = new SiteRuntime.Scripts.ScriptCompilationService(
Microsoft.Extensions.Logging.Abstractions.NullLogger<SiteRuntime.Scripts.ScriptCompilationService>.Instance);
var sharedLibrary = new SiteRuntime.Scripts.SharedScriptLibrary(
compilationService,
Microsoft.Extensions.Logging.Abstractions.NullLogger<SiteRuntime.Scripts.SharedScriptLibrary>.Instance);
return new SiteRuntime.Scripts.ScriptRuntimeContext(
actorRef,
sharedLibrary,
currentCallDepth: 0,
maxCallDepth: 10,
askTimeout: TimeSpan.FromSeconds(5),
instanceName: "test-instance",
logger: Microsoft.Extensions.Logging.Abstractions.NullLogger.Instance,
externalSystemClient: externalSystemClient,
databaseGateway: databaseGateway,
storeAndForward: storeAndForward,
siteId: "test-site");
}
}