test: add ManagementActor unit tests (12 tests, authorization + data flow + errors)

This commit is contained in:
Joseph Doherty
2026-03-17 14:57:46 -04:00
parent 229287cfd2
commit d41e156fe4

View File

@@ -0,0 +1,300 @@
using Akka.Actor;
using Akka.TestKit.Xunit2;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging.Abstractions;
using NSubstitute;
using NSubstitute.ExceptionExtensions;
using ScadaLink.Commons.Entities.Instances;
using ScadaLink.Commons.Entities.Templates;
using ScadaLink.Commons.Interfaces.Repositories;
using ScadaLink.Commons.Interfaces.Services;
using ScadaLink.Commons.Messages.Management;
using ScadaLink.Commons.Types;
using ScadaLink.ManagementService;
using ScadaLink.TemplateEngine.Services;
namespace ScadaLink.ManagementService.Tests;
public class ManagementActorTests : TestKit, IDisposable
{
private readonly ITemplateEngineRepository _templateRepo;
private readonly IAuditService _auditService;
private readonly ServiceCollection _services;
public ManagementActorTests()
{
_templateRepo = Substitute.For<ITemplateEngineRepository>();
_auditService = Substitute.For<IAuditService>();
_services = new ServiceCollection();
_services.AddScoped(_ => _templateRepo);
_services.AddScoped(_ => _auditService);
_services.AddScoped<InstanceService>();
}
private IActorRef CreateActor()
{
var sp = _services.BuildServiceProvider();
return Sys.ActorOf(Props.Create(() => new ManagementActor(
sp, NullLogger<ManagementActor>.Instance)));
}
private static ManagementEnvelope Envelope(object command, params string[] roles) =>
new(new AuthenticatedUser("testuser", "Test User", roles, Array.Empty<string>()),
command, Guid.NewGuid().ToString("N"));
void IDisposable.Dispose()
{
Shutdown();
}
// ========================================================================
// 1. Authorization test -- admin command with wrong role
// ========================================================================
[Fact]
public void CreateSiteCommand_WithDesignRole_ReturnsUnauthorized()
{
var actor = CreateActor();
var envelope = Envelope(new CreateSiteCommand("Site1", "SITE1", "Desc"), "Design");
actor.Tell(envelope);
var response = ExpectMsg<ManagementUnauthorized>(TimeSpan.FromSeconds(5));
Assert.Contains("Admin", response.Message);
Assert.Equal(envelope.CorrelationId, response.CorrelationId);
}
[Fact]
public void CreateSiteCommand_WithNoRoles_ReturnsUnauthorized()
{
var actor = CreateActor();
var envelope = Envelope(new CreateSiteCommand("Site1", "SITE1", null));
actor.Tell(envelope);
var response = ExpectMsg<ManagementUnauthorized>(TimeSpan.FromSeconds(5));
Assert.Contains("Admin", response.Message);
}
[Fact]
public void DeploymentCommand_WithDesignRole_ReturnsUnauthorized()
{
var actor = CreateActor();
var envelope = Envelope(new CreateInstanceCommand("Inst1", 1, 1), "Design");
actor.Tell(envelope);
var response = ExpectMsg<ManagementUnauthorized>(TimeSpan.FromSeconds(5));
Assert.Contains("Deployment", response.Message);
}
// ========================================================================
// 2. Read-only query passes without special role
// ========================================================================
[Fact]
public void ListTemplatesCommand_WithNoRoles_ReturnsSuccess()
{
var templates = new List<Template> { new("Template1") { Id = 1 } };
_templateRepo.GetAllTemplatesAsync(Arg.Any<CancellationToken>())
.Returns(templates);
var actor = CreateActor();
var envelope = Envelope(new ListTemplatesCommand());
actor.Tell(envelope);
var response = ExpectMsg<ManagementSuccess>(TimeSpan.FromSeconds(5));
Assert.Equal(envelope.CorrelationId, response.CorrelationId);
}
[Fact]
public void ListSitesCommand_WithDesignRole_ReturnsSuccess()
{
// ListSitesCommand is read-only, any role should work
var siteRepo = Substitute.For<ISiteRepository>();
siteRepo.GetAllSitesAsync(Arg.Any<CancellationToken>())
.Returns(new List<Commons.Entities.Sites.Site>());
_services.AddScoped(_ => siteRepo);
var actor = CreateActor();
var envelope = Envelope(new ListSitesCommand(), "Design");
actor.Tell(envelope);
var response = ExpectMsg<ManagementSuccess>(TimeSpan.FromSeconds(5));
Assert.Equal(envelope.CorrelationId, response.CorrelationId);
}
// ========================================================================
// 3. Template list test -- verify data flows through
// ========================================================================
[Fact]
public void ListTemplatesCommand_ReturnsTemplateData()
{
var templates = new List<Template>
{
new("PumpTemplate") { Id = 1, Description = "Pump" },
new("ValveTemplate") { Id = 2, Description = "Valve" }
};
_templateRepo.GetAllTemplatesAsync(Arg.Any<CancellationToken>())
.Returns(templates);
var actor = CreateActor();
var envelope = Envelope(new ListTemplatesCommand());
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);
}
// ========================================================================
// 4. Instance create test
// ========================================================================
[Fact]
public void CreateInstanceCommand_WithDeploymentRole_ReturnsSuccess()
{
var createdInstance = new Instance("Pump1") { Id = 42, TemplateId = 1, SiteId = 1 };
// InstanceService calls GetTemplateByIdAsync to verify template exists
_templateRepo.GetTemplateByIdAsync(1, Arg.Any<CancellationToken>())
.Returns(new Template("PumpTemplate") { Id = 1 });
// InstanceService calls GetInstanceByUniqueNameAsync to check uniqueness
_templateRepo.GetInstanceByUniqueNameAsync("Pump1", Arg.Any<CancellationToken>())
.Returns((Instance?)null);
// InstanceService calls AddInstanceAsync then SaveChangesAsync
_templateRepo.AddInstanceAsync(Arg.Any<Instance>(), Arg.Any<CancellationToken>())
.Returns(Task.CompletedTask);
_templateRepo.SaveChangesAsync(Arg.Any<CancellationToken>())
.Returns(1);
_auditService.LogAsync(
Arg.Any<string>(), Arg.Any<string>(), Arg.Any<string>(),
Arg.Any<string>(), Arg.Any<string>(), Arg.Any<object?>(),
Arg.Any<CancellationToken>())
.Returns(Task.CompletedTask);
var actor = CreateActor();
var envelope = Envelope(
new CreateInstanceCommand("Pump1", 1, 1),
"Deployment");
actor.Tell(envelope);
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);
}
// ========================================================================
// 5. Error handling test -- service throws exception
// ========================================================================
[Fact]
public void ListTemplatesCommand_WhenRepoThrows_ReturnsManagementError()
{
_templateRepo.GetAllTemplatesAsync(Arg.Any<CancellationToken>())
.ThrowsAsync(new InvalidOperationException("Database connection lost"));
var actor = CreateActor();
var envelope = Envelope(new ListTemplatesCommand());
actor.Tell(envelope);
var response = ExpectMsg<ManagementError>(TimeSpan.FromSeconds(5));
Assert.Equal(envelope.CorrelationId, response.CorrelationId);
Assert.Equal("COMMAND_FAILED", response.ErrorCode);
Assert.Contains("Database connection lost", response.Error);
}
[Fact]
public void CreateInstanceCommand_WhenServiceFails_ReturnsManagementError()
{
// Template not found -- InstanceService will return Result.Failure
_templateRepo.GetTemplateByIdAsync(99, Arg.Any<CancellationToken>())
.Returns((Template?)null);
var actor = CreateActor();
var envelope = Envelope(
new CreateInstanceCommand("BadInst", 99, 1),
"Deployment");
actor.Tell(envelope);
var response = ExpectMsg<ManagementError>(TimeSpan.FromSeconds(5));
Assert.Equal(envelope.CorrelationId, response.CorrelationId);
Assert.Equal("COMMAND_FAILED", response.ErrorCode);
}
// ========================================================================
// Role boundary tests -- verify each category
// ========================================================================
[Fact]
public void DesignCommand_WithAdminRole_ReturnsUnauthorized()
{
// CreateTemplateCommand requires "Design" role, "Admin" alone is insufficient
var actor = CreateActor();
var envelope = Envelope(
new CreateTemplateCommand("T1", null, null),
"Admin");
actor.Tell(envelope);
var response = ExpectMsg<ManagementUnauthorized>(TimeSpan.FromSeconds(5));
Assert.Contains("Design", response.Message);
}
[Fact]
public void AdminCommand_WithAdminRole_Succeeds()
{
var siteRepo = Substitute.For<ISiteRepository>();
siteRepo.AddSiteAsync(Arg.Any<Commons.Entities.Sites.Site>(), Arg.Any<CancellationToken>())
.Returns(Task.CompletedTask);
siteRepo.SaveChangesAsync(Arg.Any<CancellationToken>())
.Returns(1);
_services.AddScoped(_ => siteRepo);
var actor = CreateActor();
var envelope = Envelope(
new CreateSiteCommand("NewSite", "NS1", "Desc"),
"Admin");
actor.Tell(envelope);
var response = ExpectMsg<ManagementSuccess>(TimeSpan.FromSeconds(5));
Assert.Equal(envelope.CorrelationId, response.CorrelationId);
}
[Fact]
public void RoleCheck_IsCaseInsensitive()
{
var siteRepo = Substitute.For<ISiteRepository>();
siteRepo.AddSiteAsync(Arg.Any<Commons.Entities.Sites.Site>(), Arg.Any<CancellationToken>())
.Returns(Task.CompletedTask);
siteRepo.SaveChangesAsync(Arg.Any<CancellationToken>())
.Returns(1);
_services.AddScoped(_ => siteRepo);
var actor = CreateActor();
// "admin" lowercase should still match "Admin" requirement
var envelope = Envelope(
new CreateSiteCommand("Site2", "S2", null),
"admin");
actor.Tell(envelope);
var response = ExpectMsg<ManagementSuccess>(TimeSpan.FromSeconds(5));
Assert.Equal(envelope.CorrelationId, response.CorrelationId);
}
}