using Akka.Actor; using Microsoft.AspNetCore.Mvc.Testing; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using ScadaLink.ConfigurationDatabase; using ScadaLink.Host; using ScadaLink.Host.Actors; namespace ScadaLink.Host.Tests; [CollectionDefinition("ActorSystem")] public class ActorSystemCollection : ICollectionFixture { } /// /// Verifies that all expected Central-role actors are created at the correct paths /// when AkkaHostedService starts. /// [Collection("ActorSystem")] public class CentralActorPathTests : IAsyncLifetime { private WebApplicationFactory? _factory; private ActorSystem? _actorSystem; private string? _previousEnv; public Task InitializeAsync() { _previousEnv = Environment.GetEnvironmentVariable("DOTNET_ENVIRONMENT"); Environment.SetEnvironmentVariable("DOTNET_ENVIRONMENT", "Central"); _factory = new WebApplicationFactory() .WithWebHostBuilder(builder => { builder.ConfigureAppConfiguration((_, config) => { config.AddInMemoryCollection(new Dictionary { ["ScadaLink:Node:NodeHostname"] = "localhost", ["ScadaLink:Node:RemotingPort"] = "0", ["ScadaLink:Cluster:SeedNodes:0"] = "akka.tcp://scadalink@localhost:25510", ["ScadaLink:Cluster:SeedNodes:1"] = "akka.tcp://scadalink@localhost:25520", ["ScadaLink:Cluster:MinNrOfMembers"] = "1", ["ScadaLink:Database:SkipMigrations"] = "true", ["ScadaLink:Security:JwtSigningKey"] = "test-signing-key-must-be-at-least-32-chars-long!", ["ScadaLink:Security:LdapServer"] = "localhost", ["ScadaLink:Security:LdapPort"] = "3893", ["ScadaLink:Security:LdapUseTls"] = "false", ["ScadaLink:Security:AllowInsecureLdap"] = "true", ["ScadaLink:Security:LdapSearchBase"] = "dc=scadalink,dc=local", }); }); builder.UseSetting("ScadaLink:Node:Role", "Central"); builder.UseSetting("ScadaLink:Database:SkipMigrations", "true"); builder.ConfigureServices(services => { // Replace SQL Server with in-memory database var descriptorsToRemove = services .Where(d => d.ServiceType == typeof(DbContextOptions) || d.ServiceType == typeof(DbContextOptions) || d.ServiceType == typeof(ScadaLinkDbContext) || d.ServiceType.FullName?.Contains("EntityFrameworkCore") == true) .ToList(); foreach (var d in descriptorsToRemove) services.Remove(d); services.AddDbContext(options => options.UseInMemoryDatabase($"ActorPathTests_{Guid.NewGuid()}")); }); }); // CreateClient triggers host startup including AkkaHostedService _ = _factory.CreateClient(); var akkaService = _factory.Services.GetRequiredService(); _actorSystem = akkaService.ActorSystem; return Task.CompletedTask; } public async Task DisposeAsync() { _factory?.Dispose(); Environment.SetEnvironmentVariable("DOTNET_ENVIRONMENT", _previousEnv); await Task.CompletedTask; } [Fact] public async Task CentralActors_DeadLetterMonitor_Exists() => await AssertActorExists("/user/dead-letter-monitor"); [Fact] public async Task CentralActors_CentralCommunication_Exists() => await AssertActorExists("/user/central-communication"); [Fact] public async Task CentralActors_Management_Exists() => await AssertActorExists("/user/management"); private async Task AssertActorExists(string path) { Assert.NotNull(_actorSystem); var selection = _actorSystem!.ActorSelection(path); var identity = await selection.Ask( new Identify(path), TimeSpan.FromSeconds(5)); Assert.NotNull(identity.Subject); } } /// /// Verifies that all expected Site-role actors are created at the correct paths /// when AkkaHostedService starts. /// [Collection("ActorSystem")] public class SiteActorPathTests : IAsyncLifetime { private IHost? _host; private ActorSystem? _actorSystem; private string _tempDbPath = null!; public async Task InitializeAsync() { _tempDbPath = Path.Combine(Path.GetTempPath(), $"scadalink_actor_test_{Guid.NewGuid()}.db"); var builder = Microsoft.Extensions.Hosting.Host.CreateDefaultBuilder(); builder.ConfigureAppConfiguration(config => { config.Sources.Clear(); config.AddInMemoryCollection(new Dictionary { ["ScadaLink:Node:Role"] = "Site", ["ScadaLink:Node:NodeHostname"] = "localhost", ["ScadaLink:Node:SiteId"] = "TestSite", ["ScadaLink:Node:RemotingPort"] = "0", ["ScadaLink:Cluster:SeedNodes:0"] = "akka.tcp://scadalink@localhost:25510", ["ScadaLink:Cluster:SeedNodes:1"] = "akka.tcp://scadalink@localhost:25520", ["ScadaLink:Cluster:MinNrOfMembers"] = "1", ["ScadaLink:Database:SiteDbPath"] = _tempDbPath, // Configure a dummy central contact point to trigger ClusterClient creation ["ScadaLink:Communication:CentralContactPoints:0"] = "akka.tcp://scadalink@localhost:25510", }); }); builder.ConfigureServices((context, services) => { SiteServiceRegistration.Configure(services, context.Configuration); }); _host = builder.Build(); await _host.StartAsync(); var akkaService = _host.Services.GetRequiredService(); _actorSystem = akkaService.ActorSystem; } public async Task DisposeAsync() { if (_host != null) { await _host.StopAsync(); _host.Dispose(); } try { File.Delete(_tempDbPath); } catch { /* best effort */ } } [Fact] public async Task SiteActors_DeadLetterMonitor_Exists() => await AssertActorExists("/user/dead-letter-monitor"); [Fact] public async Task SiteActors_DclManager_Exists() => await AssertActorExists("/user/dcl-manager"); [Fact] public async Task SiteActors_DeploymentManagerSingleton_Exists() => await AssertActorExists("/user/deployment-manager-singleton"); [Fact] public async Task SiteActors_DeploymentManagerProxy_Exists() => await AssertActorExists("/user/deployment-manager-proxy"); [Fact] public async Task SiteActors_SiteCommunication_Exists() => await AssertActorExists("/user/site-communication"); [Fact] public async Task SiteActors_CentralClusterClient_Exists() => await AssertActorExists("/user/central-cluster-client"); private async Task AssertActorExists(string path) { Assert.NotNull(_actorSystem); var selection = _actorSystem!.ActorSelection(path); var identity = await selection.Ask( new Identify(path), TimeSpan.FromSeconds(5)); Assert.NotNull(identity.Subject); } }