feat: achieve CLI parity with Central UI
Add 33 new management message records, ManagementActor handlers, and CLI commands to close all functionality gaps between the Central UI and the Management CLI. New capabilities include: - Template member CRUD (attributes, alarms, scripts, compositions) - Shared script CRUD - Database connection definition CRUD - Inbound API method CRUD - LDAP scope rule management - API key enable/disable - Area update - Remote event log and parked message queries - Missing get/update commands for templates, sites, instances, data connections, external systems, notifications, and SMTP config Includes 12 new ManagementActor unit tests covering authorization, happy-path queries, and error handling. Updates CLI README and component design documents (Component-CLI.md, Component-ManagementService.md).
This commit is contained in:
@@ -11,6 +11,7 @@ using ScadaLink.Commons.Interfaces.Services;
|
||||
using ScadaLink.Commons.Messages.Management;
|
||||
using ScadaLink.Commons.Types;
|
||||
using ScadaLink.ManagementService;
|
||||
using ScadaLink.TemplateEngine;
|
||||
using ScadaLink.TemplateEngine.Services;
|
||||
|
||||
namespace ScadaLink.ManagementService.Tests;
|
||||
@@ -148,10 +149,9 @@ public class ManagementActorTests : TestKit, IDisposable
|
||||
actor.Tell(envelope);
|
||||
|
||||
var response = ExpectMsg<ManagementSuccess>(TimeSpan.FromSeconds(5));
|
||||
var data = Assert.IsAssignableFrom<IReadOnlyList<Template>>(response.Data);
|
||||
Assert.Equal(2, data.Count);
|
||||
Assert.Equal("PumpTemplate", data[0].Name);
|
||||
Assert.Equal("ValveTemplate", data[1].Name);
|
||||
Assert.NotNull(response.JsonData);
|
||||
Assert.Contains("PumpTemplate", response.JsonData);
|
||||
Assert.Contains("ValveTemplate", response.JsonData);
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
@@ -192,8 +192,8 @@ public class ManagementActorTests : TestKit, IDisposable
|
||||
|
||||
var response = ExpectMsg<ManagementSuccess>(TimeSpan.FromSeconds(5));
|
||||
Assert.Equal(envelope.CorrelationId, response.CorrelationId);
|
||||
var instance = Assert.IsType<Instance>(response.Data);
|
||||
Assert.Equal("Pump1", instance.UniqueName);
|
||||
Assert.NotNull(response.JsonData);
|
||||
Assert.Contains("Pump1", response.JsonData);
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
@@ -297,4 +297,185 @@ public class ManagementActorTests : TestKit, IDisposable
|
||||
var response = ExpectMsg<ManagementSuccess>(TimeSpan.FromSeconds(5));
|
||||
Assert.Equal(envelope.CorrelationId, response.CorrelationId);
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// New command authorization tests
|
||||
// ========================================================================
|
||||
|
||||
[Fact]
|
||||
public void SharedScriptCreate_WithAdminRole_ReturnsUnauthorized()
|
||||
{
|
||||
var actor = CreateActor();
|
||||
var envelope = Envelope(new CreateSharedScriptCommand("Script1", "code", null, null), "Admin");
|
||||
|
||||
actor.Tell(envelope);
|
||||
|
||||
var response = ExpectMsg<ManagementUnauthorized>(TimeSpan.FromSeconds(5));
|
||||
Assert.Contains("Design", response.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DatabaseConnectionCreate_WithDeploymentRole_ReturnsUnauthorized()
|
||||
{
|
||||
var actor = CreateActor();
|
||||
var envelope = Envelope(new CreateDatabaseConnectionDefCommand("DB1", "Server=test"), "Deployment");
|
||||
|
||||
actor.Tell(envelope);
|
||||
|
||||
var response = ExpectMsg<ManagementUnauthorized>(TimeSpan.FromSeconds(5));
|
||||
Assert.Contains("Design", response.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ApiMethodCreate_WithAdminRole_ReturnsUnauthorized()
|
||||
{
|
||||
var actor = CreateActor();
|
||||
var envelope = Envelope(new CreateApiMethodCommand("Method1", "code", 30, null, null), "Admin");
|
||||
|
||||
actor.Tell(envelope);
|
||||
|
||||
var response = ExpectMsg<ManagementUnauthorized>(TimeSpan.FromSeconds(5));
|
||||
Assert.Contains("Design", response.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AddTemplateAttribute_WithDeploymentRole_ReturnsUnauthorized()
|
||||
{
|
||||
var actor = CreateActor();
|
||||
var envelope = Envelope(new AddTemplateAttributeCommand(1, "Attr1", "Float", null, null, null, false), "Deployment");
|
||||
|
||||
actor.Tell(envelope);
|
||||
|
||||
var response = ExpectMsg<ManagementUnauthorized>(TimeSpan.FromSeconds(5));
|
||||
Assert.Contains("Design", response.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UpdateApiKey_WithDesignRole_ReturnsUnauthorized()
|
||||
{
|
||||
var actor = CreateActor();
|
||||
var envelope = Envelope(new UpdateApiKeyCommand(1, true), "Design");
|
||||
|
||||
actor.Tell(envelope);
|
||||
|
||||
var response = ExpectMsg<ManagementUnauthorized>(TimeSpan.FromSeconds(5));
|
||||
Assert.Contains("Admin", response.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AddScopeRule_WithDesignRole_ReturnsUnauthorized()
|
||||
{
|
||||
var actor = CreateActor();
|
||||
var envelope = Envelope(new AddScopeRuleCommand(1, 1), "Design");
|
||||
|
||||
actor.Tell(envelope);
|
||||
|
||||
var response = ExpectMsg<ManagementUnauthorized>(TimeSpan.FromSeconds(5));
|
||||
Assert.Contains("Admin", response.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UpdateArea_WithAdminRole_ReturnsUnauthorized()
|
||||
{
|
||||
var actor = CreateActor();
|
||||
var envelope = Envelope(new UpdateAreaCommand(1, "NewName"), "Admin");
|
||||
|
||||
actor.Tell(envelope);
|
||||
|
||||
var response = ExpectMsg<ManagementUnauthorized>(TimeSpan.FromSeconds(5));
|
||||
Assert.Contains("Design", response.Message);
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// New command read-only query tests (no role required)
|
||||
// ========================================================================
|
||||
|
||||
[Fact]
|
||||
public void ListSharedScripts_WithNoRoles_ReturnsSuccess()
|
||||
{
|
||||
_templateRepo.GetAllSharedScriptsAsync(Arg.Any<CancellationToken>())
|
||||
.Returns(new List<Commons.Entities.Scripts.SharedScript>());
|
||||
_services.AddScoped<SharedScriptService>();
|
||||
|
||||
var actor = CreateActor();
|
||||
var envelope = Envelope(new ListSharedScriptsCommand());
|
||||
|
||||
actor.Tell(envelope);
|
||||
|
||||
var response = ExpectMsg<ManagementSuccess>(TimeSpan.FromSeconds(5));
|
||||
Assert.Equal(envelope.CorrelationId, response.CorrelationId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ListDatabaseConnections_WithNoRoles_ReturnsSuccess()
|
||||
{
|
||||
var extRepo = Substitute.For<IExternalSystemRepository>();
|
||||
extRepo.GetAllDatabaseConnectionsAsync(Arg.Any<CancellationToken>())
|
||||
.Returns(new List<Commons.Entities.ExternalSystems.DatabaseConnectionDefinition>());
|
||||
_services.AddScoped(_ => extRepo);
|
||||
|
||||
var actor = CreateActor();
|
||||
var envelope = Envelope(new ListDatabaseConnectionsCommand());
|
||||
|
||||
actor.Tell(envelope);
|
||||
|
||||
var response = ExpectMsg<ManagementSuccess>(TimeSpan.FromSeconds(5));
|
||||
Assert.Equal(envelope.CorrelationId, response.CorrelationId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ListApiMethods_WithNoRoles_ReturnsSuccess()
|
||||
{
|
||||
var apiRepo = Substitute.For<IInboundApiRepository>();
|
||||
apiRepo.GetAllApiMethodsAsync(Arg.Any<CancellationToken>())
|
||||
.Returns(new List<Commons.Entities.InboundApi.ApiMethod>());
|
||||
_services.AddScoped(_ => apiRepo);
|
||||
|
||||
var actor = CreateActor();
|
||||
var envelope = Envelope(new ListApiMethodsCommand());
|
||||
|
||||
actor.Tell(envelope);
|
||||
|
||||
var response = ExpectMsg<ManagementSuccess>(TimeSpan.FromSeconds(5));
|
||||
Assert.Equal(envelope.CorrelationId, response.CorrelationId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ListScopeRules_WithAdminRole_ReturnsSuccess()
|
||||
{
|
||||
var secRepo = Substitute.For<ISecurityRepository>();
|
||||
secRepo.GetScopeRulesForMappingAsync(1, Arg.Any<CancellationToken>())
|
||||
.Returns(new List<Commons.Entities.Security.SiteScopeRule>());
|
||||
_services.AddScoped(_ => secRepo);
|
||||
|
||||
var actor = CreateActor();
|
||||
var envelope = Envelope(new ListScopeRulesCommand(1), "Admin");
|
||||
|
||||
actor.Tell(envelope);
|
||||
|
||||
var response = ExpectMsg<ManagementSuccess>(TimeSpan.FromSeconds(5));
|
||||
Assert.Equal(envelope.CorrelationId, response.CorrelationId);
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// New command error handling tests
|
||||
// ========================================================================
|
||||
|
||||
[Fact]
|
||||
public void ListDatabaseConnections_WhenRepoThrows_ReturnsError()
|
||||
{
|
||||
var extRepo = Substitute.For<IExternalSystemRepository>();
|
||||
extRepo.GetAllDatabaseConnectionsAsync(Arg.Any<CancellationToken>())
|
||||
.ThrowsAsync(new InvalidOperationException("Connection refused"));
|
||||
_services.AddScoped(_ => extRepo);
|
||||
|
||||
var actor = CreateActor();
|
||||
var envelope = Envelope(new ListDatabaseConnectionsCommand());
|
||||
|
||||
actor.Tell(envelope);
|
||||
|
||||
var response = ExpectMsg<ManagementError>(TimeSpan.FromSeconds(5));
|
||||
Assert.Equal("COMMAND_FAILED", response.ErrorCode);
|
||||
Assert.Contains("Connection refused", response.Error);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user