Files

171 lines
7.7 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>
/// WP-14: 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.
[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 ──
[Fact]
public void ExternalSystem_ErrorClassification_TransientVsPermanent()
{
// WP-8: 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 ──
// NS-019: 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 ──
[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);
}
[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.
var dbName = $"NotifyWired_{Guid.NewGuid():N}";
var connStr = $"Data Source={dbName};Mode=Memory;Cache=Shared";
using var keepAlive = new Microsoft.Data.Sqlite.SqliteConnection(connStr);
keepAlive.Open();
var storage = new StoreAndForward.StoreAndForwardStorage(
connStr, 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);
}
[Fact]
public async Task ScriptContext_ExternalSystem_NoClient_Throws()
{
var context = CreateMinimalScriptContext();
await Assert.ThrowsAsync<InvalidOperationException>(
() => context.ExternalSystem.Call("api", "method"));
}
[Fact]
public async Task ScriptContext_Database_NoGateway_Throws()
{
var context = CreateMinimalScriptContext();
await Assert.ThrowsAsync<InvalidOperationException>(
() => context.Database.Connection("db"));
}
[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,
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");
}
}