feat(management): actor dispatch for BrowseNode/SearchAddressSpace/VerifyEndpoint — CLI parity (arch-review C4)
Made CommunicationService.BrowseNodeAsync/SearchAddressSpaceAsync/VerifyEndpointAsync virtual to support the test seam (mirrors existing virtual WriteTagAsync). Claude-Session: https://claude.ai/code/session_01MtdgwpEeCUn6cUA5f1LMPj
This commit is contained in:
@@ -2,13 +2,16 @@ using Akka.Actor;
|
||||
using Akka.TestKit.Xunit2;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.Options;
|
||||
using NSubstitute;
|
||||
using ZB.MOM.WW.ScadaBridge.Communication;
|
||||
using NSubstitute.ExceptionExtensions;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Instances;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Schemas;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Entities.SecuredWrites;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Scripts;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Templates;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Protocol;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Repositories;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Services;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Messages.Management;
|
||||
@@ -3152,4 +3155,142 @@ public class ManagementActorTests : TestKit, IDisposable
|
||||
actor.Tell(Envelope(new CreateAreaCommand(1, "Area1", null), "Viewer"));
|
||||
ExpectMsg<ManagementUnauthorized>(TimeSpan.FromSeconds(5));
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// Remote-query actor dispatch for the OPC UA design-time commands
|
||||
// (arch-review C4): BrowseNode / SearchAddressSpace / VerifyEndpoint gain
|
||||
// actor handlers so the management API/CLI reaches parity with the UI.
|
||||
// SiteIdentifier is REQUIRED (the actor must route to a site); Designer-gated.
|
||||
// ========================================================================
|
||||
|
||||
[Fact]
|
||||
public void BrowseNode_WithoutSiteIdentifier_ReturnsError()
|
||||
{
|
||||
var actor = CreateActor();
|
||||
actor.Tell(Envelope(new BrowseNodeCommand("conn", null, null), "Designer"));
|
||||
var resp = ExpectMsg<ManagementError>(TimeSpan.FromSeconds(5));
|
||||
Assert.Contains("SiteIdentifier", resp.Error);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BrowseNode_Designer_RelaysToSite()
|
||||
{
|
||||
var stub = new StubCommunicationService();
|
||||
_services.AddSingleton<CommunicationService>(stub);
|
||||
var actor = CreateActor();
|
||||
actor.Tell(Envelope(new BrowseNodeCommand("conn", null, null, "SITE1"), "Designer"));
|
||||
ExpectMsg<ManagementSuccess>(TimeSpan.FromSeconds(5));
|
||||
Assert.Equal(1, stub.BrowseCount);
|
||||
Assert.Equal("SITE1", stub.LastBrowseSite);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BrowseNode_ViewerOnly_Unauthorized()
|
||||
{
|
||||
var actor = CreateActor();
|
||||
actor.Tell(Envelope(new BrowseNodeCommand("conn", null, null, "SITE1"), "Viewer"));
|
||||
ExpectMsg<ManagementUnauthorized>(TimeSpan.FromSeconds(5));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SearchAddressSpace_WithoutSiteIdentifier_ReturnsError()
|
||||
{
|
||||
var actor = CreateActor();
|
||||
actor.Tell(Envelope(new SearchAddressSpaceCommand("conn", "q", 3, 50), "Designer"));
|
||||
var resp = ExpectMsg<ManagementError>(TimeSpan.FromSeconds(5));
|
||||
Assert.Contains("SiteIdentifier", resp.Error);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SearchAddressSpace_Designer_RelaysToSite()
|
||||
{
|
||||
var stub = new StubCommunicationService();
|
||||
_services.AddSingleton<CommunicationService>(stub);
|
||||
var actor = CreateActor();
|
||||
actor.Tell(Envelope(new SearchAddressSpaceCommand("conn", "q", 3, 50, "SITE1"), "Designer"));
|
||||
ExpectMsg<ManagementSuccess>(TimeSpan.FromSeconds(5));
|
||||
Assert.Equal(1, stub.SearchCount);
|
||||
Assert.Equal("SITE1", stub.LastSearchSite);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SearchAddressSpace_ViewerOnly_Unauthorized()
|
||||
{
|
||||
var actor = CreateActor();
|
||||
actor.Tell(Envelope(new SearchAddressSpaceCommand("conn", "q", 3, 50, "SITE1"), "Viewer"));
|
||||
ExpectMsg<ManagementUnauthorized>(TimeSpan.FromSeconds(5));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VerifyEndpoint_WithoutSiteIdentifier_ReturnsError()
|
||||
{
|
||||
var actor = CreateActor();
|
||||
actor.Tell(Envelope(new VerifyEndpointCommand("conn", "OpcUa", "{}"), "Designer"));
|
||||
var resp = ExpectMsg<ManagementError>(TimeSpan.FromSeconds(5));
|
||||
Assert.Contains("SiteIdentifier", resp.Error);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VerifyEndpoint_Designer_RelaysToSite()
|
||||
{
|
||||
var stub = new StubCommunicationService();
|
||||
_services.AddSingleton<CommunicationService>(stub);
|
||||
var actor = CreateActor();
|
||||
actor.Tell(Envelope(new VerifyEndpointCommand("conn", "OpcUa", "{}", "SITE1"), "Designer"));
|
||||
ExpectMsg<ManagementSuccess>(TimeSpan.FromSeconds(5));
|
||||
Assert.Equal(1, stub.VerifyCount);
|
||||
Assert.Equal("SITE1", stub.LastVerifySite);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VerifyEndpoint_ViewerOnly_Unauthorized()
|
||||
{
|
||||
var actor = CreateActor();
|
||||
actor.Tell(Envelope(new VerifyEndpointCommand("conn", "OpcUa", "{}", "SITE1"), "Viewer"));
|
||||
ExpectMsg<ManagementUnauthorized>(TimeSpan.FromSeconds(5));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Records remote-query relays for the actor-dispatch tests. The browse /
|
||||
/// search / verify / cert-trust methods on <see cref="CommunicationService"/>
|
||||
/// are virtual so the relay can be exercised without a live actor system.
|
||||
/// </summary>
|
||||
private sealed class StubCommunicationService : CommunicationService
|
||||
{
|
||||
public StubCommunicationService()
|
||||
: base(Options.Create(new CommunicationOptions()), NullLogger<CommunicationService>.Instance)
|
||||
{
|
||||
}
|
||||
|
||||
public int BrowseCount { get; private set; }
|
||||
public string? LastBrowseSite { get; private set; }
|
||||
public int SearchCount { get; private set; }
|
||||
public string? LastSearchSite { get; private set; }
|
||||
public int VerifyCount { get; private set; }
|
||||
public string? LastVerifySite { get; private set; }
|
||||
|
||||
public override Task<BrowseNodeResult> BrowseNodeAsync(
|
||||
string siteId, BrowseNodeCommand command, CancellationToken ct = default)
|
||||
{
|
||||
BrowseCount++;
|
||||
LastBrowseSite = siteId;
|
||||
return Task.FromResult(new BrowseNodeResult(Array.Empty<BrowseNode>(), false, null));
|
||||
}
|
||||
|
||||
public override Task<SearchAddressSpaceResult> SearchAddressSpaceAsync(
|
||||
string siteId, SearchAddressSpaceCommand command, CancellationToken ct = default)
|
||||
{
|
||||
SearchCount++;
|
||||
LastSearchSite = siteId;
|
||||
return Task.FromResult(new SearchAddressSpaceResult(Array.Empty<AddressSpaceMatch>(), false, null));
|
||||
}
|
||||
|
||||
public override Task<VerifyEndpointResult> VerifyEndpointAsync(
|
||||
string siteId, VerifyEndpointCommand command, CancellationToken ct = default)
|
||||
{
|
||||
VerifyCount++;
|
||||
LastVerifySite = siteId;
|
||||
return Task.FromResult(new VerifyEndpointResult(true, null, null, null));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user