using System.Reflection; using Microsoft.AspNetCore.Mvc.Testing; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using ScadaLink.ClusterInfrastructure; using ScadaLink.Communication; using ScadaLink.DataConnectionLayer; using ScadaLink.ExternalSystemGateway; using ScadaLink.HealthMonitoring; using ScadaLink.Host; using ScadaLink.NotificationService; using ScadaLink.SiteEventLogging; using ScadaLink.SiteRuntime; using ScadaLink.StoreAndForward; namespace ScadaLink.Host.Tests; public class HostStartupTests : IDisposable { private readonly List _disposables = new(); public void Dispose() { foreach (var d in _disposables) { try { d.Dispose(); } catch { /* best effort */ } } } [Fact] public void CentralRole_StartsWithoutError() { // WebApplicationFactory replays Program.Main, which reads config from files. // Set the environment to Central so appsettings.Central.json is loaded, // and set DOTNET_ENVIRONMENT before the factory creates the host. var previousEnv = Environment.GetEnvironmentVariable("DOTNET_ENVIRONMENT"); try { Environment.SetEnvironmentVariable("DOTNET_ENVIRONMENT", "Central"); var 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:2551", ["ScadaLink:Cluster:SeedNodes:1"] = "akka.tcp://scadalink@localhost:2552", ["ScadaLink:Database:SkipMigrations"] = "true", }); }); builder.UseSetting("ScadaLink:Node:Role", "Central"); builder.UseSetting("ScadaLink:Database:SkipMigrations", "true"); }); _disposables.Add(factory); // Creating the server exercises the full DI container build and startup pipeline var client = factory.CreateClient(); _disposables.Add(client); // If we get here without exception, the central host started successfully Assert.NotNull(client); } finally { Environment.SetEnvironmentVariable("DOTNET_ENVIRONMENT", previousEnv); } } [Fact] public void SiteRole_StartsWithoutError() { var builder = Microsoft.Extensions.Hosting.Host.CreateDefaultBuilder(); builder.ConfigureAppConfiguration(config => { config.Sources.Clear(); config.AddInMemoryCollection(new Dictionary { ["ScadaLink:Node:Role"] = "Site", ["ScadaLink:Node:NodeHostname"] = "test-site", ["ScadaLink:Node:SiteId"] = "TestSite", ["ScadaLink:Node:RemotingPort"] = "8082", }); }); builder.ConfigureServices((context, services) => { // Shared components services.AddClusterInfrastructure(); services.AddCommunication(); services.AddHealthMonitoring(); services.AddExternalSystemGateway(); services.AddNotificationService(); // Site-only components services.AddSiteRuntime(); services.AddDataConnectionLayer(); services.AddStoreAndForward(); services.AddSiteEventLogging(); // Options binding (mirrors Program.cs site path) services.Configure(context.Configuration.GetSection("ScadaLink:Node")); services.Configure(context.Configuration.GetSection("ScadaLink:Cluster")); services.Configure(context.Configuration.GetSection("ScadaLink:Database")); services.Configure(context.Configuration.GetSection("ScadaLink:Communication")); services.Configure(context.Configuration.GetSection("ScadaLink:HealthMonitoring")); services.Configure(context.Configuration.GetSection("ScadaLink:Notification")); services.Configure(context.Configuration.GetSection("ScadaLink:Logging")); services.Configure(context.Configuration.GetSection("ScadaLink:DataConnection")); services.Configure(context.Configuration.GetSection("ScadaLink:StoreAndForward")); services.Configure(context.Configuration.GetSection("ScadaLink:SiteEventLog")); }); var host = builder.Build(); _disposables.Add(host); // Build succeeds = DI container is valid and all services resolve Assert.NotNull(host); Assert.NotNull(host.Services); } [Fact] public void SiteRole_DoesNotConfigureKestrel() { var builder = Microsoft.Extensions.Hosting.Host.CreateDefaultBuilder(); builder.ConfigureAppConfiguration(config => { config.Sources.Clear(); config.AddInMemoryCollection(new Dictionary { ["ScadaLink:Node:Role"] = "Site", ["ScadaLink:Node:NodeHostname"] = "test-site", ["ScadaLink:Node:SiteId"] = "TestSite", ["ScadaLink:Node:RemotingPort"] = "8082", }); }); builder.ConfigureServices((context, services) => { services.AddClusterInfrastructure(); services.AddCommunication(); services.AddHealthMonitoring(); services.AddExternalSystemGateway(); services.AddNotificationService(); services.AddSiteRuntime(); services.AddDataConnectionLayer(); services.AddStoreAndForward(); services.AddSiteEventLogging(); }); var host = builder.Build(); // Verify no Kestrel server or web host is registered. // Host.CreateDefaultBuilder does not add Kestrel, so there should be no IServer. var serverType = Type.GetType( "Microsoft.AspNetCore.Hosting.Server.IServer, Microsoft.AspNetCore.Hosting.Server.Abstractions"); if (serverType != null) { var server = host.Services.GetService(serverType); Assert.Null(server); } // Additionally verify no HTTP URLs are configured var config = host.Services.GetRequiredService(); var urls = config["urls"] ?? config["ASPNETCORE_URLS"]; Assert.Null(urls); host.Dispose(); } [Fact] public void HostProject_DoesNotUseConditionalCompilation() { var hostProjectDir = FindHostProjectDirectory(); Assert.NotNull(hostProjectDir); var sourceFiles = Directory.GetFiles(hostProjectDir, "*.cs", SearchOption.TopDirectoryOnly); Assert.NotEmpty(sourceFiles); foreach (var file in sourceFiles) { var content = File.ReadAllText(file); Assert.DoesNotContain("#if", content); Assert.DoesNotContain("#ifdef", content); Assert.DoesNotContain("#ifndef", content); Assert.DoesNotContain("#elif", content); Assert.DoesNotContain("#else", content); Assert.DoesNotContain("#endif", content); } } private static string? FindHostProjectDirectory() { // Walk up from the test assembly location to find the src directory var assemblyDir = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)!; var dir = new DirectoryInfo(assemblyDir); while (dir != null) { var hostPath = Path.Combine(dir.FullName, "src", "ScadaLink.Host"); if (Directory.Exists(hostPath)) return hostPath; dir = dir.Parent; } return null; } }