refactor: rename ScadaLink → ZB.MOM.WW.ScadaBridge (code + projects + namespaces)
Solution + 23 src projects + 26 test projects renamed; folders, csproj, namespaces, and ScadaLinkDbContext/ScadaBridgeDbContext class updated. ActorSystem "scadalink" → "scadabridge", Akka seed-node URLs migrated. SQL roles/logins, LDAP domains, CLI command name, and CLI config dir (~/.scadalink → ~/.scadabridge) also renamed. Build green; 5 Host.Tests fail awaiting SQL login rename in next commit. Pre-existing StaleTagMonitor timing flakes unchanged. Rename script committed at tools/rename-to-scadabridge.sh.
This commit is contained in:
@@ -0,0 +1,204 @@
|
||||
using System.Net;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using NSubstitute;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Entities.InboundApi;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Repositories;
|
||||
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: auth + routing + parameter validation + error codes ──
|
||||
|
||||
[Fact]
|
||||
public async Task InboundAPI_ApiKeyValidator_FullFlow_EndToEnd()
|
||||
{
|
||||
// Validates that ApiKeyValidator correctly chains all checks.
|
||||
var repository = Substitute.For<IInboundApiRepository>();
|
||||
var key = new ApiKey("test-key", "key-value-123") { Id = 1, IsEnabled = true };
|
||||
var method = new ApiMethod("getStatus", "return 1;")
|
||||
{
|
||||
Id = 10,
|
||||
ParameterDefinitions = "[{\"Name\":\"deviceId\",\"Type\":\"String\",\"Required\":true}]",
|
||||
TimeoutSeconds = 30
|
||||
};
|
||||
|
||||
// ConfigurationDatabase-012: the validator fetches every key and matches
|
||||
// the candidate by HMAC hash in constant time (no secret-equality lookup).
|
||||
repository.GetAllApiKeysAsync().Returns(new List<ApiKey> { key });
|
||||
repository.GetMethodByNameAsync("getStatus").Returns(method);
|
||||
repository.GetApprovedKeysForMethodAsync(10).Returns(new List<ApiKey> { key });
|
||||
|
||||
var validator = new ApiKeyValidator(repository);
|
||||
|
||||
// Valid key + approved method
|
||||
var result = await validator.ValidateAsync("key-value-123", "getStatus");
|
||||
Assert.True(result.IsValid);
|
||||
Assert.Equal(method, result.Method);
|
||||
|
||||
// Then validate parameters
|
||||
using var doc = JsonDocument.Parse("{\"deviceId\": \"pump-01\"}");
|
||||
var paramResult = ParameterValidator.Validate(doc.RootElement.Clone(), method.ParameterDefinitions);
|
||||
Assert.True(paramResult.IsValid);
|
||||
Assert.Equal("pump-01", paramResult.Parameters["deviceId"]);
|
||||
}
|
||||
|
||||
[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");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user