171 lines
7.7 KiB
C#
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");
|
|
}
|
|
}
|